solid-markdown
Version:
Markdown renderer for solid-js
402 lines (397 loc) • 12.1 kB
JavaScript
import { insert, createComponent, memo, effect, className, Dynamic, mergeProps as mergeProps$1, template } from 'solid-js/web';
import remarkParse from 'remark-parse';
import remarkRehype from 'remark-rehype';
import { mergeProps, createMemo, createRenderEffect, For, Switch, Match, Show } from 'solid-js';
import { createStore, reconcile } from 'solid-js/store';
import { html, find, svg } from 'property-information';
import { unified } from 'unified';
import { VFile } from 'vfile';
import { visit } from 'unist-util-visit';
import { stringify } from 'comma-separated-tokens';
import { stringify as stringify$1 } from 'space-separated-tokens';
// src/index.tsx
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;
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 ? stringify(result) : stringify$1(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) => createComponent(MarkdownChildren, {
get node() {
return props.node;
},
get context() {
return props.context;
}
});
var MarkdownChildren = (props) => createComponent(For, {
get each() {
return props.node.children;
},
children: (child, index) => createComponent(Switch, {
get children() {
return [createComponent(Match, {
get when() {
return child.type === "element";
},
get children() {
return createComponent(MarkdownNode, {
get context() {
return props.context;
},
get index() {
return index();
},
node: child,
get parent() {
return props.node;
}
});
}
}), createComponent(Match, {
get when() {
return child.type === "text" && child.value !== "\n";
},
get children() {
return createComponent(MarkdownText, {
get context() {
return props.context;
},
get index() {
return index();
},
node: child,
get parent() {
return props.node;
}
});
}
})];
}
})
});
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 createComponent(Show, {
get when() {
return childProps().component;
},
get fallback() {
return props.node.value;
},
get children() {
return createComponent(Dynamic, mergeProps$1({
get component() {
return childProps().component || "span";
}
}, () => childProps().properties));
}
});
};
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 createComponent(Dynamic, mergeProps$1({
get component() {
return childProps().component;
}
}, () => childProps().properties, {
get children() {
return createComponent(MarkdownChildren, {
get node() {
return props.node;
},
get context() {
return childProps().context;
}
});
}
}));
};
// src/index.tsx
var _tmpl$ = /* @__PURE__ */ template(`<div>`);
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 = createMemo(() => {
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 (() => {
var _el$ = _tmpl$();
insert(_el$, createComponent(MarkdownRoot, {
context: {
options,
schema: html,
listDepth: 0
},
get node() {
return memo(() => options.renderingStrategy === "memo")() ? generateNode() : node;
}
}));
effect(() => className(_el$, options.class));
return _el$;
})();
};
export { SolidMarkdown };