markdown-it-caml
Version:
Add caml (semantic) attribute syntax for markdown-it.
315 lines (282 loc) • 9.62 kB
JavaScript
// markdown-it-caml v0.0.1 - https://github.com/wikibonsai/markdown-it-caml.git
;
var lodash = require('lodash');
var CAML = require('caml-mkdn');
function _interopNamespace(e) {
if (e && e.__esModule) return e;
var n = Object.create(null);
if (e) {
Object.keys(e).forEach(function (k) {
if (k !== 'default') {
var d = Object.getOwnPropertyDescriptor(e, k);
Object.defineProperty(n, k, d.get ? d : {
enumerable: true,
get: function () { return e[k]; }
});
}
});
}
n["default"] = e;
return Object.freeze(n);
}
var CAML__namespace = /*#__PURE__*/_interopNamespace(CAML);
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__namespace.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__namespace.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__namespace.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 = lodash.merge(defaults, opts);
caml_attrs(md, fullOpts);
}
module.exports = caml_plugin;
//# sourceMappingURL=index.cjs.js.map