UNPKG

svelte-markdoc-preprocess

Version:

A Svelte preprocessor that allows you to use Markdoc.

144 lines (143 loc) 4.93 kB
import { sanitize_for_svelte } from './transformer.js'; import { escape } from 'html-escaper'; import { IMAGE_PREFIX, IMPORT_PREFIX, NODES_IMPORT } from './constants.js'; import { is_relative_path } from './utils.js'; import md from '@markdoc/markdoc'; const { Tag } = md; export async function render_html(node, dependencies, highlighter, escape_html = true) { /** * if the node is a string or number, it's a text node. */ if (typeof node === 'string' || typeof node === 'number') { if (escape_html) { return sanitize_for_svelte(escape(String(node))); } else { return sanitize_for_svelte(String(node)); } } /** * if the node is an array, render its items. */ if (Array.isArray(node)) { return Promise.all(node.map(async (item) => await render_html(item, dependencies, highlighter, escape_html))).then((items) => items.join('')); } /** * if the node is not a Tag, it's invalid. */ if (node === null || typeof node !== 'object' || !Tag.isTag(node)) { return ''; } let { name, attributes, children = [] } = node; if (!name) { return await render_html(children, dependencies, highlighter); } const is_svelte = is_svelte_component(node); /** * add attributes to the tag. */ let output = `<${name}`; for (let [key, value] of Object.entries(attributes ?? {})) { const is_src_key = key === 'src'; const is_imported_image = is_src_key && is_relative_path(value); if (is_svelte) { switch (name.toLowerCase()) { case `${NODES_IMPORT}.image`.toLowerCase(): if (is_src_key) { if (is_imported_image) { const unique_name = `${IMAGE_PREFIX}${dependencies.size}`; dependencies.set(unique_name, String(value)); output += ` ${key.toLowerCase()}=${generate_svelte_attribute_value(unique_name, 'import')}`; break; } } default: output += ` ${key.toLowerCase()}=${generate_svelte_attribute_value(value)}`; break; } } else { switch (name.toLowerCase()) { case 'img': if (is_imported_image) { /** * Allow importing relative images and import them via vite. */ const unique_name = `${IMAGE_PREFIX}${dependencies.size}`; dependencies.set(unique_name, String(value)); output += ` ${key.toLowerCase()}=${generate_svelte_attribute_value(unique_name, 'import')}`; break; } default: output += ` ${key.toLowerCase()}="${sanitize_for_svelte(escape(String(value)))}"`; break; } } } output += '>'; /** * if the tag is a void element, it doesn't need a closing tag. */ if (is_void_element(name)) { return output; } let escape_next = true; if (highlighter) { const run_highlighter = name.toLowerCase() === `${NODES_IMPORT}.fence`.toLowerCase() || name.toLowerCase() === 'pre'.toLowerCase(); if (run_highlighter) { escape_next = false; children = await Promise.all(children.map(async (child) => typeof child === 'string' ? await highlighter(child, (is_svelte ? attributes?.language : attributes['data-language']) ?? '') : child)); } } /** * render the children if present. */ if (children.length) { output += await render_html(children, dependencies, highlighter, escape_next); } /** * close the tag. */ output += `</${name}>`; return output; } function is_void_element(name) { return [ 'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 'track', 'wbr', ].includes(name); } function is_svelte_component(node) { return Tag.isTag(node) && node.name.startsWith(IMPORT_PREFIX); } function generate_svelte_attribute_value(value, type) { switch (type ?? typeof value) { case 'string': return `"${sanitize_for_svelte(escape(String(value)))}"`; case 'import': case 'number': case 'boolean': return `{${String(value)}}`; case 'object': return `{${JSON.stringify(value)}}`; default: throw new Error(`Invalid attribute value: ${value}`); } }