feat(sdk, plugins): jsx support

This commit is contained in:
Artemy 2024-05-13 16:35:36 +03:00
parent bdf625bb1f
commit 3cee45b591
11 changed files with 58 additions and 15 deletions

View File

@ -0,0 +1,13 @@
import { Engine } from '@txtdot/sdk';
import { JSX } from '@txtdot/sdk';
const Habr = new Engine('Habr', 'Habr parser', ['*']);
Habr.route('*path', async (input, ro) => {
return {
content: <div>Test</div>,
};
});
export default Habr;

View File

@ -1,5 +1,6 @@
import StackOverflow from './stackoverflow'; import StackOverflow from './stackoverflow';
import Readability from './readability'; import Readability from './readability';
import SearX from './searx'; import SearX from './searx';
import Habr from './habr';
export { StackOverflow, Readability, SearX }; export { StackOverflow, Readability, SearX, Habr };

View File

@ -18,7 +18,7 @@ Readability.route('*path', async (input, ro) => {
} }
return { return {
document: parseHTML(parsed.content).document, content: parsed.content,
textContent: parsed.textContent, textContent: parsed.textContent,
title: parsed.title, title: parsed.title,
lang: parsed.lang, lang: parsed.lang,

View File

@ -46,7 +46,7 @@ async function search(
const textContent = articles_parsed.map((a) => a.text).join(''); const textContent = articles_parsed.map((a) => a.text).join('');
return { return {
document: parseHTML(content).document, content: content,
textContent, textContent,
title: `${search} - Searx - Page ${page}`, title: `${search} - Searx - Page ${page}`,
lang: document.documentElement.lang, lang: document.documentElement.lang,

View File

@ -16,9 +16,7 @@ async function questions(
const answers = allAnswers.map((a) => postParser(a)); const answers = allAnswers.map((a) => postParser(a));
return { return {
document: parseHTML( content: `${question}<hr>${answers.length} answers <hr>${answers.join('<hr>')}`,
`${question}<hr>${answers.length} answers <hr>${answers.join('<hr>')}`
).document,
textContent: `${ro.q.id}/${ro.q.slug}\nText output not supported`, // TODO textContent: `${ro.q.id}/${ro.q.slug}\nText output not supported`, // TODO
title, title,
lang: document.documentElement.lang, lang: document.documentElement.lang,

View File

@ -27,8 +27,7 @@ async function users(
.join('<br/>'); .join('<br/>');
return { return {
document: parseHTML(`${userInfo}<hr><h3>Top Posts</h3>${topPosts}`) content: `${userInfo}<hr><h3>Top Posts</h3>${topPosts}`,
.document,
textContent: `${ro.q.id}/${ro.q.slug}\n`, // TODO textContent: `${ro.q.id}/${ro.q.slug}\n`, // TODO
title: document.querySelector('title')?.textContent || '', title: document.querySelector('title')?.textContent || '',
lang: document.documentElement.lang, lang: document.documentElement.lang,

View File

@ -13,13 +13,13 @@
/* Language and Environment */ /* Language and Environment */
"target": "ES2020" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, "target": "ES2020" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
// "jsx": "preserve", /* Specify what JSX code is generated. */ "jsx": "react" /* Specify what JSX code is generated. */,
// "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
// "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ "reactNamespace": "JSX" /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */,
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */

24
packages/sdk/src/jsx.ts Normal file
View File

@ -0,0 +1,24 @@
// eslint-disable-next-line @typescript-eslint/no-namespace
export namespace JSX {
export type Element = string;
export interface IntrinsicElements {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[elemName: string]: any;
}
}
export function createElement(
name: string,
props: { [id: string]: string },
...content: string[]
) {
props = props || {};
const propsstr = Object.keys(props)
.map((key) => {
const value = props[key];
if (key === 'className') return `class=${value}`;
else return `${key}=${value}`;
})
.join(' ');
return `<${name} ${propsstr}>${content.join('')}</${name}>`;
}

View File

@ -18,6 +18,8 @@ import {
handlerSchema, handlerSchema,
} from './types/handler'; } from './types/handler';
import * as JSX from './jsx';
export { export {
Engine, Engine,
EngineParseError, EngineParseError,
@ -32,4 +34,5 @@ export {
HandlerOutput, HandlerOutput,
Route, Route,
handlerSchema, handlerSchema,
JSX,
}; };

View File

@ -33,7 +33,7 @@ export interface HandlerOutput {
} }
export interface EngineOutput { export interface EngineOutput {
document: Document; content: string;
textContent?: string; textContent?: string;
title?: string; title?: string;
lang?: string; lang?: string;

View File

@ -9,6 +9,7 @@ import replaceHref from './utils/replace-href';
import { Engine } from '@txtdot/sdk'; import { Engine } from '@txtdot/sdk';
import { HandlerInput, HandlerOutput } from '@txtdot/sdk'; import { HandlerInput, HandlerOutput } from '@txtdot/sdk';
import config from './config'; import config from './config';
import { parseHTML } from 'linkedom';
interface IEngineId { interface IEngineId {
[key: string]: number; [key: string]: number;
@ -51,6 +52,7 @@ export class Distributor {
} }
const engine = this.getFallbackEngine(urlObj.hostname, engineName); const engine = this.getFallbackEngine(urlObj.hostname, engineName);
const output = await engine.handle( const output = await engine.handle(
new HandlerInput( new HandlerInput(
await decodeStream(data, parseEncodingName(mime)), await decodeStream(data, parseEncodingName(mime)),
@ -58,22 +60,25 @@ export class Distributor {
) )
); );
const dom = parseHTML(output.content);
// post-process // post-process
// TODO: generate dom in handler and not parse here twice // TODO: generate dom in handler and not parse here twice
replaceHref( replaceHref(
output.document, dom.document,
requestUrl, requestUrl,
new URL(remoteUrl), new URL(remoteUrl),
engineName, engineName,
redirectPath redirectPath
); );
const purify = DOMPurify(); const purify = DOMPurify(dom);
const content = purify.sanitize(output.document.toString()); const content = purify.sanitize(output.content);
return { return {
content, content,
textContent: output.textContent || output.document.textContent || '', textContent:
output.textContent || dom.document.documentElement.textContent || '',
title: output.title, title: output.title,
lang: output.lang, lang: output.lang,
}; };