UNPKG

@nuxt/markdown

Version:

Nuxt-flavoured fork of @dimerapp/markdown

869 lines (759 loc) 17.1 kB
'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;