readme-ssi
Version:
Update your markdown readme's ToC and snippets without seperate template or docs files, according to directives that remind of Server Side Includes. [npm search keywords: code examples, documentation, table of contents]
135 lines (107 loc) • 3.73 kB
JavaScript
/*jslint indent: 2, maxlen: 80, continue: false, unparam: false, node: true */
/* -*- tab-width: 2 -*- */
;
var EX = module.exports, coreCmds = require('./core_cmds.js'),
cmdVerbatim = require('./cmd_verbatim.js'),
kisi = require('./kitchen_sink.js');
EX.defaultTocPfx = '### ';
EX.defaultTocFmt = ' * [&$caption;](#&$anchor;)';
EX.tocStopMarkRgx = /^[ \t]*<!--#toc[ \t]+stop="scan"[ \t]+-->/;
EX.markDownLinksRgx = /\[(\S*?)\]\(\S*?\)/g;
EX.defaultAnchor = 'toc-#';
EX.cmd = {};
EX.cmd.toc = function (text, tag, buf) {
if (text) { throw tag.err('unexpected input text'); }
var renderer = this, stop = tag.popAttr('stop'), headlines = [];
switch (stop) {
case undefined:
break;
case 'scan':
return '';
default:
throw tag.err('unsupported stop mode: ' + stop);
}
tag.try(coreCmds.replaceUntilMark, [buf]);
tag.tocText = EX.tocGen(tag, buf, renderer, headlines);
EX.tocAddAnchorTargets(tag, buf, headlines);
return tag.tocText;
};
EX.tocGen = function (tag, buf, renderer, headlines) {
var scanNextLine, ln, alreadyEatenOffset = -1, opts = {
pfx: tag.popAttr('pfx', EX.defaultTocPfx),
fmt: tag.popAttr('fmt', EX.defaultTocFmt),
captionStart: tag.popAttr('cap-start'),
captionEnd: tag.popAttr('cap-end'),
anchor: tag.popAttr('anchor', EX.defaultAnchor),
};
if (!headlines) { headlines = []; }
buf = buf.clone();
headlines.srcOffset = buf.calcPosLnChar();
scanNextLine = function () {
if (cmdVerbatim.checkEat(renderer, buf)) { return; }
ln = buf.eatLine().replace(/\s+$/, '');
if (!ln) { return; }
if (EX.tocStopMarkRgx.exec(ln)) {
buf = null;
return;
}
ln = EX.tocFindCaption(ln, opts);
if (!ln) { return; }
ln = {
caption: ln,
anchor: opts.anchor.replace(/#/g, EX.dashify(ln)),
srcPos: buf.calcPosLnChar(),
};
ln.srcPos.ln += alreadyEatenOffset;
ln.tocLine = tag.try(EX.tocFmt, [opts.fmt, {
caption: ln.caption.replace(/\[|\]/g, EX.xmlCharRef),
anchor: ln.anchor,
}], ln);
headlines.push(ln);
};
while (buf && buf.notEmpty()) { scanNextLine(); }
return headlines.map(function (hl) { return hl.tocLine; }).concat('',
'' // <-- Github work-around
).join('\n');
};
EX.tocFindCaption = function (ln, opts) {
if (!ln.startsWith(opts.pfx)) { return; }
ln = ln.slice(opts.pfx.length);
if (!ln) { return; }
ln = ln.replace(EX.markDownLinksRgx, '$1');
ln = kisi.multiMarkSplit(ln, opts.captionStart, -1);
ln = kisi.multiMarkSplit(ln, opts.captionEnd, 0);
return ln;
};
EX.dashify = function (text) {
return text.toLowerCase().match(/([a-z0-9]+)/g).join('-');
};
EX.xmlCharRef = function (c) {
c = c.charCodeAt(0).toString(16).toUpperCase();
return '&#x' + (c.length % 2 ? '0' : '') + c + ';';
};
EX.tocFmt = function (fmt, slots) {
return fmt.replace(/\&\$([A-Za-z0-9\-]+);/g, function (match, slot) {
match = slots[slot];
if (match !== undefined) { return match; }
throw new Error('unsupporte toc fmt slot name: ' + slot);
});
};
EX.tocAddAnchorTargets = function (tag, buf, headlines) {
var bufLines = buf.buf.split(/\n/);
tag = '<a class="readme-ssi-toc-target"';
headlines.forEach(function (hl) {
var lnumRel = hl.srcPos.ln - headlines.srcOffset.ln,
anchorLineNum = lnumRel - 1,
anchorLineOld = bufLines[anchorLineNum],
anchorLineNew = tag + ' id="' + hl.anchor +
'" name="' + hl.anchor + '"></a>';
if (anchorLineOld && anchorLineOld.startsWith(tag)) {
bufLines[anchorLineNum] = anchorLineNew;
} else {
bufLines[lnumRel] = anchorLineNew + '\n' + bufLines[lnumRel];
}
});
buf.buf = bufLines.join('\n');
};
/*scroll*/