@nuxtjs/mdc
Version:
Nuxt MDC module
268 lines (267 loc) • 9.16 kB
JavaScript
import { defaultHandlers, toMdast } from "hast-util-to-mdast";
import { nodeTextContent } from "@nuxtjs/mdc/runtime/utils/node";
import { hasProtocol } from "ufo";
import { toHtml } from "hast-util-to-html";
import { visit } from "unist-util-visit";
import { format } from "hast-util-format";
import { computeHighlightRanges, refineCodeLanguage } from "./utils.js";
const mdcRemarkElementType = "mdc-element";
const mdastTextComponentType = "textDirective";
const mdcTextComponentType = "textComponent";
const own = {}.hasOwnProperty;
export function mdcRemark(options) {
return function(node, _file) {
const tree = preProcessElementNodes(node);
const mdast = toMdast(tree, {
/**
* Default to true in rehype-remark
* @see https://github.com/rehypejs/rehype-remark/blob/main/lib/index.js#L37ckages/remark/lib/index.js#L100
*/
document: true,
newlines: true,
...options,
handlers: {
...mdcRemarkHandlers,
...options?.handlers
},
nodeHandlers: {
...mdcRemarkNodeHandlers,
...options?.nodeHandlers
}
});
visit(mdast, (node2) => node2.type === mdastTextComponentType, (node2, index, parent) => {
node2.type = mdcTextComponentType;
if (node2.name === "binding") {
return;
}
if (index && parent && parent.children) {
if (index > 0 && parent.children[index - 1].type === "text") {
const text = parent.children[index - 1];
if (!["\n", " ", " "].includes(text.value.slice(-1))) {
text.value += " ";
}
}
if (index && index < parent.children.length - 1 && parent.children[index + 1].type === "text") {
const text = parent.children[index + 1];
if (!["\n", " ", " ", ",", "."].includes(text.value.slice(0, 1))) {
text.value = " " + text.value;
}
}
}
});
return mdast;
};
}
function preProcessElementNodes(node) {
if (node.type === "element") {
if (node.children?.length && (node.children || []).every((child) => child.tag === "template")) {
node.children = node.children.flatMap((child) => {
if (typeof child.props?.["v-slot:default"] !== "undefined" && Object.keys(child.props).length === 1) {
return child.children || [];
}
return child;
});
}
const result = {
type: mdcRemarkElementType,
tagName: node.tag,
properties: node.props,
children: (node.children || []).map(preProcessElementNodes)
};
if (!node.children?.length) {
delete result.children;
}
return result;
}
if (node?.children) {
return {
...node,
children: (node.children || []).map(preProcessElementNodes)
};
}
return node;
}
const mdcRemarkNodeHandlers = {
[mdcRemarkElementType]: (state, node, parent) => {
if (node.properties && node.properties.dataMdast === "ignore") {
return;
}
if (node.properties && (node.properties.className || node.properties["class-name"])) {
const pascal = Array.isArray(node.properties.className || "") ? node.properties.className : String(node.properties.className || "").split(" ");
const kebab = Array.isArray(node.properties["class-name"] || "") ? node.properties["class-name"] : String(node.properties["class-name"] || "").split(" ");
node.properties.class = [node.properties.class || "", ...pascal, ...kebab].filter(Boolean).join(" ");
Reflect.deleteProperty(node.properties, "className");
Reflect.deleteProperty(node.properties, "class-name");
}
if (own.call(state.handlers, node.tagName)) {
return state.handlers[node.tagName](state, node, parent) || void 0;
}
if ("value" in node && typeof node.value === "string") {
const result = { type: "text", value: node.value };
state.patch(node, result);
return result;
}
const isInlineElement = (parent?.children || []).some((child) => child.type === "text") || ["p", "li", "strong", "em", "span"].includes(parent?.tagName);
if (isInlineElement) {
return {
type: mdastTextComponentType,
name: node.tagName,
attributes: node.properties,
children: state.all(node)
};
}
return {
type: "containerComponent",
name: node.tagName,
attributes: node.properties,
children: state.all(node)
};
}
};
const mdcRemarkHandlers = {
template: (state, node) => {
const vSlot = Object.keys(node.properties || {}).find((prop) => prop?.startsWith("v-slot:"))?.replace("v-slot:", "") || "default";
const attributes = Object.fromEntries(Object.entries(node.properties || {}).filter(([key]) => !key.startsWith("v-slot:")));
return {
type: "componentContainerSection",
name: vSlot,
attributes,
children: state.toFlow(state.all(node))
};
},
div: (state, node) => {
return {
type: "containerComponent",
name: "div",
attributes: node.properties,
children: state.toFlow(state.all(node))
};
},
code: (state, node) => {
const attributes = { ...node.properties };
if ("style" in attributes && !attributes.style) {
delete attributes.style;
}
if ("class" in attributes) {
attributes.className = String(attributes.class).split(" ").filter(Boolean);
delete attributes.class;
}
if (Array.isArray(attributes.className)) {
attributes.className = attributes.className.filter((name) => !name.startsWith("language-"));
if (Array.isArray(attributes.className) && !attributes.className.length) {
delete attributes.className;
}
}
if (attributes.language) {
attributes.lang = refineCodeLanguage(attributes.language);
delete attributes.language;
}
const result = { type: "inlineCode", value: nodeTextContent(node), attributes };
state.patch(node, result);
return result;
},
pre: (_state, node) => {
const meta = [
node.properties.filename ? `[${String(node.properties.filename).replace(/\]/g, "\\]")}]` : "",
node.properties.highlights?.length ? `{${computeHighlightRanges(node.properties.highlights)}}` : "",
node.properties.meta
].filter(Boolean).join(" ");
const value = String(node.properties.code || "").replace(/\n$/, "");
return {
type: "code",
value,
lang: refineCodeLanguage(node.properties.language),
meta
};
},
button: (state, node) => {
if (
// @ts-expect-error: custom type
node.children?.find((child) => child.type === mdcRemarkElementType) || node.children?.find((child) => child.type === "text" && child.value.includes("\n"))
) {
return {
type: "containerComponent",
name: "button",
children: state.all(node),
attributes: node.properties
};
}
return createTextComponent("button")(state, node);
},
span: createTextComponent("span"),
binding: createTextComponent("binding"),
iframe: createTextComponent("iframe"),
video: createTextComponent("video"),
"nuxt-img": createTextComponent("nuxt-img"),
"nuxt-picture": createTextComponent("nuxt-picture"),
br: createTextComponent("br"),
table: (state, node) => {
visit(node, (node2) => {
if (node2.type === mdcRemarkElementType) {
node2.type = "element";
}
});
if (Object.keys(node.properties).length) {
format({ type: "root", children: [node] });
return {
type: "html",
value: toHtml(node)
};
}
return defaultHandlers.table(state, node);
},
img: (state, node) => {
const { src, title, alt, ...attributes } = node.properties || {};
const result = {
type: "image",
url: state.resolve(String(src || "") || null),
title: title ? String(title) : null,
alt: alt ? String(alt) : "",
attributes
};
state.patch(node, result);
return result;
},
em: (state, node) => {
const result = { type: "emphasis", children: state.all(node), attributes: node.properties };
state.patch(node, result);
return result;
},
strong: (state, node) => {
const result = { type: "strong", children: state.all(node), attributes: node.properties };
state.patch(node, result);
return result;
},
a(state, node) {
const { href, title, ...attributes } = node.properties || {};
if (hasProtocol(String(href || ""))) {
if (attributes.target === "_blank") {
delete attributes.target;
}
if (["nofollow,noopener,noreferrer"].includes(String(attributes.rel))) {
delete attributes.rel;
}
}
const result = {
type: "link",
url: state.resolve(String(href || "") || null),
title: title ? String(title) : null,
children: state.all(node),
attributes
};
state.patch(node, result);
return result;
}
};
function createTextComponent(name) {
return (state, node) => {
const result = {
type: mdastTextComponentType,
name,
attributes: node.properties,
children: state.all(node)
};
state.patch(node, result);
return result;
};
}