UNPKG

foremark

Version:

A technology for writing semi-plain text documents that extends upon the concept of Markdeep.

491 lines 18.8 kB
"use strict"; 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