UNPKG

@nuxtjs/mdc

Version:
254 lines (253 loc) 7.67 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 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, ...options, handlers: { ...mdcRemarkHandlers, ...options?.handlers }, nodeHandlers: { ...mdcRemarkNodeHandlers, ...options?.nodeHandlers } }); 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 (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"].includes(parent?.tagName); if (isInlineElement) { return { type: "textComponent", 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 }; }, binding: (state, node) => { return { type: "textComponent", name: "binding", attributes: node.properties, children: state.toFlow(state.all(node)) }; }, span: (state, node) => { const result = { type: "textComponent", name: "span", attributes: node.properties, children: state.all(node) }; state.patch(node, result); return result; }, video: (state, node) => { return { type: "textComponent", name: "video", attributes: node.properties, children: state.toFlow(state.all(node)) }; }, "nuxt-img": (state, node) => { return { type: "textComponent", name: "nuxt-img", attributes: node.properties, children: state.toFlow(state.all(node)) }; }, "nuxt-picture": (state, node) => { return { type: "textComponent", name: "nuxt-picture", attributes: node.properties, children: state.toFlow(state.all(node)) }; }, 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; }, br(state, node) { return { type: "textComponent", name: "br", attributes: node.properties }; } };