Files
visiome.neuroinf.jp/src/common/lib/XoopsCode.tsx
T
Yoshihiro OKUMURA 2b47d882f7 refactor: clear all biome lint diagnostics
Biome reported 229 diagnostics after aligning the config with
bsi-ni.brain.riken.jp. Fix them all so `biome check` passes clean
without per-rule overrides.

- replace `any` in LokiJS queries with an `ItemFilter` interface and
  a single `asLokiQuery()` cast, matching bsi-ni
- type the XOOPS redirect maps as
  `Record<string, { key: string; isNumber: boolean }>`
- use `ItemSubTypes<string>` instead of `any` for sub item types
- join author lists with `.join(', ')` instead of index-keyed nodes
- give list elements stable keys derived from their own data
- return `React.ReactNode` from `ListBase.renderBody()`
- drop the temporary `noArrayIndexKey` override from biome.json
2026-09-11 17:28:44 +09:00

161 lines
4.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 `<div class="xoopsCode"><pre><code>${text}</code></pre></div>`;
});
}
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] +
'<a href="' +
matches[2] +
'" target="_blank" rel="external noopener noreferrer">' +
matches[2] +
'</a>'
);
}
);
text = text.replace(
/(^|[^\]_a-zA-Z0-9-="'/:.]+)([a-zA-Z0-9.!#$%&*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)+)/g,
(...matches) => {
return `${matches[1]}<a href="mailto:${matches[2]}">${matches[2]}</a>`;
}
);
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, '<br />');
};
const cssConvert = (text: string): CSSProperties => {
const ret: Record<string, string> = {};
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 (
<HashLink key={idx} to={resolveUrl(url, basePath)} style={cssConvert(style)} title={title}>
{element.children.map((child, index) => convertNodeToElement(child, index, transform))}
</HashLink>
);
};
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 <div>{ReactHtmlParser(text, { transform: makeTransform(pathname) })}</div>;
};
export default XoopsCode;