UNPKG

@nuxtjs/mdc

Version:
268 lines (267 loc) 9.16 kB
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; }; }