@podlite/to-jsx
Version:
JSX converter for Podlite markup language
476 lines (475 loc) • 21.2 kB
JavaScript
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