@nuxt/markdown
Version:
Nuxt-flavoured fork of @dimerapp/markdown
869 lines (759 loc) • 17.1 kB
JavaScript
'use strict';
function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; }
const unified = _interopDefault(require('unified'));
const remarkParse = _interopDefault(require('remark-parse'));
const remarkSlug = _interopDefault(require('remark-slug'));
const squeezeParagraphs = _interopDefault(require('remark-squeeze-paragraphs'));
const remarkRehype = _interopDefault(require('remark-rehype'));
const rehypeRaw = _interopDefault(require('rehype-raw'));
const rehypePrism = _interopDefault(require('@mapbox/rehype-prism'));
const rehypeSanitize = _interopDefault(require('rehype-sanitize'));
const rehypeStringify = _interopDefault(require('rehype-stringify'));
const remarkMacro = _interopDefault(require('remark-macro'));
const visit = _interopDefault(require('unist-util-visit'));
const wrap$1 = _interopDefault(require('mdast-util-to-hast/lib/wrap'));
const all = _interopDefault(require('mdast-util-to-hast/lib/all'));
const u = _interopDefault(require('unist-builder'));
const detab = _interopDefault(require('detab'));
const normalize = _interopDefault(require('mdurl/encode'));
const revert = _interopDefault(require('mdast-util-to-hast/lib/revert'));
const collapse = _interopDefault(require('collapse-white-space'));
const position = _interopDefault(require('unist-util-position'));
const trimLines = _interopDefault(require('trim-lines'));
// Code taken from https://github.com/rigor789/remark-autolink-headings
const icon = 'icon';
const link = 'link';
const wrap = 'wrap';
const methodMap = {
prepend: 'unshift',
append: 'push'
};
function base (node, callback) {
const { data } = node;
if (!data || !data.hProperties || !data.hProperties.id) {
return
}
// eslint-disable-next-line standard/no-callback-literal
return callback(`#${data.hProperties.id}`)
}
const contentDefaults = {
type: 'element',
tagName: 'span',
properties: { className: [icon, `${icon}-${link}`] }
};
function attacher (opts = {}) {
// eslint-disable-next-line prefer-const
let { linkProperties, behaviour, content } = {
behaviour: 'prepend',
content: contentDefaults,
...opts
};
if (behaviour !== wrap && !linkProperties) {
linkProperties = { 'aria-hidden': 'true' };
}
function injectNode (node) {
return base(node, (url) => {
if (!Array.isArray(content)) {
content = [content];
}
node.children[methodMap[behaviour]]({
type: link,
url,
title: null,
data: {
hProperties: linkProperties,
hChildren: content
}
});
})
}
function wrapNode (node) {
return base(node, (url) => {
const { children } = node;
node.children = [{
type: link,
url,
title: null,
children,
data: {
hProperties: linkProperties
}
}];
})
}
return ast => visit(ast, 'heading', behaviour === wrap ? wrapNode : injectNode)
}
function blockquote (h, node) {
return h(node, 'blockquote', wrap$1(all(h, node), true))
}
function hardBreak (h, node) {
return [h(node, 'br'), u('text', '\n')]
}
function code (h, node) {
const value = node.value ? detab(node.value + '\n') : '';
const lang = node.lang && node.lang.match(/^[^ \t]+(?=[ \t]|$)/);
const props = {};
if (lang) {
props.className = ['language-' + lang];
}
return h(node.position, 'pre', [h(node, 'code', props, [u('text', value)])])
}
function strikethrough (h, node) {
return h(node, 'del', all(h, node))
}
function emphasis (h, node) {
return h(node, 'em', all(h, node))
}
function footnoteReference (h, node) {
const footnoteOrder = h.footnoteOrder;
const identifier = node.identifier;
if (!footnoteOrder.includes(identifier)) {
footnoteOrder.push(identifier);
}
return h(node.position, 'sup', { id: 'fnref-' + identifier }, [
h(node, 'a', { href: '#fn-' + identifier, className: ['footnote-ref'] }, [
u('text', node.label || identifier)
])
])
}
function footnote (h, node) {
const footnoteById = h.footnoteById;
const footnoteOrder = h.footnoteOrder;
let identifier = 1;
while (identifier in footnoteById) {
identifier++;
}
identifier = String(identifier);
// No need to check if `identifier` exists in `footnoteOrder`, it’s guaranteed
// to not exist because we just generated it.
footnoteOrder.push(identifier);
footnoteById[identifier] = {
identifier,
type: 'footnoteDefinition',
children: [{ type: 'paragraph', children: node.children }],
position: node.position
};
return footnoteReference(h, {
identifier,
type: 'footnoteReference',
position: node.position
})
}
function extractText (node) {
let text = '';
for (const child of node.children) {
if (child.children && child.children.length) {
text += extractText(child);
continue
}
if (!child.value) {
continue
}
text += child.value;
}
return text
}
function heading (h, node) {
let link;
const _link = node.children.find(c => c.type === 'link');
if (_link && _link.url.startsWith('#')) {
link = _link.url;
}
const text = extractText(node);
if (this.toc && text && link) {
this.toc.push([node.depth, text, link]);
}
return h(node, `h${node.depth}`, all(h, node))
}
// Return either a `raw` node in dangerous mode, otherwise nothing.
function html (h, node) {
return h.dangerous ? h.augment(node, u('raw', node.value)) : null
}
function imageReference (h, node) {
const def = h.definition(node.identifier);
if (!def) {
return revert(h, node)
}
const props = { src: normalize(def.url || ''), alt: node.alt };
if (def.title !== null && def.title !== undefined) {
props.title = def.title;
}
return h(node, 'img', props)
}
function image (h, node) {
const props = {
src: normalize(node.url),
alt: node.alt
};
if (node.title !== null && node.title !== undefined) {
props.title = node.title;
}
return h(node, 'img', props)
}
function inlineCode (h, node) {
return h(node, 'code', [u('text', collapse(node.value))])
}
function linkReference (h, node) {
const def = h.definition(node.identifier);
if (!def) {
return revert(h, node)
}
const props = { href: normalize(def.url || '') };
if (def.title !== null && def.title !== undefined) {
props.title = def.title;
}
return h(node, 'a', props, all(h, node))
}
function link$1 (h, node) {
let tagName;
const url = normalize(node.url);
const props = {};
if (node.title !== null && node.title !== undefined) {
props.title = node.title;
}
if (url.startsWith('#') || url.match(/^https?:\/\//)) {
props.href = url;
tagName = 'a';
} else {
props.to = url;
tagName = 'nuxt-link';
props['data-press-link'] = 'true';
}
return h(node, tagName, props, all(h, node))
}
function listItem (h, node, parent) {
const children = node.children;
const head = children[0];
const raw = all(h, node);
const loose = parent ? listLoose(parent) : listItemLoose(node);
const props = {};
let result;
let container;
let index;
let length;
let child;
// Tight lists should not render `paragraph` nodes as `p` elements.
if (loose) {
result = raw;
} else {
result = [];
length = raw.length;
index = -1;
while (++index < length) {
child = raw[index];
if (child.tagName === 'p') {
result = result.concat(child.children);
} else {
result.push(child);
}
}
}
if (typeof node.checked === 'boolean') {
if (loose && (!head || head.type !== 'paragraph')) {
result.unshift(h(null, 'p', []));
}
container = loose ? result[0].children : result;
if (container.length !== 0) {
container.unshift(u('text', ' '));
}
container.unshift(
h(null, 'input', {
type: 'checkbox',
checked: node.checked,
disabled: true
})
);
// According to github-markdown-css, this class hides bullet.
// See: <https://github.com/sindresorhus/github-markdown-css>.
props.className = ['task-list-item'];
}
if (loose && result.length !== 0) {
result = wrap$1(result, true);
}
return h(node, 'li', props, result)
}
function listLoose (node) {
let loose = node.spread;
const children = node.children;
const length = children.length;
let index = -1;
while (!loose && ++index < length) {
loose = listItemLoose(children[index]);
}
return loose
}
function listItemLoose (node) {
const spread = node.spread;
return spread === undefined || spread === null
? node.children.length > 1
: spread
}
function list (h, node) {
const props = {};
const name = node.ordered ? 'ol' : 'ul';
let index = -1;
if (typeof node.start === 'number' && node.start !== 1) {
props.start = node.start;
}
const items = all(h, node);
const length = items.length;
// Like GitHub, add a class for custom styling.
while (++index < length) {
if (
items[index].properties.className &&
items[index].properties.className.includes('task-list-item')
) {
props.className = ['contains-task-list'];
break
}
}
return h(node, name, props, wrap$1(items, true))
}
function paragraph (h, node) {
return h(node, 'p', all(h, node))
}
function root (h, node) {
return h.augment(node, u('root', wrap$1(all(h, node))))
}
function strong (h, node) {
return h(node, 'strong', all(h, node))
}
function table (h, node) {
const rows = node.children;
let index = rows.length;
const align = node.align;
const alignLength = align.length;
const result = [];
let pos;
let row;
let out;
let name;
let cell;
while (index--) {
row = rows[index].children;
name = index === 0 ? 'th' : 'td';
pos = alignLength;
out = [];
while (pos--) {
cell = row[pos];
out[pos] = h(cell, name, { align: align[pos] }, cell ? all(h, cell) : []);
}
result[index] = h(rows[index], 'tr', wrap$1(out, true));
}
return h(
node,
'table',
wrap$1(
[
h(result[0].position, 'thead', wrap$1([result[0]], true)),
h(
{
start: position.start(result[1]),
end: position.end(result[result.length - 1])
},
'tbody',
wrap$1(result.slice(1), true)
)
],
true
)
)
}
function text (h, node) {
return h.augment(node, u('text', trimLines(node.value)))
}
function thematicBreak (h, node) {
return h(node, 'hr')
}
const builtinHandlers = {
blockquote,
code,
emphasis,
footnoteReference,
footnote,
heading,
html,
imageReference,
image,
inlineCode,
linkReference,
link: link$1,
listItem,
list,
paragraph,
root,
strong,
table,
text,
thematicBreak,
break: hardBreak,
delete: strikethrough,
toml: ignore,
yaml: ignore,
definition: ignore,
footnoteDefinition: ignore
};
// Return nothing for nodes that are ignored.
function ignore () {
return null
}
var strip = [
"script"
];
var clobberPrefix = "";
var clobber = [
];
var ancestors = {
li: [
"ol",
"ul"
],
tbody: [
"table"
],
tfoot: [
"table"
],
thead: [
"table"
],
td: [
"table"
],
th: [
"table"
],
tr: [
"table"
]
};
var protocols = {
href: [
"http",
"https",
"mailto"
],
cite: [
"http",
"https"
],
src: [
"http",
"https"
],
longDesc: [
"http",
"https"
]
};
var tagNames = [
"dimertitle",
"button",
"h1",
"h2",
"h3",
"h4",
"h5",
"h6",
"h7",
"h8",
"br",
"b",
"i",
"strong",
"em",
"a",
"pre",
"code",
"img",
"tt",
"div",
"ins",
"del",
"sup",
"sub",
"p",
"ol",
"ul",
"table",
"thead",
"tbody",
"tfoot",
"blockquote",
"dl",
"dt",
"dd",
"kbd",
"q",
"samp",
"iframe",
"var",
"hr",
"ruby",
"rt",
"rp",
"li",
"tr",
"td",
"input",
"th",
"s",
"span",
"strike",
"summary",
"details"
];
var attributes = {
a: [
"href",
"aria-hidden"
],
img: [
"src",
"dataSrc",
"longDesc"
],
iframe: [
"src",
"height",
"scrolling",
"frameborder",
"allowtransparency",
"allowfullscreen",
"style"
],
div: [
"itemScope",
"itemType"
],
blockquote: [
"cite"
],
del: [
"cite"
],
ins: [
"cite"
],
q: [
"cite"
],
li: [
"dataTitle"
],
pre: [
"dataLine"
],
"*": [
"abbr",
"accept",
"acceptCharset",
"accessKey",
"action",
"align",
"alt",
"axis",
"border",
"cellPadding",
"cellSpacing",
"char",
"charoff",
"charSet",
"checked",
"className",
"clear",
"cols",
"colSpan",
"color",
"compact",
"coords",
"dateTime",
"dir",
"disabled",
"encType",
"htmlFor",
"frame",
"headers",
"height",
"hrefLang",
"hspace",
"isMap",
"id",
"label",
"lang",
"maxLength",
"media",
"method",
"multiple",
"name",
"nohref",
"noshade",
"nowrap",
"open",
"prompt",
"readOnly",
"rel",
"rev",
"rows",
"rowSpan",
"rules",
"scope",
"selected",
"shape",
"size",
"span",
"start",
"summary",
"tabIndex",
"target",
"title",
"type",
"useMap",
"valign",
"value",
"vspace",
"width",
"itemProp"
]
};
const sanitizeOptions = {
strip: strip,
clobberPrefix: clobberPrefix,
clobber: clobber,
ancestors: ancestors,
protocols: protocols,
tagNames: tagNames,
attributes: attributes
};
// Safely escape {{ }} in code blocks using zero-width whitespace
function escapeVueInMarkdown (raw) {
let c;
let i = 0;
let escaped = false;
let r = '';
for (i = 0; i < raw.length; i++) {
c = raw.charAt(i);
if (c === '`' && raw.slice(i, i + 3) === '```' && raw.charCodeAt(i - 1) !== 92) {
escaped = !escaped;
r += raw.slice(i, i + 3);
i += 2;
continue
} else if (c === '`' && raw.charCodeAt(i - 1) !== 92) {
escaped = !escaped;
r += c;
continue
}
if (!escaped) {
r += c;
} else if (c === '{' && raw.charAt(i + 1) === '{' && raw.charCodeAt(i - 1) !== 92) {
i += 1;
r += '{\u200B{'; // zero width white space character
} else {
r += c;
}
}
return r
}
const macroEngine = remarkMacro();
const layers = [
['remark-parse', remarkParse],
['remark-slug', remarkSlug],
['remark-autolink-headings', attacher],
['remark-macro', macroEngine.transformer],
['remark-squeeze-paragraphs', squeezeParagraphs],
['remark-rehype', remarkRehype, { allowDangerousHTML: true }],
['rehype-raw', rehypeRaw],
['rehype-prism', rehypePrism, { ignoreMissing: true }],
['rehype-stringify', rehypeStringify]
];
class NuxtMarkdown {
constructor (config = {}) {
const { toc, sanitize, handlers, extend } = {
toc: false,
sanitize: false,
handlers: {},
extend: () => {},
...config
};
this.layers = [ ...layers ];
const extendLayerProxy = new Proxy(this.layers, {
get: (_, prop) => {
if (['push', 'splice', 'unshift', 'shift', 'pop', 'slice'].includes(prop)) {
return (...args) => this.layers[prop](...args)
}
return this.layers.find(l => l[0] === prop)
},
set: (_, prop, value) => {
if (!Array.isArray(value)) {
value = [prop, value, {}];
} else {
value.unshift(prop);
}
this.layers.splice(this.layers.length - 1, 0, value);
return value
}
});
const registerMacroProxy = new Proxy({}, {
get: (_, name) => {
if (name === 'inline') {
return (callback) => {
macroEngine.addMacro(name, callback, true);
}
}
return (callback) => {
macroEngine.addMacro(name, callback, false);
}
}
});
const remarkRehypeOptions = extendLayerProxy['remark-rehype'][2];
remarkRehypeOptions.handlers = {
...builtinHandlers,
...handlers
};
for (const handler in remarkRehypeOptions.handlers) {
remarkRehypeOptions.handlers[handler] = remarkRehypeOptions.handlers[handler].bind(this);
}
if (sanitize) {
this.layers.splice(this.layers.length - 1, 0, ['rehype-sanitize', rehypeSanitize, sanitizeOptions]);
}
this.options = { toc };
extend({
layers: extendLayerProxy,
macros: registerMacroProxy
});
}
get toc () {
if (!this.options.toc) {
return
}
return this._toc
}
set toc (v) {
if (!this.options.toc) {
return
}
this._toc = v;
}
get processor () {
if (this._processor) {
return this._processor
}
this._processor = unified()
.use({ plugins: this.layers.map(l => l.slice(1)) });
return this._processor
}
async toMarkup (markdown) {
// explictly set to false to nothing is added in ./handlers/heading
this.toc = this.options.toc ? [] : undefined;
markdown = escapeVueInMarkdown(markdown);
const { contents: html } = await this.processor.process(markdown);
if (this.options.toc) {
return {
html,
toc: this.toc
}
}
return { html }
}
}
module.exports = NuxtMarkdown;