UNPKG

@vivliostyle/vfm

Version:

Custom Markdown syntax specialized in book authoring.

212 lines (211 loc) 6.61 kB
import { JSON_SCHEMA, load as yaml } from 'js-yaml'; import { toString } from 'mdast-util-to-string'; import stringify from 'rehype-stringify'; import frontmatter from 'remark-frontmatter'; import markdown from 'remark-parse'; import remark2rehype from 'remark-rehype'; import unified from 'unified'; import { select } from 'unist-util-select'; import { visit } from 'unist-util-visit'; import { mdast as attr } from './attr.js'; import { mdast as footnotes } from './footnotes.js'; /** * Read the title from heading without footnotes. * @param tree Tree of Markdown AST. * @returns Title text or `undefined`. */ const readTitleFromHeading = (tree) => { const heading = select('heading', tree); if (!heading) { return; } // Create title string with footnotes removed const children = [...heading.children]; heading.children = heading.children.filter((child) => child.type !== 'footnote'); // Remove ruby text and HTML tags const text = toString(heading) .replace(/{(.+?)(?<=[^\\|])\|(.+?)}/g, '$1') .replace(/<[^<>]*>/g, ''); heading.children = children; return text; }; /** * Parse Markdown's Frontmatter to metadate (`VFile.data`). * @returns Handler. * @see https://github.com/Symbitic/remark-plugins/blob/master/packages/remark-meta/src/index.js */ const mdast = () => (tree, file) => { visit(tree, 'yaml', (node) => { const value = yaml(node.value, { schema: JSON_SCHEMA }); if (typeof value === 'object') { file.data = { ...file.data, ...value, }; } }); // If title is undefined in frontmatter, read from heading if (!file.data.title) { const title = readTitleFromHeading(tree); if (title) { file.data.title = title; } } visit(tree, 'shortcode', (node) => { if (node.identifier !== 'toc') { return; } if (file.data.vfm) { file.data.vfm.toc = true; } else { file.data.vfm = { math: true, toc: true }; } }); }; /** * Parse Markdown frontmatter. * @param md Markdown. * @returns Key/Value pair. */ const parseMarkdown = (md) => { const processor = unified() .use([ [markdown, { gfm: true, commonmark: true }], // Remove footnotes when reading title from heading footnotes, attr, frontmatter, mdast, ]) .data('settings', { position: false }) .use(remark2rehype) .use(stringify); return processor.processSync(md).data; }; /** * Read the string or null value in the YAML parse result. * If the value is null, it will be an empty string * @param value Value of YAML parse result. * @returns Text. */ const readStringOrNullValue = (value) => { return value === null ? '' : `${value}`; }; /** * Read an attributes from data object. * @param data Data object. * @returns Attributes of HTML tag. */ const readAttributes = (data) => { if (data === null || typeof data !== 'object') { return; } const result = []; for (const key of Object.keys(data)) { result.push({ name: key, value: readStringOrNullValue(data[key]) }); } return result; }; /** * Read an attributes collection from data object. * @param data Data object. * @returns Attributes collection of HTML tag. */ const readAttributesCollection = (data) => { if (!Array.isArray(data)) { return; } const result = []; data.forEach((value) => { const attributes = readAttributes(value); if (attributes) { result.push(attributes); } }); return result; }; /** * Read VFM settings from data object. * @param data Data object. * @returns Settings. */ const readSettings = (data) => { if (data === null || typeof data !== 'object') { return { toc: false }; } return { math: typeof data.math === 'boolean' ? data.math : undefined, partial: typeof data.partial === 'boolean' ? data.partial : undefined, hardLineBreaks: typeof data.hardLineBreaks === 'boolean' ? data.hardLineBreaks : undefined, disableFormatHtml: typeof data.disableFormatHtml === 'boolean' ? data.disableFormatHtml : undefined, theme: typeof data.theme === 'string' ? data.theme : undefined, toc: typeof data.toc === 'boolean' ? data.toc : false, assignIdToFigcaption: typeof data.assignIdToFigcaption === 'boolean' ? data.assignIdToFigcaption : false, }; }; /** * Read metadata from Markdown frontmatter. * * Keys that are not defined as VFM are treated as `meta`. If you specify a key name in `customKeys`, the key and its data type will be preserved and stored in `custom` instead of `meta`. * @param md Markdown. * @param customKeys A collection of key names to be ignored by meta processing. * @returns Metadata. */ export const readMetadata = (md, customKeys = []) => { const metadata = {}; const data = parseMarkdown(md); const others = []; for (const key of Object.keys(data)) { if (customKeys.includes(key)) { if (!metadata.custom) { metadata.custom = {}; } metadata.custom[key] = data[key]; continue; } switch (key) { case 'id': case 'lang': case 'dir': case 'class': case 'title': metadata[key] = readStringOrNullValue(data[key]); break; case 'html': case 'body': case 'base': metadata[key] = readAttributes(data[key]); break; case 'meta': case 'link': case 'script': metadata[key] = readAttributesCollection(data[key]); break; case 'vfm': metadata[key] = readSettings(data[key]); break; case 'style': case 'head': // Reserved for future use. break; default: others.push([ { name: 'name', value: key }, { name: 'content', value: readStringOrNullValue(data[key]) }, ]); break; } } // Other properties should be `<meta>` if (0 < others.length) { metadata.meta = metadata.meta ? metadata.meta.concat(others) : others; } return metadata; };