import React, { Component, PureComponent } from 'react'; import { format_time, Time, TitleLine } from './infrastructure/widgets'; import HtmlToReact from 'html-to-react'; import './Common.css'; import { URL_PID_RE, URL_RE, PID_RE, NICKNAME_RE, TAG_RE, split_text, } from './text_splitter'; import { save_config } from './Config'; import renderMd from './Markdown'; export { format_time, Time, TitleLine }; export const API_BASE = '/_api/v1'; // https://stackoverflow.com/questions/3446170/escape-string-for-use-in-javascript-regex function escape_regex(string) { return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string } function is_video(s) { try { let url = new URL(s); return ( url.pathname.endsWith('.mp4') || url.pathname.endsWith('.mov') || (url.searchParams.get('filetype') || '').startsWith('video/') ); } catch (e) { return false; } } export function build_highlight_re( txt, split = ' ', option = 'g', isRegex = false, ) { if (isRegex) { try { return new RegExp('(' + txt.slice(1, -1) + ')', option); } catch (e) { return /^$/g; } } else { return txt ? new RegExp( `(${txt .split(split) .filter((x) => !!x) .map(escape_regex) .join('|')})`, option, ) : /^$/g; } } export function ColoredSpan(props) { return ( {props.children} ); } function normalize_url(url) { return /^https?:\/\//.test(url) ? url : 'http://' + url; } function stop_loading(e) { e.target.parentNode.classList.remove('loading'); } // props: text, show_pid, color_picker, search_param export class HighlightedMarkdown extends Component { render() { const props = this.props; const processDefs = new HtmlToReact.ProcessNodeDefinitions(React); const processInstructions = [ { shouldProcessNode: (node) => /^h[123456]$/.test(node.name), processNode(node, children, index) { let currentLevel = +node.name[1]; if (currentLevel < 3) currentLevel = 3; const HeadingTag = `h${currentLevel}`; return {children}; }, }, { shouldProcessNode: (node) => node.name === 'img', processNode(node, index) { return ( {node.alt} ); }, }, { shouldProcessNode: (node) => node.name === 'a', processNode(node, children, index) { return ( {children} ); }, }, { shouldProcessNode(node) { return ( node.type === 'text' && (!node.parent || !node.parent.attribs || node.parent.attribs['encoding'] !== 'application/x-tex') ); // pid, nickname, search }, processNode(node, children, index) { const originalText = node.data; let rules = [ ['url_pid', URL_PID_RE], ['url', URL_RE], ['pid', PID_RE], ['nickname', NICKNAME_RE], ['tag', TAG_RE], ]; if (props.search_param) { let search_kws = props.search_param.split(' ').filter((s) => !!s); rules.push([ 'search', new RegExp( `(${search_kws .map((s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')) .join('|')})`, 'g', ), ]); } const splitted = split_text(originalText, rules); return ( {splitted.map(([rule, p], idx) => { return ( {rule === 'url_pid' ? ( /## ) : rule === 'url' ? ( <> {p} {is_video(p) && ( ); })} ); }, }, { shouldProcessNode: () => true, processNode: processDefs.processDefaultNode, }, ]; const parser = new HtmlToReact.Parser(); let rawMd = props.text; const renderedMarkdown = renderMd(rawMd); return ( parser.parseWithInstructions( renderedMarkdown, (node) => node.type !== 'script', processInstructions, ) || null ); } } window.TEXTAREA_BACKUP = {}; export class SafeTextarea extends Component { constructor(props) { super(props); this.state = { text: '', }; this.on_change_bound = this.on_change.bind(this); this.on_keydown_bound = this.on_keydown.bind(this); this.clear = this.clear.bind(this); this.area_ref = React.createRef(); this.change_callback = props.on_change || (() => {}); this.submit_callback = props.on_submit || (() => {}); } componentDidMount() { this.setState( { text: window.TEXTAREA_BACKUP[this.props.id] || '', }, () => { this.change_callback(this.state.text); }, ); } componentWillUnmount() { window.TEXTAREA_BACKUP[this.props.id] = this.state.text; this.change_callback(this.state.text); } on_change(event) { this.setState({ text: event.target.value, }); this.change_callback(event.target.value); } on_keydown(event) { if (event.key === 'Enter' && event.ctrlKey && !event.altKey) { event.preventDefault(); this.submit_callback(); } } clear() { this.setState({ text: '', }); } set(text) { this.change_callback(text); this.setState({ text: text, }); } get() { return this.state.text; } focus() { this.area_ref.current.focus(); } render() { return (