@vivliostyle/vfm
Version:
Custom Markdown syntax specialized in book authoring.
212 lines (211 loc) • 6.61 kB
JavaScript
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;
};