UNPKG

@podlite/to-jsx

Version:

JSX converter for Podlite markup language

476 lines (475 loc) 21.2 kB
import * as React from 'react'; import { createElement } from 'react'; import { podlite as podlite_core } from 'podlite'; import { parse, makeAttrs, emptyContent, content as nodeContent, setFn, subUse, toAny, isNamedBlock, isSemanticBlock, Writer, toAnyRules, frozenIds, getSafeNodeId, } from '@podlite/schema'; import { pluginCleanLocation as clean_plugin } from '@podlite/schema'; import { decodeHTMLStrict } from 'entities'; const helperMakeReact = ({ wrapElement }) => { let i_key_i = 0; let mapByType = {}; const getIdForNode = ({ type = 'notype', name = 'noname' }) => { const type_idx = `${type}_${name}`; if (!mapByType[type_idx]) { mapByType[type_idx] = 0; } ++mapByType[type_idx]; return `${type_idx}_${mapByType[type_idx]}`; }; return function (src, node, children, extraProps = {}, ctx = {}) { // for string return it if (typeof node == 'string') { return node; } const { ...attr } = node; const key = 'type' in node && node.type ? getIdForNode(node) : ++i_key_i; const result = typeof src === 'function' ? src({ ...attr, key, children }, children) : createElement(src, { ...extraProps, key }, children); // // create react element and pass key // if (!isValidElementType(src)) { // throw new Error(`Bad React element for ${ruleName} rule `) // } if (typeof wrapElement === 'function') { return wrapElement(node, result, ctx); } return result; }; }; export const Podlite = ({ children, ...options }) => { const result = podlite(children, options); return result; }; const mapToReact = (makeComponent) => { const mkComponent = src => (writer, processor) => (node, ctx, interator) => { // prepare extraProps for createElement // add id attribute if exists const id = getSafeNodeId(node, ctx); // check if node.content defined return makeComponent(src, node, 'content' in node ? interator(node.content, { ...ctx }) : [], { id }, ctx); }; // Handle nested block and :nested block attribute const handleNested = (defaultHandler, implicitLevel) => { return (writer, processor) => { const defaultHandlerInited = defaultHandler(writer, processor); return (node, ctx, interator) => { const nesting = makeAttrs(node, ctx).getFirstValue('nested') || implicitLevel; const children = defaultHandlerInited(node, ctx, interator); // if no nesting needs - simply return children if (!nesting) { return children; } const arr = [...Array(nesting).keys()]; return arr.reduce(acc => makeComponent('blockquote', node, acc), children); }; }; }; // Handle nested block and :nested block attribute const handleNotificationBlock = defaultHandler => { return (writer, processor) => { const defaultHandlerInited = defaultHandler(writer, processor); return (node, ctx, interator) => { const conf = makeAttrs(node, ctx); const notify = conf.getFirstValue('notify'); const caption = conf.exists('caption') ? conf.getFirstValue('caption') : null; const children = defaultHandlerInited(node, ctx, interator); // if no nesting needs - simply return children if (!notify) { return children; } return makeComponent(({ children, key }) => (React.createElement("aside", { className: `notify ${notify.toLowerCase()}`, key: key }, React.createElement("p", { className: "notify-title" }, caption || notify.charAt(0).toUpperCase() + notify.slice(1)), children)), node, children, {}, ctx); }; }; }; return { pod: mkComponent('div'), root: nodeContent, data: emptyContent(), ':ambient': emptyContent(), ':code': setFn((node, ctx) => { const id = getSafeNodeId(node, ctx); return mkComponent(({ children, key }) => (React.createElement("pre", { key: key, id: id }, React.createElement("code", null, children)))); }), code: setFn((node, ctx) => { const id = getSafeNodeId(node, ctx); const conf = makeAttrs(node, ctx); const caption = conf.exists('caption') ? conf.getFirstValue('caption') : null; return mkComponent(({ children, key }) => (React.createElement("div", { className: "code-block", key: `${key}-code-div` }, React.createElement("pre", { key: key, id: id }, React.createElement("code", null, children)), caption ? (React.createElement("div", { key: `${key}-caption`, className: "caption" }, caption)) : null))); }), image: nodeContent, ':image': setFn((node, ctx) => { return mkComponent(({ children, key }) => React.createElement("img", { key: key, src: node.src, alt: node.alt })); }), ':text': (writer, processor) => (node, ctx, interator) => { return node.value; }, ':verbatim': (writer, processor) => (node, ctx, interator) => { return node.value; }, 'head:block': subUse({ // inside head don't wrap into <p> ':para': nodeContent, }, setFn((node, ctx) => { const { level } = node; // TODO: refactor linking for blocks const id = getSafeNodeId(node, ctx); return mkComponent(({ level, children, key }) => createElement(`h${level}`, { key, id }, children)); })), ':blankline': emptyContent(), ':para': mkComponent('p'), para: handleNested(mkComponent('div')), 'comment:block': emptyContent(), defn: subUse([ // to avoid overlap para blocks handlers // define general :para at first { ':para': mkComponent('dd') }, { 'term:para': mkComponent('dt') }, ], nodeContent), nested: handleNotificationBlock(handleNested(nodeContent, 1)), output: mkComponent(({ children, key }) => (React.createElement("pre", { key: key }, React.createElement("samp", null, children)))), input: mkComponent(({ children, key }) => (React.createElement("pre", { key: key }, React.createElement("kbd", null, children)))), // TODO: add support for selectors, something like "data provider" include: emptyContent(), // Directives ':config': setFn((node, ctx) => { // setup context if (!ctx.hasOwnProperty('config')) ctx.config = {}; //collect configs in context ctx.config[node.name] = node.config; return emptyContent(); }), ':alias': setFn((node, ctx) => { // set alias if (!ctx.hasOwnProperty('alias')) ctx.alias = {}; //collect configs in context ctx.alias[node.name] = node.replacement; return emptyContent(); }), // Markup codes 'A<>': (writer, processor) => (node, ctx, interator) => { let term = node.content; if (typeof term !== 'string' && 'value' in term) { term = term.value; } //get replacement text if (!(ctx.alias && ctx.alias.hasOwnProperty(term))) { return makeComponent(({ children, key }) => React.createElement("code", { key: key }, "A<", children, ">"), node, interator(node.content, ctx)); } else { const src = ctx.alias[term].join('\n'); const tree_1 = processor(src); // now clean locations const tree = clean_plugin()(tree_1); if (tree[0].type === 'para') { return interator(tree[0].content, ctx); } else { return interator(tree, ctx); } } }, 'B<>': mkComponent('strong'), 'C<>': mkComponent('code'), 'E<>': (writer, processor) => (node, ctx, interator) => { if ('content' in node && Array.isArray(node.content)) return node.content .filter(e => e && e.type) .map(element => { if (element.type == 'number' && 'value' in element) { return String.fromCharCode(element.value); } if (element.type == 'html_named' && 'value' in element) { return decodeHTMLStrict(`&${element.value};`); } console.warn(`[jsx] E<> unsupported or unknown element type: ${element.type}`); return ''; }) .join(''); }, 'I<>': mkComponent('i'), 'K<>': mkComponent('kbd'), /** * CSS rules for footnotes .footnote a { text-decoration: none; } .footnotes { border-top-style: solid; border-top-width: 1px; border-top-color: #eee; } */ 'N<>': (writer, processor) => { writer.addListener('end', () => { if (!writer.hasOwnProperty('FOOTNOTES')) { return; } const footnotes = writer.FOOTNOTES; if (footnotes.length < 1) { return; } // if empty footnotes if (!writer.hasOwnProperty('postInterator')) { writer.postInterator = []; } const FootNotes = makeComponent(({ children, key }) => (React.createElement("div", { key: `${key}_FOOTNOTES`, className: "footnotes" }, footnotes.map((footnote, id) => { return (React.createElement("p", { key: id }, React.createElement("sup", { id: footnote.fnId, className: "footnote" }, React.createElement("a", { href: `#${footnote.fnRefId}` }, "[", footnote.gid, "]")), footnote.make())); }))), {}, []); writer.postInterator.push(FootNotes); }); return (node, ctx, interator) => { // skip empty notes if (node.content.length < 1) { return; } if (!writer.hasOwnProperty('gid')) { writer.gid = 1; } // get foot note id const gid = writer.gid++; const fnRefId = `fnref:${gid}`; const fnId = `fn:${gid}`; if (!writer.hasOwnProperty('FOOTNOTES')) { writer.FOOTNOTES = []; } writer.FOOTNOTES.push({ gid, fnRefId, fnId, node, make: () => { return interator(node.content, ctx); }, }); return makeComponent(({ children, key }) => (React.createElement("sup", { key: key, id: fnRefId, className: "footnote" }, React.createElement("a", { href: `#${fnId}` }, "[", gid, "]"))), node, []); }; }, 'R<>': mkComponent('var'), 'T<>': mkComponent('samp'), 'D<>': (writer, processor) => (node, ctx, interator) => { let { synonyms } = node; let definition = [node.content[0]]; if (synonyms) { definition = synonyms; } if (!writer.hasOwnProperty('DEFINITIONS')) { writer.DEFINITIONS = []; } writer.DEFINITIONS.push({ definition }); return makeComponent('dfn', node, interator(node.content, ctx)); }, 'L<>': setFn((node, ctx) => { let { meta } = node; if (meta === null) { meta = node.content; } //TODO: extract text from content array if (Array.isArray(meta)) { meta = meta[0]; } if (meta && typeof meta !== 'string' && 'value' in meta) { meta = meta.value; } return mkComponent(({ children, key }) => (React.createElement("a", { href: meta, key: key }, children))); }), 'S<>': (writer, processor) => (node, ctx, interator) => { let content = node.content || ''; if (typeof content !== 'string' && 'value' in content) { content = content.value; } const Content = content.split('').map((symbol, index) => { if (symbol === ' ') return '\u00a0'; if (symbol === '\n') return React.createElement("br", { key: index }); return symbol; }); return makeComponent(({ children, key }) => children, {}, Content); }, 'V<>': nodeContent, 'Z<>': emptyContent(), 'U<>': mkComponent('u'), 'X<>': (writer, processor) => (node, ctx, interator) => { let { entry } = node; if (entry === null && node.content.length > 0) { //@ts-ignore entry = [node.content[0]]; } // else { return } if (!writer.hasOwnProperty('INDEXTERMS')) { writer.INDEXTERMS = []; } writer.INDEXTERMS.push({ entry, }); return interator(node.content, ctx); }, 'Delete<>': mkComponent('del'), // table section table: (writer, processor) => (node, ctx, interator) => { const conf = makeAttrs(node, ctx); let attr = { caption: '' }; if (conf.exists('caption')) { const caption = conf.getFirstValue('caption'); attr.caption = caption; } if (typeof node === 'string') { return node; } if (!('content' in node)) { console.warn('[jsx] no content in node'); return ''; } const id = getSafeNodeId(node, ctx); return makeComponent(({ key, children }) => { return (React.createElement("table", { key: key, id: id }, React.createElement("caption", { className: "caption" }, attr.caption), React.createElement("tbody", null, children))); }, node, interator(node.content, { ...ctx, ...(node.align && { 'table.align': node.align }) })); }, ':separator': emptyContent(), table_row: setFn((node, ctx) => { if (ctx['table.align']) { ctx['cellinRow'] = 0; } return mkComponent(({ children, key }) => React.createElement("tr", { key: key }, children)); }), table_cell: setFn((node, ctx) => { const align = (alignMap => { if (!alignMap) return null; const num = ctx['cellinRow']++; return alignMap[num % alignMap.length]; })(ctx['table.align']); return mkComponent(({ children, key }) => (React.createElement("td", { align: align, key: key }, children))); }), table_head: subUse({ table_cell: setFn((node, ctx) => { const align = (alignMap => { if (!alignMap) return null; const num = ctx['cellinRow']++; return alignMap[num % alignMap.length]; })(ctx['table.align']); return mkComponent(({ children, key }) => React.createElement("th", { key: key }, children)); }), }, setFn((node, ctx) => { if (ctx['table.align']) { ctx['cellinRow'] = 0; } return mkComponent(({ children, key }) => React.createElement("tr", { key: key }, children)); })), ':list': setFn((node, ctx) => node.list === 'ordered' ? mkComponent('ol') : node.list === 'variable' ? mkComponent('dl') : mkComponent('ul')), 'item:block': (writer, processor) => (node, ctx, interator) => { if (typeof node === 'string') { return node; } if (!('content' in node)) { console.warn('[jsx] no content in node'); return ''; } // make text from first para if (!(node.content instanceof Array)) { console.error(node); } const id = getSafeNodeId(node, ctx); return makeComponent('li', node, interator(node.content, { ...ctx }), { id }); }, // table of content ':toc': setFn((node, ctx) => { const tocTitle = node.title; return mkComponent(({ children, key }) => (React.createElement("div", { className: "toc", key: key }, tocTitle ? React.createElement("div", { className: "toctitle" }, tocTitle) : '', children))); }), ':toc-list': setFn((node, ctx) => { const { level } = node; return mkComponent(({ children, key }) => (React.createElement("ul", { className: `toc-list listlevel${level}`, key: key }, children))); }), ':toc-item': subUse({ // inside head don't wrap into <p> ':para': nodeContent, }, setFn((node, ctx) => { return mkComponent(({ children, key }) => (React.createElement("li", { className: "toc-item", key: key }, children))); })), }; }; function podlite(children, { file, plugins = () => { }, wrapElement, tree, }, ...args) { const ast = (tree => { if (tree) return tree.interator; let podlite = podlite_core({ importPlugins: true }); let treeAfterParsed = podlite.parse(children || file); return podlite.toAst(treeAfterParsed); })(tree); // const ast = parse( children || content ) let i_key_i = 10000; const makeComponent = helperMakeReact({ wrapElement }); const jsxPlugins = toAnyRules('toJSX', podlite_core({ importPlugins: true }).getPlugins()); // initialize each plugin const jsxPluginInited = Object.fromEntries(Object.entries(jsxPlugins).map(([key, value]) => [key, value(makeComponent)])); const rules = { ...mapToReact(makeComponent), ...plugins(makeComponent), ...jsxPluginInited, }; const writer = new Writer(s => { }); const res = toAny({ processor: parse }) .use({ '*:*': () => (node, ctx, interator) => { // skip named blocks if (isNamedBlock(node.name)) { return null; } if (isSemanticBlock(node)) { return makeComponent(({ key, children }) => { return (React.createElement("div", { key: key }, React.createElement("h1", { className: node.name, key: key }, node.name), interator(node.content, { ...ctx }))); }, node, interator(node.content, { ...ctx })); } console.warn('[to-jsx] Not supported: ' + JSON.stringify(node, null, 2)); return createElement('code', { key: ++i_key_i }, `not supported node:${JSON.stringify(node, null, 2)}`); }, }) .use(rules) .run(ast, writer); // union main react elements and post processed via onEnd event return new Array().concat(res.interator, writer.postInterator); } // this is a helper function for using in unit test export const TestPodlite = ({ children, ...options }) => { let podlite = podlite_core({ importPlugins: true }); // its replace all ids with "id" const tree = frozenIds()(podlite.toAst(podlite.parse(children))); return React.createElement(Podlite, { ...{ children, ...options, tree: { interator: tree } } }); }; export const makeTestPodlite = (podlite = podlite_core({ importPlugins: true })) => ({ children, ...options }) => { let treeAfterParsed = podlite.parse(children); // its replace all ids with "id" const tree = frozenIds()(podlite.toAst(treeAfterParsed)); return React.createElement(Podlite, { ...{ children, ...options, tree: { interator: tree } } }); }; export default Podlite; //# sourceMappingURL=index.js.map