foremark
Version:
A technology for writing semi-plain text documents that extends upon the concept of Markdeep.
491 lines • 18.8 kB
JavaScript
Object.defineProperty(exports, "__esModule", { value: true });
var common_1 = require("./common");
var string_1 = require("../utils/string");
var dom_1 = require("../utils/dom");
/** Represents the top-level block state. */
var TopLevel = {
canContinue: function () { return false; },
continue: function () { throw new Error(); },
close: function () { throw new Error(); },
};
/**
* `BlockInitiator`/`BlockState` for unordered lists.
*/
var UnorderedList = {
markerPattern: /(?:(?:-\s+)?\[[x ]\])|-|\+|\*/,
start: function (marker, caption) {
return [
UnorderedList,
"<ul><li class=\"" + unorderedListMarkerToClass(marker) + "\">",
];
},
canContinue: function (marker) {
return /^(?:(?:-\s+)?\[[x ]\]|-|\+|\*)$/.test(marker);
},
continue: function (marker, caption) {
return "</li><li class=\"" + unorderedListMarkerToClass(marker) + "\">";
},
close: function () {
return '</li></ul>';
},
};
function unorderedListMarkerToClass(marker) {
if (marker.indexOf('[x]') >= 0) {
return 'checked';
}
else if (marker.indexOf('[ ]') >= 0) {
return 'unchecked';
}
switch (marker) {
case '-':
return 'minus';
case '+':
return 'plus';
case '*':
return 'asterisk';
default:
throw new Error();
}
}
/**
* `BlockInitiator`/`BlockState` for ordered lists.
*/
var OrderedList = /** @class */ (function () {
function OrderedList(nextCounter) {
this.nextCounter = nextCounter;
}
OrderedList.start = function (marker, caption) {
var i = parseInt(marker, 10);
return [
new OrderedList(i + 1),
i == 1 ? "<ol><li>" : "<ol start=\"" + i + "\"><li>",
];
};
OrderedList.prototype.canContinue = function (marker) {
var i = parseInt(marker, 10);
return i === this.nextCounter;
};
OrderedList.prototype.continue = function (marker, caption) {
this.nextCounter += 1;
return "</li><li>";
};
OrderedList.prototype.close = function () {
return '</li></ol>';
};
OrderedList.markerPattern = /\d+\./;
return OrderedList;
}());
;
/**
* `BlockInitiator`/`BlockState` for definition lists.
*/
var DefinitionList = {
markerPattern: /:/,
captionStyle: 1 /* PrecedingLine */,
start: function (marker, caption) {
return [
DefinitionList,
"<dl>" + (caption != null ? "<dt>" + caption + "</dt>" : '') + "<dd>",
];
},
canContinue: function (marker) {
return marker === ':';
},
continue: function (marker, caption) {
return "</dd>" + (caption != null ? "<dt>" + caption + "</dt>" : '') + "<dd>";
},
close: function () {
return '</dd></dl>';
},
};
/**
* `BlockInitiator`/`BlockState` for admonitions.
*/
var Admonition = {
markerPattern: /!!!/,
captionStyle: 2 /* SameLine */,
start: function (marker, caption) {
var _a = caption.match(/^(?:([^:]+)(?::\s*|$))?(.*)/i), _ = _a[0], type = _a[1], title = _a[2];
return [
Admonition,
"<" + "mf-admonition" /* Admonition */ + " type=\"" + (type || '').toLowerCase() + "\">" +
(title != '' ? "<" + "mf-admonition-title" /* AdmonitionTitle */ + ">" + title + "</" + "mf-admonition-title" /* AdmonitionTitle */ + ">" : ''),
];
},
canContinue: function (marker) { return false; },
continue: function (marker, caption) { throw new Error(); },
close: function () { return "</" + "mf-admonition" /* Admonition */ + ">"; },
};
/**
* `BlockInitiator`/`BlockState` for link target definitons like `[linkname]: ...`.
*/
var LinkTargetDefinition = {
markerPattern: new RegExp(/\[[^!^#][^\][]*?\]:/),
captionStyle: 0 /* None */,
verbatim: true,
start: function (marker, caption, ctx) {
var id = ctx.expand(marker.substring(1, marker.length - 2));
return [
LinkTargetDefinition,
"<" + "mf-link-target" /* LinkTarget */ + " link-id=\"" + dom_1.escapeXmlText(id) + "\">",
];
},
canContinue: function (marker) { return false; },
continue: function (marker, caption) { throw new Error(); },
close: function () { return "</" + "mf-link-target" /* LinkTarget */ + ">"; },
};
var citationPattern = new RegExp("\\[#(" + common_1.CITE_ID_RE.source + ")\\]:");
/**
* `BlockInitiator`/`BlockState` for citations like `[#notename]: ...`.
*/
var Citation = {
markerPattern: new RegExp(citationPattern.source.replace(/([^\\]|^)\(/g, '$1(?:')),
captionStyle: 0 /* None */,
start: function (marker, caption) {
var _a = citationPattern.exec(marker), _ = _a[0], idRaw = _a[1];
var id = dom_1.unescapeXmlText(idRaw);
id = " id=\"" + dom_1.escapeXmlText(id) + "\"";
return [
Citation,
"<" + "mf-cite" /* Cite */ + id + ">",
];
},
canContinue: function (marker) { return false; },
continue: function (marker, caption) { throw new Error(); },
close: function () { return "</" + "mf-cite" /* Cite */ + ">"; },
};
var endnotePattern = new RegExp("\\[(" + common_1.FLOATING_SIZE_RE.source + ")(" + common_1.ENDNOTE_ID_RE.source + ")?\\]:");
/**
* `BlockInitiator`/`BlockState` for endnotes like `[^notename]: ...`.
*/
var Endnote = {
markerPattern: new RegExp(endnotePattern.source.replace(/([^\\]|^)\(/g, '$1(?:')),
captionStyle: 0 /* None */,
start: function (marker, caption) {
var _a = endnotePattern.exec(marker), _ = _a[0], sizeRaw = _a[1], _b = _a[2], idRaw = _b === void 0 ? '' : _b;
var size = common_1.parseFloatingSize(marker.substr(1, 1));
size = size ? " size=\"" + size + "\"" : '';
var id = dom_1.unescapeXmlText(idRaw);
// `id` is optional
if (id !== '') {
id = " id=\"" + dom_1.escapeXmlText(id) + "\"";
}
return [
Endnote,
"<" + "mf-note" /* Note */ + id + size + ">",
];
},
canContinue: function (marker) { return false; },
continue: function (marker, caption) { throw new Error(); },
close: function () { return "</" + "mf-note" /* Note */ + ">"; },
};
var figurePattern = new RegExp("\\[(" + common_1.FLOATING_SIZE_RE.source + ")(" + common_1.FIGURE_ID_RE.source + ")\\]:");
/**
* `BlockInitiator`/`BlockState` for endnotes like `[^Figure figid]: capture`.
*/
var Figure = {
markerPattern: new RegExp(figurePattern.source.replace(/([^\\]|^)\(/g, '$1(?:')),
captionStyle: 2 /* SameLine */,
start: function (marker, caption) {
var _a = figurePattern.exec(marker), _ = _a[0], sizeRaw = _a[1], idRaw = _a[2];
var size = common_1.parseFloatingSize(marker.substr(1, 1));
size = size ? " size=\"" + size + "\"" : '';
var id = dom_1.unescapeXmlText(idRaw);
return [
Figure,
"<" + "mf-figure" /* Figure */ + " id=\"" + dom_1.escapeXmlText(id) + "\"" + size + ">" +
("<" + "mf-figure-caption" /* FigureCaption */ + ">" + caption + "</" + "mf-figure-caption" /* FigureCaption */ + ">"),
];
},
canContinue: function (marker) { return false; },
continue: function (marker, caption) { throw new Error(); },
close: function () { return "</" + "mf-figure" /* Figure */ + ">"; },
};
var imageBlockPattern = new RegExp(
// `![^FigType basename]`
"!\\[(" + common_1.FLOATING_SIZE_RE.source + ")(" + common_1.FIGURE_ID_RE.source + ")?\\]" +
// `[alttext]`
"s*\\[([^\\]]*)\\]" +
(
// `(URL attr...)`
"s*\\(" + common_1.MEDIA_PARAM_RE.source + "\\):"));
/**
* `BlockInitiator`/`BlockState` for image blocks.
*/
var ImageBlock = {
markerPattern: new RegExp(imageBlockPattern.source.replace(/([^\\]|^)\(/g, '$1(?:')),
start: function (marker, caption) {
var _a = imageBlockPattern.exec(marker), _ = _a[0], sizesym = _a[1], _b = _a[2], idRaw = _b === void 0 ? '' : _b, altRaw = _a[3], urlRaw = _a[4], _c = _a[5], attribsRaw = _c === void 0 ? '' : _c;
var size = common_1.parseFloatingSize(sizesym);
size = size ? " size=\"" + size + "\"" : '';
if (urlRaw.startsWith('"')) {
urlRaw = urlRaw.substring(1, urlRaw.length - 1);
}
var id = dom_1.unescapeXmlText(idRaw);
var alt = dom_1.unescapeXmlText(altRaw);
var url = dom_1.unescapeXmlText(urlRaw), attribs = dom_1.unescapeXmlText(attribsRaw);
var img = "<p><" + "mf-media" /* Media */ + " src=\"" + dom_1.escapeXmlText(url) + "\" alt=\"" + dom_1.escapeXmlText(alt) + "\"" +
(dom_1.legalizeAttributes(attribs, ['src', 'alt'], function (e) { return console.warn(e); }) + " /></p>");
if (id !== '') {
id = " id=\"" + dom_1.escapeXmlText(id) + "\"";
}
return [
ImageBlock,
"<" + "mf-figure" /* Figure */ + id + size + ">" +
img +
("<" + "mf-figure-caption" /* FigureCaption */ + ">"),
];
},
canContinue: function (marker) { return false; },
continue: function (marker, caption) { throw new Error(); },
close: function () { return "</" + "mf-figure-caption" /* FigureCaption */ + "></" + "mf-figure" /* Figure */ + ">"; },
};
var BLOCK_INITIATORS = [
UnorderedList,
OrderedList,
DefinitionList,
Admonition,
LinkTargetDefinition,
Citation,
Endnote,
Figure,
ImageBlock,
];
var MARKER_LINE_PATTERN = new RegExp('^(' +
BLOCK_INITIATORS.map(function (i) { return i.markerPattern.source; }).join('|') +
')' +
/([ \t]+|$)([^ \t][^]*|$)/.source);
var EXACT_MARKER_PATTERNS = BLOCK_INITIATORS
.map(function (i) { return [
i,
new RegExp('^(?:' + i.markerPattern.source + ')$')
]; });
var blockInitiatorFromString = function (marker) {
return EXACT_MARKER_PATTERNS.find(function (_a) {
var _ = _a[0], pattern = _a[1];
return pattern.test(marker);
})[0];
};
/**
* Replace nestable block elements.
*/
function replaceBlocks(html, ctx) {
var lines = html.split('\n');
var output = [];
// `true` if the last two elements of `output` are `['some raw text', '\n']`.
var lastOutputIsText = false;
// Stack where the *first* element is top
var levels = [
{
originalIndentation: '',
bodyIndentation: '',
state: TopLevel,
},
];
var numPendingNewlines = 0;
// Consider the following example:
//
// ```
// term1
// : definition1
// term2
// : definition2
// ```
//
// When we are at `term2`, we can't tell if `term2` is a part of the
// definition list that starts from `term1` or not. So, we store `term2`
// to `definitionTermBuffer` until it's settled.
var definitionTermBuffer = null;
function removeTrailingNewline() {
numPendingNewlines = 0;
if (output[output.length - 1] === '\n') {
output.pop(); // Remove trailing newline
}
}
function closeCurrentList() {
removeTrailingNewline();
lastOutputIsText = false;
output.push(levels[0].state.close());
output.push('\n');
levels.shift();
if (definitionTermBuffer != null) {
// The last nonmarker line we saw turned out to be not contents of
// `<dt>`.
flushDefinitionTermBuffer();
}
}
/**
* This function is called when a line stored to `definitionTermBuffer`
* turned out to be not a part of a definition list.
*/
function flushDefinitionTermBuffer() {
var notTerm = string_1.removePrefix(definitionTermBuffer[0], levels[0].bodyIndentation) + definitionTermBuffer[1];
lastOutputIsText = true;
output.push(notTerm);
output.push('\n');
definitionTermBuffer = null;
}
for (var _i = 0, lines_1 = lines; _i < lines_1.length; _i++) {
var line = lines_1[_i];
if (line.match(/^\s*$/)) {
// A blank line lacks indentation information, so we defer
// indentation change checking, whlist keeping the number of blank
// lines.
numPendingNewlines++;
continue;
}
var _a = line.match(/^([ \t]*)([^]*)$/), _2 = _a[0], indent = _a[1], lineBody = _a[2];
// Detect list marker
var markerMatch = lineBody.match(MARKER_LINE_PATTERN);
// Detect outdent
var indentCommand = void 0;
if (!markerMatch && numPendingNewlines === 0) {
// The current line might have been hard wrapped from the last line
// like this:
// ```
// - This is a long paragraph being
// wrapped. This line might look outside of the list but it's not
// ```
indentCommand = 1 /* Indent */;
}
else {
while ((indentCommand = string_1.analyzeIndent(indent, levels[0].originalIndentation))
== 2 /* Outdent */) {
closeCurrentList();
}
}
// indentCommand
// - `Indent` = The current line is inside of `levels[0]`
// - `Preserve` = The current line has the same indentation as `levels[0]`,
// which means:
// - It's outside of `levels[0]` but still inside of `levels[1]`
// - It's a caption line of the next item in `levels[0]`
// (only the case with `CaptionStyle.PrecedingLine`)
// If we don't know the body indentation level yet, then guess one from
// the first line of the body.
if (levels[0].bodyIndentation == null) {
levels[0].bodyIndentation = indent;
}
// Do not parse blocks inside a verbatim block.
if (indentCommand === 1 /* Indent */ && levels[0].state.verbatim) {
markerMatch = null;
}
if (!markerMatch) {
// No marker's here
if (indentCommand != 1 /* Indent */) {
if (levels[0].state.captionStyle === 1 /* PrecedingLine */) {
if (definitionTermBuffer != null) {
// A definiton list item cannot contain more than one
// line. So, the current definition list ends here.
closeCurrentList();
}
else {
// This might be contents of `<dt>`.
numPendingNewlines = 0;
definitionTermBuffer = [indent, lineBody];
continue;
}
}
else if (levels.length > 1) {
// We are no longer in the list.
closeCurrentList();
}
}
// Re-insert blank lines
for (; numPendingNewlines; --numPendingNewlines) {
output.push('\n');
}
// Preserve indentation
lineBody = string_1.removePrefix(indent, levels[0].bodyIndentation) + lineBody;
lastOutputIsText = true;
output.push(lineBody);
output.push('\n');
continue;
}
// Discard blank lines
numPendingNewlines = 0;
var _ = markerMatch[0], markerText = markerMatch[1], markerSpace = markerMatch[2], markerBody = markerMatch[3];
//const [markerType, markerAttr] = markerInfoFromString(markerText);
// Is this marker is an addition to the current list?
//
// If `definitionTermBuffer != null`, then the input looks like:
// ```
// term1
// : definition1
// term2
// : definition2 <----
// ```
var isContinuation = (indentCommand == 0 /* Preserve */ ||
definitionTermBuffer != null) &&
levels[0].state.canContinue(markerText);
if (!isContinuation && indentCommand != 1 /* Indent */ && levels.length > 1) {
// Close the current list first.
closeCurrentList();
}
// Compute the indentation level of the item's body. Example:
// ```
// - body
// ^^^^^^^ this part
// ```
var markerBodyIndentation = indent + ' '
.repeat(markerText.length + markerSpace.length);
var caption = null;
if (isContinuation) {
levels[0].bodyIndentation = markerBodyIndentation;
removeTrailingNewline();
if (definitionTermBuffer != null) {
// Create `<dt>` between `<dd>`s
caption = string_1.removePrefix(definitionTermBuffer[0], levels[0].originalIndentation) + definitionTermBuffer[1];
definitionTermBuffer = null;
}
if (levels[0].state.captionStyle === 2 /* SameLine */) {
caption = markerBody;
levels[0].bodyIndentation = null;
}
output.push(levels[0].state.continue(markerText, caption, ctx));
}
else {
// Start a new list.
if (definitionTermBuffer) {
throw new Error();
}
var initiator = blockInitiatorFromString(markerText);
if (initiator.captionStyle === 1 /* PrecedingLine */) {
// A definition list takes the last line as the contents of `<dt>`.
if (lastOutputIsText) {
output.pop();
caption = output.pop();
}
else {
caption = '';
}
}
var bodyIndentation = markerBodyIndentation;
if (initiator.captionStyle === 2 /* SameLine */) {
caption = markerBody;
bodyIndentation = null;
}
var _b = initiator.start(markerText, caption, ctx), state = _b[0], fragment = _b[1];
output.push(fragment);
levels.unshift({
bodyIndentation: bodyIndentation,
originalIndentation: indent,
state: state,
});
}
if (levels[0].state.captionStyle !== 2 /* SameLine */) {
// The first line of the body is treated as a caption text.
lastOutputIsText = true;
output.push(markerBody);
output.push('\n');
}
}
while (levels.length > 1) {
closeCurrentList();
}
removeTrailingNewline();
return output.join('');
}
exports.replaceBlocks = replaceBlocks;
//# sourceMappingURL=blocks.js.map
;