UNPKG

solid-markdown

Version:
329 lines (323 loc) 10.8 kB
// src/index.tsx import remarkParse from "remark-parse"; import remarkRehype from "remark-rehype"; import { createMemo as createMemo2, createRenderEffect, mergeProps } from "solid-js"; import { createStore, reconcile } from "solid-js/store"; import { html } from "property-information"; import { unified } from "unified"; import { VFile } from "vfile"; // src/rehype-filter.ts import { visit } from "unist-util-visit"; var rehypeFilter = (options) => { if (options.allowedElements && options.disallowedElements) { throw new TypeError( "Only one of `allowedElements` and `disallowedElements` should be defined" ); } if (options.allowedElements || options.disallowedElements || options.allowElement) { return (tree) => { visit(tree, "element", (node, index, parent_) => { const parent = parent_; if (parent === null) return; let remove; if (options.allowedElements) { remove = !options.allowedElements.includes(node.tagName); } else if (options.disallowedElements) { remove = options.disallowedElements.includes(node.tagName); } if (!remove && options.allowElement && typeof index === "number" && parent) { remove = !options.allowElement(node, index, parent); } if (remove && typeof index === "number" && parent) { if (options.unwrapDisallowed && node.children) { parent.children.splice(index, 1, ...node.children); } else { parent.children.splice(index, 1); } return index; } return void 0; }); }; } }; var rehype_filter_default = rehypeFilter; // src/renderer.tsx import { For, Match, Switch, createMemo, Show } from "solid-js"; import { svg } from "property-information"; import { Dynamic } from "solid-js/web"; // src/utils.ts import { stringify as commas } from "comma-separated-tokens"; import { find } from "property-information"; import { stringify as spaces } from "space-separated-tokens"; function getInputElement(node) { let index = -1; while (++index < node.children.length) { const child = node.children[index]; if (child?.type === "element" && child?.tagName === "input") { return child; } } return null; } function getElementsBeforeCount(parent, node) { let index = -1; let count = 0; while (++index < parent.children.length) { if (parent.children[index] === node) break; if (parent.children[index]?.type === "element") count++; } return count; } function addProperty(props, prop, value, ctx) { const info = find(ctx.schema, prop); let result = value; if (info.property === "className") { info.property = "class"; } if (result === null || result === void 0 || result !== result) { return; } if (Array.isArray(result)) { result = info.commaSeparated ? commas(result) : spaces(result); } if (info.space && info.property) { props[info.property] = result; } else if (info.attribute) { props[info.attribute] = result; } } function flattenPosition(pos) { return [ pos.start.line, ":", pos.start.column, "-", pos.end.line, ":", pos.end.column ].map((d) => String(d)).join(""); } // src/renderer.tsx var own = {}.hasOwnProperty; var MarkdownRoot = (props) => <MarkdownChildren node={props.node} context={props.context} />; var MarkdownChildren = (props) => <For each={props.node.children}>{(child, index) => <Switch> <Match when={child.type === "element"}><MarkdownNode context={props.context} index={index()} node={child} parent={props.node} /></Match> <Match when={child.type === "text" && child.value !== "\n"}><MarkdownText context={props.context} index={index()} node={child} parent={props.node} /></Match> </Switch>}</For>; var MarkdownText = (props) => { const childProps = createMemo(() => { const context = { ...props.context }; const options = context.options; const node = props.node; const parent = props.parent; const properties = { parent }; const position = node.position || { start: { line: null, column: null, offset: null }, end: { line: null, column: null, offset: null } }; const component = options.components && own.call(options.components, "text") ? options.components.text : null; const basic = typeof component === "string"; properties.key = [ "text", position.start.line, position.start.column, props.index ].join("-"); if (options.sourcePos) { properties["data-sourcepos"] = flattenPosition(position); } if (!basic && options.rawSourcePos) { properties.sourcePosition = node.position; } if (!basic) { properties.node = node; } return { properties, context, component }; }); return <Show when={childProps().component} fallback={props.node.value}><Dynamic component={childProps().component || "span"} {...childProps().properties} /></Show>; }; var MarkdownNode = (props) => { const childProps = createMemo(() => { const context = { ...props.context }; const options = context.options; const parentSchema = context.schema; const node = props.node; const name = node.tagName; const parent = props.parent; const properties = {}; let schema = parentSchema; let property; if (parentSchema.space === "html" && name === "svg") { schema = svg; context.schema = schema; } if (node.properties) { for (property in node.properties) { if (own.call(node.properties, property)) { addProperty(properties, property, node.properties[property], context); } } } if (name === "ol" || name === "ul") { context.listDepth++; } if (name === "ol" || name === "ul") { context.listDepth--; } context.schema = parentSchema; const position = node.position || { start: { line: null, column: null, offset: null }, end: { line: null, column: null, offset: null } }; const component = options.components && own.call(options.components, name) ? options.components[name] : name; const basic = typeof component === "string"; properties.key = [ name, position.start.line, position.start.column, props.index ].join("-"); if (name === "a" && options.linkTarget) { properties.target = typeof options.linkTarget === "function" ? options.linkTarget( String(properties.href || ""), node.children, typeof properties.title === "string" ? properties.title : void 0 ) : options.linkTarget; } if (name === "a" && options.transformLinkUri) { properties.href = options.transformLinkUri( String(properties.href || ""), node.children, typeof properties.title === "string" ? properties.title : void 0 ); } if (!basic && name === "code" && parent.type === "element" && parent.tagName !== "pre") { properties.inline = true; } if (!basic && (name === "h1" || name === "h2" || name === "h3" || name === "h4" || name === "h5" || name === "h6")) { properties.level = Number.parseInt(name.charAt(1), 10); } if (name === "img" && options.transformImageUri) { properties.src = options.transformImageUri( String(properties.src || ""), String(properties.alt || ""), typeof properties.title === "string" ? properties.title : void 0 ); } if (!basic && name === "li" && parent.type === "element") { const input = getInputElement(node); properties.checked = input?.properties ? Boolean(input.properties.checked) : null; properties.index = getElementsBeforeCount(parent, node); properties.ordered = parent.tagName === "ol"; } if (!basic && (name === "ol" || name === "ul")) { properties.ordered = name === "ol"; properties.depth = context.listDepth; } if (name === "td" || name === "th") { if (properties.align) { if (!properties.style) properties.style = {}; properties.style.textAlign = properties.align; delete properties.align; } if (!basic) { properties.isHeader = name === "th"; } } if (!basic && name === "tr" && parent.type === "element") { properties.isHeader = Boolean(parent.tagName === "thead"); } if (options.sourcePos) { properties["data-sourcepos"] = flattenPosition(position); } if (!basic && options.rawSourcePos) { properties.sourcePosition = node.position; } if (!basic && options.includeElementIndex) { properties.index = getElementsBeforeCount(parent, node); properties.siblingCount = getElementsBeforeCount(parent); } if (!basic) { properties.node = node; } return { properties, context, component }; }); return <Dynamic component={childProps().component} {...childProps().properties}><MarkdownChildren node={props.node} context={childProps().context} /></Dynamic>; }; // src/index.tsx var defaults = { renderingStrategy: "memo", remarkPlugins: [], rehypePlugins: [], class: "", unwrapDisallowed: false, disallowedElements: void 0, allowedElements: void 0, allowElement: void 0, children: "", sourcePos: false, rawSourcePos: false, skipHtml: false, includeElementIndex: false, transformLinkUri: null, transformImageUri: void 0, linkTarget: "_self", components: {} }; var SolidMarkdown = (opts) => { const options = mergeProps(defaults, opts); const [node, setNode] = createStore({ type: "root", children: [] }); const generateNode = createMemo2(() => { const children = options.children; const processor = unified().use(remarkParse).use(options.remarkPlugins || []).use(remarkRehype, { allowDangerousHtml: true }).use(options.rehypePlugins || []).use(rehype_filter_default, options); const file = new VFile(); if (typeof children === "string") { file.value = children; } else if (children !== void 0 && options.children !== null) { console.warn( `[solid-markdown] Warning: please pass a string as \`children\` (not: \`${typeof children}\`)` ); } const hastNode = processor.runSync(processor.parse(file), file); if (hastNode.type !== "root") { throw new TypeError("Expected a `root` node"); } return hastNode; }); if (options.renderingStrategy === "reconcile") { createRenderEffect(() => { setNode(reconcile(generateNode())); }); } return <><div class={options.class}><MarkdownRoot context={{ options, schema: html, listDepth: 0 }} node={options.renderingStrategy === "memo" ? generateNode() : node} /></div></>; }; export { SolidMarkdown };