UNPKG

markdown-it-caml

Version:

Add caml (semantic) attribute syntax for markdown-it.

293 lines (263 loc) 9.07 kB
// markdown-it-caml v0.0.1 - https://github.com/wikibonsai/markdown-it-caml.git import { merge } from 'lodash'; import * as CAML from 'caml-mkdn'; const caml_attrs = (md, opts) => { // ruler rules // // the 'caml' block rule is the parse that drives the markdown-it-caml plugin // should execute just before 'markdown-it-wikirefs': // [ ..., 'hr', 'caml', 'wikiattr', 'list', ... ] // // note: 'attrs' is added as an extra dummy 'alt' specifically for markdown-it-wikirefs interop md.block.ruler.after('hr', 'caml', caml_rule, { alt: ['paragraph', 'attrs'] }); // in case bugs show up: [ 'paragraph', 'reference', 'blockquote', 'list' ] // the 'attrbox' rule is the midpoint between the parse and render rules. if (opts.attrs.render) { md.core.ruler.after('inline', 'attrbox', attrbox); } // render rules // md.renderer.rules.metadata_caml = metadata_caml; md.renderer.rules.attr_open = attr_open; md.renderer.rules.attr_key = attr_key; md.renderer.rules.attr_val = attr_val; md.renderer.rules.attr_close = attr_close; // ruler function caml_rule(state, startLine, endLine, silent) { // init if (!state.env.attrs) { state.env.attrs = {}; } // from: https://github.com/markdown-it/markdown-it/blob/df4607f1d4d4be7fdc32e71c04109aea8cc373fa/lib/rules_block/list.js#L132 // if it's indented more than 3 spaces, it should be a code block if (state.sCount[startLine] - state.blkIndent >= 4) { return false; } // 'bMarks' = beginning of line markers // 'eMarks' = end of line markers let pos = state.bMarks[startLine]; let max = state.eMarks[startLine]; // 'attrs' must be at the top-most-level // !('list' | 'blockquote' | 'reference' | 'footnote') if (state.parentType !== 'root' && state.parentType !== 'paragraph') { return false; } const chunk = state.src.substring(pos, max); const lineOneMatch = CAML.RGX.LINE.KEY.exec(chunk); if (lineOneMatch === null) { return false; } // is in a list item // note: this is only necessary for unprefixed wikiattrs // todo: keep an eye on this...might cause problems... if (lineOneMatch[0].indexOf('- ') === 0 || lineOneMatch[0].indexOf('* ') === 0 || lineOneMatch[0].indexOf('+ ') === 0) { return false; } // "Don't run any pairs in validation mode": // 'silent' is used when this rule is being checked against // in another rule to see whether or not the other rule should // kick out for this (or some other) one. return 'true' to // signify that the kick out should happen. if (silent) { return true; } /* * handle match and return true */ let iterLine = 0; let m; const curAttrItems = []; const key = lineOneMatch[1].trim(); const value = lineOneMatch[2]; // values // - single/comma-separated list if (value !== '' && value !== null && value !== undefined) { iterLine += 1; let curVal = ''; let inDoubleQuote = false; let inSingleQuote = false; for (const char of value) { // comma separation if (!inDoubleQuote && !inSingleQuote && char === ',') { curAttrItems.push(curVal.trim()); curVal = ''; continue; } // quote if (/"/.test(char)) { inDoubleQuote = !inDoubleQuote; } if (/'/.test(char)) { inSingleQuote = !inSingleQuote; } // char curVal += char; } // single / last value curAttrItems.push(curVal.trim()); // - mkdn-separated list } else { // loop through each markdown-style list item // do-while: https://stackoverflow.com/a/6323598 do { // increment iterLine += 1; pos = state.bMarks[startLine + iterLine]; max = state.eMarks[startLine + iterLine]; const thisChunk = state.src.substring(pos, max); m = CAML.RGX.LINE.LIST_ITEM.exec(thisChunk); if (m !== null) { // m[0]: full match; // m[1]: bullet type; // m[2]: filename curAttrItems.push(m[2]); } } while (m); } // set 'state.env.attrs' to trigger tokens -- if valid. if (curAttrItems.length === 0) { return false; } else { // init if (!state.env.attrs[key]) { state.env.attrs[key] = []; } // prep renderables for (const attrItem of curAttrItems) { const typedItem = CAML.resolve(attrItem); state.env.attrs[key].push(typedItem); } // metadata if (opts.addAttr) { const tok = new state.Token('metadata_caml', '', 0); state.tokens.push(tok); tok.attrSet('key', key); tok.attrSet('vals', state.env.attrs[key].map(item => item.string)); } state.line += iterLine; return true; } } function attrbox(state) { if (!state.env.attrs || JSON.stringify(state.env.attrs) === '{}' || Object.keys(state.env.attrs).length === 0) { return; } const tokens = []; // open // const tokOpen = new state.Token('attr_open', '', 0); // tokOpen.map = [startLine, iterLine]; tokens.push(tokOpen); // body // for (const key in state.env.attrs) { // key / linktype const tokType = new state.Token('attr_key', '', 0); tokType.attrSet('key', key); tokens.push(tokType); // values / items for (const item of state.env.attrs[key]) { // wikirefs // todo: only add token if 'markdown-it-wikirefs' is detected let tokItem; if (item.type === 'wiki') { tokItem = new state.Token('wikiattr_val', '', 1); const filename = item.filename; if (!filename) { continue; } tokItem.attrSet('key', key); tokItem.attrSet('val', filename); // primitives } else { tokItem = new state.Token('attr_val', '', 1); tokItem.attrSet('key', key); tokItem.attrSet('type', item.type); tokItem.attrSet('val', item.string); } //else { // todo: error // } tokens.push(tokItem); } } // close // const tokClose = new state.Token('attr_close', '', 0); tokens.push(tokClose); // add infobox tokens to **front** of token stream (should occur after flush) if (tokens) { state.tokens = tokens.concat(state.tokens); } } // "render" function metadata_caml(tokens, index, mdOpts, env) { const token = tokens[index]; const attrtype = token.attrGet('key'); // @ts-expect-error: forcing array -- technically not supposed to, but it works so nicely here (see note above) const filenames = token.attrGet('vals'); if (attrtype && filenames && opts.addAttr) { for (const filename of filenames) { opts.addAttr(env, attrtype, filename); } } return ''; } // render /* eslint-disable-next-line @typescript-eslint/no-unused-vars */ function attr_open(tokens, index, mdOpts, env) { return `<aside class="${opts.cssNames.attrbox}">\n<span class="${opts.cssNames.attrboxTitle}">${opts.attrs.title}</span>\n<dl>\n`; } // attr : key : attrtype /* eslint-disable-next-line @typescript-eslint/no-unused-vars */ function attr_key(tokens, index, mdOpts, env) { const token = tokens[index]; const key = token.attrGet('key'); if (key === null) { return '<dt>attr key error</dt>\n'; } else { return `<dt>${key}</dt>\n`; } } // attr : val(s) : item(s) /* eslint-disable-next-line @typescript-eslint/no-unused-vars */ function attr_val(tokens, index, mdOpts, env) { const token = tokens[index]; // invalid if (token === null) { return '<dd>attr error</dd>\n'; // primitives } else { var _strValue; const key = token.attrGet('key'); const valType = token.attrGet('type'); let strValue = token.attrGet('val'); // multi-line string if ((_strValue = strValue) !== null && _strValue !== void 0 && _strValue.includes('\n')) ; else { const keySlug = key ? key.trim().toLowerCase().replace(/ /g, '-').replace(/[^\w-]+/g, '') : ''; strValue = `<span class="${opts.cssNames.attr} ${valType} ${keySlug}">${strValue}</span>`; } return `<dd>${strValue}</dd>\n`; } } /* eslint-disable-next-line @typescript-eslint/no-unused-vars */ function attr_close(tokens, index, mdOpts, env) { delete env.cur_attr_key; return '</dl>\n</aside>\n'; } }; function caml_plugin(md, opts) { // opts const defaults = { attrs: { enable: true, render: true, title: 'Attributes' }, cssNames: { attr: 'attr', wiki: 'wiki', invalid: 'invalid', attrbox: 'attrbox', attrboxTitle: 'attrbox-title' } }; const fullOpts = merge(defaults, opts); caml_attrs(md, fullOpts); } export { caml_plugin as default }; //# sourceMappingURL=index.esm.js.map