solid-markdown
Version:
Markdown renderer for solid-js
329 lines (323 loc) • 10.8 kB
JSX
// 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
};