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
161 lines
4.9 KiB
TypeScript
161 lines
4.9 KiB
TypeScript
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;
|