import ReactHtmlParser, { convertNodeToElement, type DomElement, type DomNode, type Transform, } from '@orrisroot/react-html-parser'; import type { CSSProperties } from 'react'; import { useLocation } from 'react-router'; import type { MultiLang } from '../../config'; import Functions from '../../functions'; import HashLink from './HashLink'; interface Props { lang: MultiLang; text: string; dohtml?: boolean; dosmiley?: boolean; doxcode?: boolean; doimage?: boolean; dobr?: boolean; } const preConvertXCode = (text: string, doxcode: boolean): string => { if (doxcode) { return text.replace(/\[code\](.*)\[\/code\]/gs, (_m0, m1) => { return `[code]${Functions.base64Encode(m1)}[/code]`; }); } return text; }; const postConvertXCode = (text: string, doxcode: boolean, doimage: boolean): string => { if (doxcode) { return text.replace(/\[code\](.*)\[\/code\]/gs, (_m0, m1) => { const text = convertXCode(Functions.htmlspecialchars(Functions.base64Decode(m1)), doimage); return `
${text}
`; }); } return text; }; const convertClickable = (text: string) => { text = text.replace( /(^|[^\]_a-zA-Z0-9-="'/]+)((?:https?|ftp)(?::\/\/[-_.!~*'()a-zA-Z0-9;/?:@&=+$,%#]+[a-zA-Z0-9=]))/g, (...matches) => { return ( matches[1] + '' + matches[2] + '' ); } ); text = text.replace( /(^|[^\]_a-zA-Z0-9-="'/:.]+)([a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)+)/g, (...matches) => { return `${matches[1]}${matches[2]}`; } ); return text; }; const convertXCode = (text: string, _doimage: boolean): string => { // TODO: implement return text; }; const convertSmiley = (text: string) => { // TODO: implement return text; }; const convertBr = (text: string): string => { return text.replace(/(\r?\n|\r)/g, '
'); }; const cssConvert = (text: string): CSSProperties => { const ret: Record = {}; text.split(';').forEach((line) => { const line_ = line.trim(); if (line_.length === 0) { return; } const kv = line_.split(':'); if (kv.length < 2) { return; } ret[Functions.camelCase(kv[0].trim())] = kv.slice(1).join(':').trim(); }); return ret as CSSProperties; }; /** * Stored content links are written relative to the page that contains them * (`node4.html`, `manual.html#SECTION1`, ...), so they have to be resolved the * way a browser resolves an `href`. react-router would instead resolve them * against the whole current pathname, turning `/manual_ja/node3.html` + * `node4.html` into `/manual_ja/node3.html/node4.html`. */ const resolveUrl = (href: string, basePath: string): string => { const url = new URL(href, window.location.origin + basePath); return url.pathname + url.search + url.hash; }; const makeTransform = (basePath: string): Transform => { const transform: Transform = (node: DomNode, idx: number | string) => { if (node.type !== 'tag') { return undefined; } const element = node as DomElement; if (element.name !== 'a') { return undefined; } const url = element.attribs?.href; const isExternal = !url || /^(mailto|https?:?\/\/)/.test(url); if (isExternal) { return undefined; } const style = element.attribs?.style || ''; const title = element.attribs?.title || ''; return ( {element.children.map((child, index) => convertNodeToElement(child, index, transform))} ); }; return transform; }; const XoopsCode = (props: Props) => { const { lang } = props; const { pathname } = useLocation(); let text = props.text; const dohtml = !!props.dohtml; const dosmiley = !!props.dosmiley; const doxcode = !!props.doxcode; const doimage = !!props.doimage; const dobr = !!props.dobr; text = preConvertXCode(text, doxcode); if (!dohtml) { text = Functions.htmlspecialchars(text); text = convertClickable(text); } if (dosmiley) { text = convertSmiley(text); } if (doxcode) { text = convertXCode(text, doimage); } if (dobr) { text = convertBr(text); } text = postConvertXCode(text, doxcode, doimage); text = Functions.mlang(text, lang); return
{ReactHtmlParser(text, { transform: makeTransform(pathname) })}
; }; export default XoopsCode;