2017-12-15 22:55:51 -08:00
|
|
|
|
// @ts-check
|
|
|
|
|
|
2015-03-08 23:08:43 -07:00
|
|
|
|
"use strict";
|
|
|
|
|
|
2015-03-15 23:39:17 -07:00
|
|
|
|
// Regular expression for matching common newline characters
|
2018-04-29 22:29:03 -07:00
|
|
|
|
// See NEWLINES_RE in markdown-it/lib/rules_core/normalize.js
|
2019-09-14 22:31:08 -07:00
|
|
|
|
const newLineRe = /\r\n?|\n/g;
|
2019-08-16 19:56:52 -07:00
|
|
|
|
module.exports.newLineRe = newLineRe;
|
2015-03-15 23:39:17 -07:00
|
|
|
|
|
2017-07-02 20:33:29 -07:00
|
|
|
|
// Regular expression for matching common front matter (YAML and TOML)
|
2019-03-17 22:05:50 -07:00
|
|
|
|
module.exports.frontMatterRe =
|
2019-07-27 18:10:26 -07:00
|
|
|
|
// eslint-disable-next-line max-len
|
2020-11-14 19:40:15 -08:00
|
|
|
|
/((^---\s*$[^]*?^---\s*$)|(^\+\+\+\s*$[^]*?^(\+\+\+|\.\.\.)\s*$)|(^\{\s*$[^]*?^\}\s*$))(\r\n|\r|\n|$)/m;
|
2015-07-25 22:18:30 -07:00
|
|
|
|
|
2022-02-12 17:46:46 -08:00
|
|
|
|
// Regular expression for matching the start of inline disable/enable comments
|
|
|
|
|
const inlineCommentStartRe =
|
2019-06-08 19:26:11 -07:00
|
|
|
|
// eslint-disable-next-line max-len
|
2022-05-15 15:59:11 -07:00
|
|
|
|
/(<!--\s*markdownlint-(disable|enable|capture|restore|disable-file|enable-file|disable-line|disable-next-line|configure-file))(?:\s|-->)/ig;
|
2022-02-12 17:46:46 -08:00
|
|
|
|
module.exports.inlineCommentStartRe = inlineCommentStartRe;
|
2015-09-26 16:55:33 -07:00
|
|
|
|
|
2022-04-10 05:37:57 +00:00
|
|
|
|
// Regular expression for matching HTML elements
|
2022-06-02 22:17:32 -07:00
|
|
|
|
const htmlElementRe = /<(([A-Za-z][A-Za-z0-9-]*)(?:\s[^`>]*)?)\/?>/g;
|
2022-04-25 21:50:33 -07:00
|
|
|
|
module.exports.htmlElementRe = htmlElementRe;
|
2022-04-10 05:37:57 +00:00
|
|
|
|
|
2018-01-18 21:27:07 -08:00
|
|
|
|
// Regular expressions for range matching
|
2020-06-21 21:47:32 -07:00
|
|
|
|
module.exports.bareUrlRe = /(?:http|ftp)s?:\/\/[^\s\]"']*(?:\/|[^\s\]"'\W])/ig;
|
2019-12-01 17:30:47 -08:00
|
|
|
|
module.exports.listItemMarkerRe = /^([\s>]*)(?:[*+-]|\d+[.)])\s+/;
|
2018-06-15 22:37:12 -07:00
|
|
|
|
module.exports.orderedListItemMarkerRe = /^[\s>]*0*(\d+)[.)]/;
|
2018-01-18 21:27:07 -08:00
|
|
|
|
|
2020-05-10 17:06:07 -07:00
|
|
|
|
// Regular expression for all instances of emphasis markers
|
|
|
|
|
const emphasisMarkersRe = /[_*]/g;
|
2020-04-25 15:10:07 -07:00
|
|
|
|
|
2022-06-01 20:23:08 -07:00
|
|
|
|
// Regular expression for reference links (full and collapsed but not shortcut)
|
|
|
|
|
const referenceLinkRe =
|
2022-06-12 18:00:37 -07:00
|
|
|
|
/!?\\?\[((?:\[[^\]\0]*]|[^\]\0])*)](?:(?:\[([^\]\0]*)\])|[^(]|$)/g;
|
2022-06-01 20:23:08 -07:00
|
|
|
|
|
|
|
|
|
// Regular expression for link reference definitions
|
|
|
|
|
const linkReferenceDefinitionRe = /^ {0,3}\[([^\]]*[^\\])]:/;
|
|
|
|
|
module.exports.linkReferenceDefinitionRe = linkReferenceDefinitionRe;
|
2020-05-08 16:01:42 -07:00
|
|
|
|
|
2019-06-06 22:21:31 -07:00
|
|
|
|
// All punctuation characters (normal and full-width)
|
2020-11-17 20:32:17 -08:00
|
|
|
|
const allPunctuation = ".,;:!?。,;:!?";
|
|
|
|
|
module.exports.allPunctuation = allPunctuation;
|
|
|
|
|
|
|
|
|
|
// All punctuation characters without question mark (normal and full-width)
|
|
|
|
|
module.exports.allPunctuationNoQuestion = allPunctuation.replace(/[??]/gu, "");
|
2019-06-06 22:21:31 -07:00
|
|
|
|
|
2018-02-27 21:14:02 -08:00
|
|
|
|
// Returns true iff the input is a number
|
|
|
|
|
module.exports.isNumber = function isNumber(obj) {
|
|
|
|
|
return typeof obj === "number";
|
|
|
|
|
};
|
|
|
|
|
|
2018-02-25 16:04:13 -08:00
|
|
|
|
// Returns true iff the input is a string
|
|
|
|
|
module.exports.isString = function isString(obj) {
|
|
|
|
|
return typeof obj === "string";
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Returns true iff the input string is empty
|
|
|
|
|
module.exports.isEmptyString = function isEmptyString(str) {
|
|
|
|
|
return str.length === 0;
|
|
|
|
|
};
|
|
|
|
|
|
2019-09-14 13:39:27 -07:00
|
|
|
|
// Returns true iff the input is an object
|
|
|
|
|
module.exports.isObject = function isObject(obj) {
|
|
|
|
|
return (obj !== null) && (typeof obj === "object") && !Array.isArray(obj);
|
|
|
|
|
};
|
|
|
|
|
|
2022-02-11 21:54:43 -08:00
|
|
|
|
/**
|
|
|
|
|
* Returns true iff the input line is blank (contains nothing, whitespace, or
|
|
|
|
|
* comments (unclosed start/end comments allowed)).
|
|
|
|
|
*
|
|
|
|
|
* @param {string} line Input line.
|
|
|
|
|
* @returns {boolean} True iff line is blank.
|
|
|
|
|
*/
|
|
|
|
|
function isBlankLine(line) {
|
|
|
|
|
const startComment = "<!--";
|
|
|
|
|
const endComment = "-->";
|
|
|
|
|
const removeComments = (s) => {
|
|
|
|
|
// eslint-disable-next-line no-constant-condition
|
|
|
|
|
while (true) {
|
|
|
|
|
const start = s.indexOf(startComment);
|
|
|
|
|
const end = s.indexOf(endComment);
|
|
|
|
|
if ((end !== -1) && ((start === -1) || (end < start))) {
|
|
|
|
|
// Unmatched end comment is first
|
|
|
|
|
s = s.slice(end + endComment.length);
|
|
|
|
|
} else if ((start !== -1) && (end !== -1)) {
|
|
|
|
|
// Start comment is before end comment
|
|
|
|
|
s = s.slice(0, start) + s.slice(end + endComment.length);
|
|
|
|
|
} else if ((start !== -1) && (end === -1)) {
|
|
|
|
|
// Unmatched start comment is last
|
|
|
|
|
s = s.slice(0, start);
|
|
|
|
|
} else {
|
|
|
|
|
// No more comments to remove
|
|
|
|
|
return s;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
2021-12-20 04:18:45 +00:00
|
|
|
|
return (
|
|
|
|
|
!line ||
|
|
|
|
|
!line.trim() ||
|
2022-02-11 21:54:43 -08:00
|
|
|
|
!removeComments(line).replace(/>/g, "").trim()
|
2021-12-20 04:18:45 +00:00
|
|
|
|
);
|
2022-02-11 21:54:43 -08:00
|
|
|
|
}
|
|
|
|
|
module.exports.isBlankLine = isBlankLine;
|
2019-03-20 21:48:18 -07:00
|
|
|
|
|
2020-01-23 19:42:46 -08:00
|
|
|
|
/**
|
|
|
|
|
* Compare function for Array.prototype.sort for ascending order of numbers.
|
|
|
|
|
*
|
|
|
|
|
* @param {number} a First number.
|
|
|
|
|
* @param {number} b Second number.
|
|
|
|
|
* @returns {number} Positive value if a>b, negative value if b<a, 0 otherwise.
|
|
|
|
|
*/
|
|
|
|
|
module.exports.numericSortAscending = function numericSortAscending(a, b) {
|
|
|
|
|
return a - b;
|
|
|
|
|
};
|
|
|
|
|
|
2019-03-28 22:06:42 -07:00
|
|
|
|
// Returns true iff the sorted array contains the specified element
|
|
|
|
|
module.exports.includesSorted = function includesSorted(array, element) {
|
|
|
|
|
let left = 0;
|
|
|
|
|
let right = array.length - 1;
|
|
|
|
|
while (left <= right) {
|
2020-09-12 12:01:20 -07:00
|
|
|
|
// eslint-disable-next-line no-bitwise
|
2019-03-28 22:06:42 -07:00
|
|
|
|
const mid = (left + right) >> 1;
|
|
|
|
|
if (array[mid] < element) {
|
|
|
|
|
left = mid + 1;
|
|
|
|
|
} else if (array[mid] > element) {
|
|
|
|
|
right = mid - 1;
|
|
|
|
|
} else {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
};
|
|
|
|
|
|
2021-01-30 14:36:11 -08:00
|
|
|
|
// Replaces the content of properly-formatted CommonMark comments with "."
|
2017-07-16 23:08:47 -07:00
|
|
|
|
// This preserves the line/column information for the rest of the document
|
2021-01-30 13:08:57 -08:00
|
|
|
|
// https://spec.commonmark.org/0.29/#html-blocks
|
|
|
|
|
// https://spec.commonmark.org/0.29/#html-comment
|
2018-04-27 22:05:34 -07:00
|
|
|
|
const htmlCommentBegin = "<!--";
|
|
|
|
|
const htmlCommentEnd = "-->";
|
2018-01-18 21:27:07 -08:00
|
|
|
|
module.exports.clearHtmlCommentText = function clearHtmlCommentText(text) {
|
2018-04-27 22:05:34 -07:00
|
|
|
|
let i = 0;
|
2017-07-16 23:08:47 -07:00
|
|
|
|
while ((i = text.indexOf(htmlCommentBegin, i)) !== -1) {
|
2021-01-30 13:08:57 -08:00
|
|
|
|
const j = text.indexOf(htmlCommentEnd, i + 2);
|
2017-07-16 23:08:47 -07:00
|
|
|
|
if (j === -1) {
|
2020-03-09 23:06:13 -07:00
|
|
|
|
// Un-terminated comments are treated as text
|
|
|
|
|
break;
|
2017-07-16 23:08:47 -07:00
|
|
|
|
}
|
2021-01-30 13:08:57 -08:00
|
|
|
|
// If the comment has content...
|
|
|
|
|
if (j > i + htmlCommentBegin.length) {
|
|
|
|
|
let k = i - 1;
|
|
|
|
|
while (text[k] === " ") {
|
|
|
|
|
k--;
|
|
|
|
|
}
|
|
|
|
|
// If comment is not within an indented code block...
|
|
|
|
|
if (k >= i - 4) {
|
|
|
|
|
const content = text.slice(i + htmlCommentBegin.length, j);
|
|
|
|
|
const isBlock = (k < 0) || (text[k] === "\n");
|
|
|
|
|
const isValid = isBlock ||
|
|
|
|
|
(!content.startsWith(">") && !content.startsWith("->") &&
|
|
|
|
|
!content.endsWith("-") && !content.includes("--"));
|
|
|
|
|
// If a valid block/inline comment...
|
|
|
|
|
if (isValid) {
|
2022-06-04 22:59:19 -07:00
|
|
|
|
text =
|
|
|
|
|
text.slice(0, i + htmlCommentBegin.length) +
|
|
|
|
|
content.replace(/[^\r\n]/g, ".") +
|
|
|
|
|
text.slice(j);
|
2021-01-30 13:08:57 -08:00
|
|
|
|
}
|
|
|
|
|
}
|
2017-07-16 23:08:47 -07:00
|
|
|
|
}
|
|
|
|
|
i = j + htmlCommentEnd.length;
|
|
|
|
|
}
|
|
|
|
|
return text;
|
2018-01-18 21:27:07 -08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Escapes a string for use in a RegExp
|
|
|
|
|
module.exports.escapeForRegExp = function escapeForRegExp(str) {
|
|
|
|
|
return str.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&");
|
|
|
|
|
};
|
|
|
|
|
|
2019-04-29 22:09:03 -07:00
|
|
|
|
// Un-escapes Markdown content (simple algorithm; not a parser)
|
|
|
|
|
const escapedMarkdownRe = /\\./g;
|
|
|
|
|
module.exports.unescapeMarkdown =
|
|
|
|
|
function unescapeMarkdown(markdown, replacement) {
|
|
|
|
|
return markdown.replace(escapedMarkdownRe, (match) => {
|
|
|
|
|
const char = match[1];
|
|
|
|
|
if ("!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~".includes(char)) {
|
|
|
|
|
return replacement || char;
|
|
|
|
|
}
|
|
|
|
|
return match;
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
2020-01-23 19:42:46 -08:00
|
|
|
|
/**
|
|
|
|
|
* Return the string representation of a fence markup character.
|
|
|
|
|
*
|
|
|
|
|
* @param {string} markup Fence string.
|
|
|
|
|
* @returns {string} String representation.
|
|
|
|
|
*/
|
|
|
|
|
module.exports.fencedCodeBlockStyleFor =
|
|
|
|
|
function fencedCodeBlockStyleFor(markup) {
|
|
|
|
|
switch (markup[0]) {
|
|
|
|
|
case "~":
|
|
|
|
|
return "tilde";
|
|
|
|
|
default:
|
|
|
|
|
return "backtick";
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2021-10-21 06:42:48 +02:00
|
|
|
|
/**
|
|
|
|
|
* Return the string representation of a emphasis or strong markup character.
|
|
|
|
|
*
|
|
|
|
|
* @param {string} markup Emphasis or strong string.
|
|
|
|
|
* @returns {string} String representation.
|
|
|
|
|
*/
|
|
|
|
|
module.exports.emphasisOrStrongStyleFor =
|
|
|
|
|
function emphasisOrStrongStyleFor(markup) {
|
|
|
|
|
switch (markup[0]) {
|
|
|
|
|
case "*":
|
|
|
|
|
return "asterisk";
|
|
|
|
|
default:
|
|
|
|
|
return "underscore";
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2020-01-23 19:42:46 -08:00
|
|
|
|
/**
|
|
|
|
|
* Return the number of characters of indent for a token.
|
|
|
|
|
*
|
|
|
|
|
* @param {Object} token MarkdownItToken instance.
|
|
|
|
|
* @returns {number} Characters of indent.
|
|
|
|
|
*/
|
2018-03-05 20:56:12 -08:00
|
|
|
|
function indentFor(token) {
|
2018-04-27 22:05:34 -07:00
|
|
|
|
const line = token.line.replace(/^[\s>]*(> |>)/, "");
|
2021-02-06 19:55:22 -08:00
|
|
|
|
return line.length - line.trimStart().length;
|
2018-03-05 20:56:12 -08:00
|
|
|
|
}
|
|
|
|
|
module.exports.indentFor = indentFor;
|
2018-01-18 21:27:07 -08:00
|
|
|
|
|
|
|
|
|
// Returns the heading style for a heading token
|
|
|
|
|
module.exports.headingStyleFor = function headingStyleFor(token) {
|
|
|
|
|
if ((token.map[1] - token.map[0]) === 1) {
|
2018-04-28 14:49:31 -07:00
|
|
|
|
if (/[^\\]#\s*$/.test(token.line)) {
|
2018-01-18 21:27:07 -08:00
|
|
|
|
return "atx_closed";
|
|
|
|
|
}
|
|
|
|
|
return "atx";
|
|
|
|
|
}
|
|
|
|
|
return "setext";
|
|
|
|
|
};
|
|
|
|
|
|
2020-01-23 19:42:46 -08:00
|
|
|
|
/**
|
|
|
|
|
* Return the string representation of an unordered list marker.
|
|
|
|
|
*
|
|
|
|
|
* @param {Object} token MarkdownItToken instance.
|
|
|
|
|
* @returns {string} String representation.
|
|
|
|
|
*/
|
|
|
|
|
module.exports.unorderedListStyleFor = function unorderedListStyleFor(token) {
|
|
|
|
|
switch (token.markup) {
|
|
|
|
|
case "-":
|
|
|
|
|
return "dash";
|
|
|
|
|
case "+":
|
|
|
|
|
return "plus";
|
|
|
|
|
// case "*":
|
|
|
|
|
default:
|
|
|
|
|
return "asterisk";
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Calls the provided function for each matching token.
|
|
|
|
|
*
|
|
|
|
|
* @param {Object} params RuleParams instance.
|
|
|
|
|
* @param {string} type Token type identifier.
|
|
|
|
|
* @param {Function} handler Callback function.
|
|
|
|
|
* @returns {void}
|
|
|
|
|
*/
|
2019-03-30 14:36:04 -07:00
|
|
|
|
function filterTokens(params, type, handler) {
|
2022-06-07 22:59:48 -07:00
|
|
|
|
for (const token of params.tokens) {
|
2018-03-01 22:37:37 -08:00
|
|
|
|
if (token.type === type) {
|
2019-03-30 14:36:04 -07:00
|
|
|
|
handler(token);
|
2018-03-01 22:37:37 -08:00
|
|
|
|
}
|
2022-06-07 22:59:48 -07:00
|
|
|
|
}
|
2017-07-16 23:08:47 -07:00
|
|
|
|
}
|
2018-01-18 21:27:07 -08:00
|
|
|
|
module.exports.filterTokens = filterTokens;
|
|
|
|
|
|
2021-01-24 17:50:39 -08:00
|
|
|
|
/**
|
|
|
|
|
* Returns whether a token is a math block (created by markdown-it-texmath).
|
|
|
|
|
*
|
|
|
|
|
* @param {Object} token MarkdownItToken instance.
|
|
|
|
|
* @returns {boolean} True iff token is a math block.
|
|
|
|
|
*/
|
|
|
|
|
function isMathBlock(token) {
|
|
|
|
|
return (
|
2021-12-17 01:56:42 +00:00
|
|
|
|
((token.tag === "$$") || (token.tag === "math")) &&
|
2021-01-24 17:50:39 -08:00
|
|
|
|
token.type.startsWith("math_block") &&
|
|
|
|
|
!token.type.endsWith("_end")
|
|
|
|
|
);
|
|
|
|
|
}
|
2021-12-23 04:34:25 +00:00
|
|
|
|
module.exports.isMathBlock = isMathBlock;
|
2021-01-24 17:50:39 -08:00
|
|
|
|
|
2019-04-10 21:26:59 -07:00
|
|
|
|
// Get line metadata array
|
|
|
|
|
module.exports.getLineMetadata = function getLineMetadata(params) {
|
2021-01-24 17:50:39 -08:00
|
|
|
|
const lineMetadata = params.lines.map(
|
|
|
|
|
(line, index) => [ line, index, false, 0, false, false, false, false ]
|
|
|
|
|
);
|
|
|
|
|
filterTokens(params, "fence", (token) => {
|
2019-01-12 16:29:10 -08:00
|
|
|
|
lineMetadata[token.map[0]][3] = 1;
|
|
|
|
|
lineMetadata[token.map[1] - 1][3] = -1;
|
2019-01-10 21:29:27 -08:00
|
|
|
|
for (let i = token.map[0] + 1; i < token.map[1] - 1; i++) {
|
2019-01-12 16:29:10 -08:00
|
|
|
|
lineMetadata[i][2] = true;
|
2018-03-01 22:37:37 -08:00
|
|
|
|
}
|
|
|
|
|
});
|
2021-01-24 17:50:39 -08:00
|
|
|
|
filterTokens(params, "code_block", (token) => {
|
2018-04-27 22:05:34 -07:00
|
|
|
|
for (let i = token.map[0]; i < token.map[1]; i++) {
|
2019-01-12 16:29:10 -08:00
|
|
|
|
lineMetadata[i][2] = true;
|
2018-03-01 22:37:37 -08:00
|
|
|
|
}
|
|
|
|
|
});
|
2021-01-24 17:50:39 -08:00
|
|
|
|
filterTokens(params, "table_open", (token) => {
|
2018-04-27 22:05:34 -07:00
|
|
|
|
for (let i = token.map[0]; i < token.map[1]; i++) {
|
2019-01-12 16:29:10 -08:00
|
|
|
|
lineMetadata[i][4] = true;
|
2018-03-01 22:37:37 -08:00
|
|
|
|
}
|
|
|
|
|
});
|
2021-01-24 17:50:39 -08:00
|
|
|
|
filterTokens(params, "list_item_open", (token) => {
|
2020-03-28 14:16:28 -07:00
|
|
|
|
let count = 1;
|
2019-08-02 22:58:41 -07:00
|
|
|
|
for (let i = token.map[0]; i < token.map[1]; i++) {
|
2020-03-28 14:16:28 -07:00
|
|
|
|
lineMetadata[i][5] = count;
|
|
|
|
|
count++;
|
2019-08-02 22:58:41 -07:00
|
|
|
|
}
|
|
|
|
|
});
|
2021-01-24 17:50:39 -08:00
|
|
|
|
filterTokens(params, "hr", (token) => {
|
2020-03-28 14:16:28 -07:00
|
|
|
|
lineMetadata[token.map[0]][6] = true;
|
|
|
|
|
});
|
2022-06-08 22:10:27 -07:00
|
|
|
|
for (const token of params.tokens.filter(isMathBlock)) {
|
2021-01-24 17:50:39 -08:00
|
|
|
|
for (let i = token.map[0]; i < token.map[1]; i++) {
|
|
|
|
|
lineMetadata[i][7] = true;
|
|
|
|
|
}
|
2022-06-08 22:10:27 -07:00
|
|
|
|
}
|
2019-04-10 21:26:59 -07:00
|
|
|
|
return lineMetadata;
|
|
|
|
|
};
|
|
|
|
|
|
2021-11-23 04:40:05 +00:00
|
|
|
|
/**
|
|
|
|
|
* Calls the provided function for each line.
|
|
|
|
|
*
|
|
|
|
|
* @param {Object} lineMetadata Line metadata object.
|
|
|
|
|
* @param {Function} handler Function taking (line, lineIndex, inCode, onFence,
|
|
|
|
|
* inTable, inItem, inBreak, inMath).
|
|
|
|
|
* @returns {void}
|
|
|
|
|
*/
|
|
|
|
|
function forEachLine(lineMetadata, handler) {
|
2022-06-08 22:10:27 -07:00
|
|
|
|
for (const metadata of lineMetadata) {
|
2019-04-10 21:26:59 -07:00
|
|
|
|
handler(...metadata);
|
2022-06-08 22:10:27 -07:00
|
|
|
|
}
|
2021-11-23 04:40:05 +00:00
|
|
|
|
}
|
|
|
|
|
module.exports.forEachLine = forEachLine;
|
2018-03-01 22:37:37 -08:00
|
|
|
|
|
2019-04-10 21:26:59 -07:00
|
|
|
|
// Returns (nested) lists as a flat array (in order)
|
2021-06-17 22:01:27 -07:00
|
|
|
|
module.exports.flattenLists = function flattenLists(tokens) {
|
2018-04-27 22:05:34 -07:00
|
|
|
|
const flattenedLists = [];
|
|
|
|
|
const stack = [];
|
|
|
|
|
let current = null;
|
2020-03-21 13:17:34 -07:00
|
|
|
|
let nesting = 0;
|
|
|
|
|
const nestingStack = [];
|
2018-04-27 22:05:34 -07:00
|
|
|
|
let lastWithMap = { "map": [ 0, 1 ] };
|
2022-06-08 22:10:27 -07:00
|
|
|
|
for (const token of tokens) {
|
2018-03-01 22:37:37 -08:00
|
|
|
|
if ((token.type === "bullet_list_open") ||
|
|
|
|
|
(token.type === "ordered_list_open")) {
|
|
|
|
|
// Save current context and start a new one
|
|
|
|
|
stack.push(current);
|
|
|
|
|
current = {
|
|
|
|
|
"unordered": (token.type === "bullet_list_open"),
|
|
|
|
|
"parentsUnordered": !current ||
|
|
|
|
|
(current.unordered && current.parentsUnordered),
|
|
|
|
|
"open": token,
|
2018-03-05 20:56:12 -08:00
|
|
|
|
"indent": indentFor(token),
|
|
|
|
|
"parentIndent": (current && current.indent) || 0,
|
2018-03-01 22:37:37 -08:00
|
|
|
|
"items": [],
|
2020-03-21 13:17:34 -07:00
|
|
|
|
"nesting": nesting,
|
2018-03-01 22:37:37 -08:00
|
|
|
|
"lastLineIndex": -1,
|
|
|
|
|
"insert": flattenedLists.length
|
|
|
|
|
};
|
2020-03-21 13:17:34 -07:00
|
|
|
|
nesting++;
|
2018-03-01 22:37:37 -08:00
|
|
|
|
} else if ((token.type === "bullet_list_close") ||
|
|
|
|
|
(token.type === "ordered_list_close")) {
|
|
|
|
|
// Finalize current context and restore previous
|
|
|
|
|
current.lastLineIndex = lastWithMap.map[1];
|
|
|
|
|
flattenedLists.splice(current.insert, 0, current);
|
|
|
|
|
delete current.insert;
|
|
|
|
|
current = stack.pop();
|
2020-03-21 13:17:34 -07:00
|
|
|
|
nesting--;
|
2018-03-01 22:37:37 -08:00
|
|
|
|
} else if (token.type === "list_item_open") {
|
|
|
|
|
// Add list item
|
|
|
|
|
current.items.push(token);
|
2020-03-21 13:17:34 -07:00
|
|
|
|
} else if (token.type === "blockquote_open") {
|
|
|
|
|
nestingStack.push(nesting);
|
|
|
|
|
nesting = 0;
|
|
|
|
|
} else if (token.type === "blockquote_close") {
|
2022-06-12 17:51:47 -07:00
|
|
|
|
nesting = nestingStack.pop() || 0;
|
2022-06-07 22:16:34 -07:00
|
|
|
|
}
|
|
|
|
|
if (token.map) {
|
2018-03-01 22:37:37 -08:00
|
|
|
|
// Track last token with map
|
|
|
|
|
lastWithMap = token;
|
|
|
|
|
}
|
2022-06-08 22:10:27 -07:00
|
|
|
|
}
|
2019-04-10 21:26:59 -07:00
|
|
|
|
return flattenedLists;
|
2018-01-18 21:27:07 -08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Calls the provided function for each specified inline child token
|
|
|
|
|
module.exports.forEachInlineChild =
|
2019-03-30 14:36:04 -07:00
|
|
|
|
function forEachInlineChild(params, type, handler) {
|
2018-01-18 21:27:07 -08:00
|
|
|
|
filterTokens(params, "inline", function forToken(token) {
|
2022-06-08 22:10:27 -07:00
|
|
|
|
for (const child of token.children) {
|
2018-01-18 21:27:07 -08:00
|
|
|
|
if (child.type === type) {
|
2019-03-30 14:36:04 -07:00
|
|
|
|
handler(child, token);
|
2018-01-18 21:27:07 -08:00
|
|
|
|
}
|
2022-06-08 22:10:27 -07:00
|
|
|
|
}
|
2018-01-18 21:27:07 -08:00
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Calls the provided function for each heading's content
|
2019-03-30 14:36:04 -07:00
|
|
|
|
module.exports.forEachHeading = function forEachHeading(params, handler) {
|
2018-04-27 22:05:34 -07:00
|
|
|
|
let heading = null;
|
2022-06-08 22:10:27 -07:00
|
|
|
|
for (const token of params.tokens) {
|
2018-01-18 21:27:07 -08:00
|
|
|
|
if (token.type === "heading_open") {
|
|
|
|
|
heading = token;
|
|
|
|
|
} else if (token.type === "heading_close") {
|
|
|
|
|
heading = null;
|
|
|
|
|
} else if ((token.type === "inline") && heading) {
|
2022-04-10 05:37:57 +00:00
|
|
|
|
handler(heading, token.content, token);
|
2018-01-18 21:27:07 -08:00
|
|
|
|
}
|
2022-06-08 22:10:27 -07:00
|
|
|
|
}
|
2018-01-18 21:27:07 -08:00
|
|
|
|
};
|
|
|
|
|
|
2020-04-25 15:10:07 -07:00
|
|
|
|
/**
|
|
|
|
|
* Calls the provided function for each inline code span's content.
|
|
|
|
|
*
|
|
|
|
|
* @param {string} input Markdown content.
|
2021-11-23 04:40:05 +00:00
|
|
|
|
* @param {Function} handler Callback function taking (code, lineIndex,
|
|
|
|
|
* columnIndex, ticks).
|
2020-04-25 15:10:07 -07:00
|
|
|
|
* @returns {void}
|
|
|
|
|
*/
|
|
|
|
|
function forEachInlineCodeSpan(input, handler) {
|
2022-06-19 02:14:03 +00:00
|
|
|
|
const backtickRe = /`+/g;
|
|
|
|
|
let match = null;
|
|
|
|
|
const backticksLengthAndIndex = [];
|
|
|
|
|
while ((match = backtickRe.exec(input)) !== null) {
|
|
|
|
|
backticksLengthAndIndex.push([ match[0].length, match.index ]);
|
|
|
|
|
}
|
|
|
|
|
const newLinesIndex = [];
|
|
|
|
|
while ((match = newLineRe.exec(input)) !== null) {
|
|
|
|
|
newLinesIndex.push(match.index);
|
|
|
|
|
}
|
|
|
|
|
let lineIndex = 0;
|
|
|
|
|
let lineStartIndex = 0;
|
|
|
|
|
let k = 0;
|
|
|
|
|
for (let i = 0; i < backticksLengthAndIndex.length - 1; i++) {
|
|
|
|
|
const [ startLength, startIndex ] = backticksLengthAndIndex[i];
|
|
|
|
|
if ((startIndex === 0) || (input[startIndex - 1] !== "\\")) {
|
|
|
|
|
for (let j = i + 1; j < backticksLengthAndIndex.length; j++) {
|
|
|
|
|
const [ endLength, endIndex ] = backticksLengthAndIndex[j];
|
|
|
|
|
if (startLength === endLength) {
|
|
|
|
|
for (; k < newLinesIndex.length; k++) {
|
|
|
|
|
const newLineIndex = newLinesIndex[k];
|
|
|
|
|
if (startIndex < newLineIndex) {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
lineIndex++;
|
|
|
|
|
lineStartIndex = newLineIndex + 1;
|
|
|
|
|
}
|
|
|
|
|
const columnIndex = startIndex - lineStartIndex + startLength;
|
2020-04-25 15:10:07 -07:00
|
|
|
|
handler(
|
2022-06-19 02:14:03 +00:00
|
|
|
|
input.slice(startIndex + startLength, endIndex),
|
|
|
|
|
lineIndex,
|
|
|
|
|
columnIndex,
|
|
|
|
|
startLength
|
|
|
|
|
);
|
|
|
|
|
i = j;
|
|
|
|
|
break;
|
2019-01-30 22:09:20 -08:00
|
|
|
|
}
|
|
|
|
|
}
|
2020-04-25 15:10:07 -07:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
module.exports.forEachInlineCodeSpan = forEachInlineCodeSpan;
|
2019-01-30 22:09:20 -08:00
|
|
|
|
|
2022-06-01 20:23:08 -07:00
|
|
|
|
/**
|
|
|
|
|
* Adds ellipsis to the left/right/middle of the specified text.
|
|
|
|
|
*
|
|
|
|
|
* @param {string} text Text to ellipsify.
|
|
|
|
|
* @param {boolean} [start] True iff the start of the text is important.
|
|
|
|
|
* @param {boolean} [end] True iff the end of the text is important.
|
|
|
|
|
* @returns {string} Ellipsified text.
|
|
|
|
|
*/
|
|
|
|
|
function ellipsify(text, start, end) {
|
|
|
|
|
if (text.length <= 30) {
|
|
|
|
|
// Nothing to do
|
|
|
|
|
} else if (start && end) {
|
|
|
|
|
text = text.slice(0, 15) + "..." + text.slice(-15);
|
|
|
|
|
} else if (end) {
|
|
|
|
|
text = "..." + text.slice(-30);
|
|
|
|
|
} else {
|
|
|
|
|
text = text.slice(0, 30) + "...";
|
|
|
|
|
}
|
|
|
|
|
return text;
|
|
|
|
|
}
|
|
|
|
|
module.exports.ellipsify = ellipsify;
|
|
|
|
|
|
2020-01-23 19:42:46 -08:00
|
|
|
|
/**
|
|
|
|
|
* Adds a generic error object via the onError callback.
|
|
|
|
|
*
|
|
|
|
|
* @param {Object} onError RuleOnError instance.
|
|
|
|
|
* @param {number} lineNumber Line number.
|
|
|
|
|
* @param {string} [detail] Error details.
|
|
|
|
|
* @param {string} [context] Error context.
|
|
|
|
|
* @param {number[]} [range] Column and length of error.
|
|
|
|
|
* @param {Object} [fixInfo] RuleOnErrorFixInfo instance.
|
|
|
|
|
* @returns {void}
|
|
|
|
|
*/
|
2019-08-16 19:56:52 -07:00
|
|
|
|
function addError(onError, lineNumber, detail, context, range, fixInfo) {
|
2018-01-18 21:27:07 -08:00
|
|
|
|
onError({
|
2019-08-16 19:56:52 -07:00
|
|
|
|
lineNumber,
|
|
|
|
|
detail,
|
|
|
|
|
context,
|
|
|
|
|
range,
|
|
|
|
|
fixInfo
|
2018-01-18 21:27:07 -08:00
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
module.exports.addError = addError;
|
|
|
|
|
|
|
|
|
|
// Adds an error object with details conditionally via the onError callback
|
|
|
|
|
module.exports.addErrorDetailIf = function addErrorDetailIf(
|
2019-08-24 22:55:51 -07:00
|
|
|
|
onError, lineNumber, expected, actual, detail, context, range, fixInfo) {
|
2018-01-18 21:27:07 -08:00
|
|
|
|
if (expected !== actual) {
|
|
|
|
|
addError(
|
|
|
|
|
onError,
|
|
|
|
|
lineNumber,
|
|
|
|
|
"Expected: " + expected + "; Actual: " + actual +
|
|
|
|
|
(detail ? "; " + detail : ""),
|
2019-03-24 21:50:56 -07:00
|
|
|
|
context,
|
2019-08-24 22:55:51 -07:00
|
|
|
|
range,
|
|
|
|
|
fixInfo);
|
2018-01-18 21:27:07 -08:00
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Adds an error object with context via the onError callback
|
2019-08-28 21:47:07 -07:00
|
|
|
|
module.exports.addErrorContext = function addErrorContext(
|
|
|
|
|
onError, lineNumber, context, left, right, range, fixInfo) {
|
2022-06-01 20:23:08 -07:00
|
|
|
|
context = ellipsify(context, left, right);
|
2022-06-12 17:51:47 -07:00
|
|
|
|
addError(onError, lineNumber, undefined, context, range, fixInfo);
|
2019-08-28 21:47:07 -07:00
|
|
|
|
};
|
2018-01-18 21:27:07 -08:00
|
|
|
|
|
2021-06-17 21:50:03 -07:00
|
|
|
|
/**
|
2021-11-23 04:40:05 +00:00
|
|
|
|
* Returns an array of code block and span content ranges.
|
2021-06-17 21:50:03 -07:00
|
|
|
|
*
|
2021-11-23 04:40:05 +00:00
|
|
|
|
* @param {Object} params RuleParams instance.
|
|
|
|
|
* @param {Object} lineMetadata Line metadata object.
|
|
|
|
|
* @returns {number[][]} Array of ranges (lineIndex, columnIndex, length).
|
2021-06-17 21:50:03 -07:00
|
|
|
|
*/
|
2021-11-23 04:40:05 +00:00
|
|
|
|
module.exports.codeBlockAndSpanRanges = (params, lineMetadata) => {
|
2021-06-17 21:50:03 -07:00
|
|
|
|
const exclusions = [];
|
2021-11-23 04:40:05 +00:00
|
|
|
|
// Add code block ranges (excludes fences)
|
|
|
|
|
forEachLine(lineMetadata, (line, lineIndex, inCode, onFence) => {
|
|
|
|
|
if (inCode && !onFence) {
|
2021-12-17 17:24:00 -08:00
|
|
|
|
exclusions.push([ lineIndex, 0, line.length ]);
|
2021-06-17 21:50:03 -07:00
|
|
|
|
}
|
2021-11-23 04:40:05 +00:00
|
|
|
|
});
|
|
|
|
|
// Add code span ranges (excludes ticks)
|
|
|
|
|
filterTokens(params, "inline", (token) => {
|
|
|
|
|
if (token.children.some((child) => child.type === "code_inline")) {
|
|
|
|
|
const tokenLines = params.lines.slice(token.map[0], token.map[1]);
|
|
|
|
|
forEachInlineCodeSpan(
|
|
|
|
|
tokenLines.join("\n"),
|
|
|
|
|
(code, lineIndex, columnIndex) => {
|
|
|
|
|
const codeLines = code.split(newLineRe);
|
2022-06-02 21:42:48 -07:00
|
|
|
|
for (const [ i, line ] of codeLines.entries()) {
|
2021-11-23 04:40:05 +00:00
|
|
|
|
exclusions.push([
|
|
|
|
|
token.lineNumber - 1 + lineIndex + i,
|
|
|
|
|
i ? 0 : columnIndex,
|
|
|
|
|
line.length
|
|
|
|
|
]);
|
2022-06-02 21:42:48 -07:00
|
|
|
|
}
|
2021-11-23 04:40:05 +00:00
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
});
|
2021-06-17 21:50:03 -07:00
|
|
|
|
return exclusions;
|
|
|
|
|
};
|
|
|
|
|
|
2022-04-25 21:50:33 -07:00
|
|
|
|
/**
|
|
|
|
|
* Returns an array of HTML element ranges.
|
|
|
|
|
*
|
|
|
|
|
* @param {Object} params RuleParams instance.
|
|
|
|
|
* @param {Object} lineMetadata Line metadata object.
|
|
|
|
|
* @returns {number[][]} Array of ranges (lineIndex, columnIndex, length).
|
|
|
|
|
*/
|
|
|
|
|
module.exports.htmlElementRanges = (params, lineMetadata) => {
|
|
|
|
|
const exclusions = [];
|
|
|
|
|
forEachLine(lineMetadata, (line, lineIndex, inCode) => {
|
|
|
|
|
let match = null;
|
|
|
|
|
// eslint-disable-next-line no-unmodified-loop-condition
|
|
|
|
|
while (!inCode && ((match = htmlElementRe.exec(line)) !== null)) {
|
|
|
|
|
exclusions.push([ lineIndex, match.index, match[0].length ]);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
return exclusions;
|
|
|
|
|
};
|
|
|
|
|
|
2022-06-01 20:23:08 -07:00
|
|
|
|
/**
|
|
|
|
|
* Determines whether the specified range is within another range.
|
|
|
|
|
*
|
|
|
|
|
* @param {number[][]} ranges Array of ranges (line, index, length).
|
|
|
|
|
* @param {number} lineIndex Line index to check.
|
|
|
|
|
* @param {number} index Index to check.
|
|
|
|
|
* @param {number} length Length to check.
|
|
|
|
|
* @returns {boolean} True iff the specified range is within.
|
|
|
|
|
*/
|
|
|
|
|
const withinAnyRange = (ranges, lineIndex, index, length) => (
|
|
|
|
|
!ranges.every((span) => (
|
|
|
|
|
(lineIndex !== span[0]) ||
|
|
|
|
|
(index < span[1]) ||
|
|
|
|
|
(index + length > span[1] + span[2])
|
|
|
|
|
))
|
|
|
|
|
);
|
2022-06-02 22:17:32 -07:00
|
|
|
|
module.exports.withinAnyRange = withinAnyRange;
|
2022-06-01 20:23:08 -07:00
|
|
|
|
|
2018-01-18 21:27:07 -08:00
|
|
|
|
// Returns a range object for a line by applying a RegExp
|
|
|
|
|
module.exports.rangeFromRegExp = function rangeFromRegExp(line, regexp) {
|
2018-04-27 22:05:34 -07:00
|
|
|
|
let range = null;
|
|
|
|
|
const match = line.match(regexp);
|
2018-01-18 21:27:07 -08:00
|
|
|
|
if (match) {
|
2019-12-12 21:22:45 -08:00
|
|
|
|
const column = match.index + 1;
|
|
|
|
|
const length = match[0].length;
|
2018-01-18 21:27:07 -08:00
|
|
|
|
range = [ column, length ];
|
|
|
|
|
}
|
|
|
|
|
return range;
|
|
|
|
|
};
|
2019-03-16 20:21:57 -07:00
|
|
|
|
|
|
|
|
|
// Determines if the front matter includes a title
|
|
|
|
|
module.exports.frontMatterHasTitle =
|
|
|
|
|
function frontMatterHasTitle(frontMatterLines, frontMatterTitlePattern) {
|
|
|
|
|
const ignoreFrontMatter =
|
|
|
|
|
(frontMatterTitlePattern !== undefined) && !frontMatterTitlePattern;
|
|
|
|
|
const frontMatterTitleRe =
|
2020-11-14 19:40:15 -08:00
|
|
|
|
new RegExp(
|
|
|
|
|
String(frontMatterTitlePattern || "^\\s*\"?title\"?\\s*[:=]"),
|
|
|
|
|
"i"
|
|
|
|
|
);
|
2019-03-16 20:21:57 -07:00
|
|
|
|
return !ignoreFrontMatter &&
|
|
|
|
|
frontMatterLines.some((line) => frontMatterTitleRe.test(line));
|
|
|
|
|
};
|
2019-08-16 19:56:52 -07:00
|
|
|
|
|
2022-02-18 21:14:14 -08:00
|
|
|
|
/**
|
|
|
|
|
* Calls the provided function for each link.
|
|
|
|
|
*
|
|
|
|
|
* @param {string} line Line of Markdown input.
|
|
|
|
|
* @param {Function} handler Function taking (index, link, text, destination).
|
|
|
|
|
* @returns {void}
|
|
|
|
|
*/
|
|
|
|
|
function forEachLink(line, handler) {
|
|
|
|
|
// Helper to find matching close symbol for link text/destination
|
|
|
|
|
const findClosingSymbol = (index) => {
|
|
|
|
|
const begin = line[index];
|
|
|
|
|
const end = (begin === "[") ? "]" : ")";
|
|
|
|
|
let nesting = 0;
|
|
|
|
|
let escaping = false;
|
|
|
|
|
let pointy = false;
|
|
|
|
|
for (let i = index + 1; i < line.length; i++) {
|
|
|
|
|
const current = line[i];
|
|
|
|
|
if (current === "\\") {
|
|
|
|
|
escaping = !escaping;
|
|
|
|
|
} else if (!escaping && (current === begin)) {
|
|
|
|
|
nesting++;
|
|
|
|
|
} else if (!escaping && (current === end)) {
|
|
|
|
|
if (nesting > 0) {
|
|
|
|
|
nesting--;
|
|
|
|
|
} else if (!pointy) {
|
|
|
|
|
// Return index after matching close symbol
|
|
|
|
|
return i + 1;
|
|
|
|
|
}
|
|
|
|
|
} else if ((i === index + 1) && (begin === "(") && (current === "<")) {
|
|
|
|
|
pointy = true;
|
|
|
|
|
} else if (!escaping && pointy && current === ">") {
|
|
|
|
|
pointy = false;
|
|
|
|
|
nesting = 0;
|
|
|
|
|
} else {
|
|
|
|
|
escaping = false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// No match found
|
|
|
|
|
return -1;
|
|
|
|
|
};
|
|
|
|
|
// Scan line for unescaped "[" character
|
|
|
|
|
let escaping = false;
|
|
|
|
|
for (let i = 0; i < line.length; i++) {
|
|
|
|
|
const current = line[i];
|
|
|
|
|
if (current === "\\") {
|
|
|
|
|
escaping = !escaping;
|
|
|
|
|
} else if (!escaping && (current === "[")) {
|
|
|
|
|
// Scan for matching close "]" of link text
|
|
|
|
|
const textEnd = findClosingSymbol(i);
|
|
|
|
|
if (textEnd !== -1) {
|
|
|
|
|
if ((line[textEnd] === "(") || (line[textEnd] === "[")) {
|
|
|
|
|
// Scan for matching close ")" or "]" of link destination
|
|
|
|
|
const destEnd = findClosingSymbol(textEnd);
|
|
|
|
|
if (destEnd !== -1) {
|
|
|
|
|
// Call handler with link text and destination
|
|
|
|
|
const link = line.slice(i, destEnd);
|
|
|
|
|
const text = line.slice(i, textEnd);
|
|
|
|
|
const dest = line.slice(textEnd, destEnd);
|
|
|
|
|
handler(i, link, text, dest);
|
|
|
|
|
i = destEnd;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (i < textEnd) {
|
|
|
|
|
// Call handler with link text only
|
|
|
|
|
const text = line.slice(i, textEnd);
|
|
|
|
|
handler(i, text, text);
|
|
|
|
|
i = textEnd;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
escaping = false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
module.exports.forEachLink = forEachLink;
|
|
|
|
|
|
2020-04-25 15:10:07 -07:00
|
|
|
|
/**
|
2020-05-08 16:01:42 -07:00
|
|
|
|
* Returns a list of emphasis markers in code spans and links.
|
2020-04-25 15:10:07 -07:00
|
|
|
|
*
|
|
|
|
|
* @param {Object} params RuleParams instance.
|
|
|
|
|
* @returns {number[][]} List of markers.
|
|
|
|
|
*/
|
2020-05-08 16:01:42 -07:00
|
|
|
|
function emphasisMarkersInContent(params) {
|
2020-04-25 15:10:07 -07:00
|
|
|
|
const { lines } = params;
|
|
|
|
|
const byLine = new Array(lines.length);
|
2021-11-26 05:37:04 +00:00
|
|
|
|
// Search links
|
2022-06-08 22:10:27 -07:00
|
|
|
|
for (const [ tokenLineIndex, tokenLine ] of lines.entries()) {
|
2021-11-26 05:37:04 +00:00
|
|
|
|
const inLine = [];
|
2022-02-18 21:14:14 -08:00
|
|
|
|
forEachLink(tokenLine, (index, match) => {
|
2021-11-26 05:37:04 +00:00
|
|
|
|
let markerMatch = null;
|
2022-02-18 21:14:14 -08:00
|
|
|
|
while ((markerMatch = emphasisMarkersRe.exec(match))) {
|
|
|
|
|
inLine.push(index + markerMatch.index);
|
2021-11-26 05:37:04 +00:00
|
|
|
|
}
|
2022-02-18 21:14:14 -08:00
|
|
|
|
});
|
2021-11-26 05:37:04 +00:00
|
|
|
|
byLine[tokenLineIndex] = inLine;
|
2022-06-08 22:10:27 -07:00
|
|
|
|
}
|
2020-05-08 16:01:42 -07:00
|
|
|
|
// Search code spans
|
2020-04-25 15:10:07 -07:00
|
|
|
|
filterTokens(params, "inline", (token) => {
|
|
|
|
|
const { children, lineNumber, map } = token;
|
|
|
|
|
if (children.some((child) => child.type === "code_inline")) {
|
|
|
|
|
const tokenLines = lines.slice(map[0], map[1]);
|
|
|
|
|
forEachInlineCodeSpan(
|
|
|
|
|
tokenLines.join("\n"),
|
|
|
|
|
(code, lineIndex, column, tickCount) => {
|
|
|
|
|
const codeLines = code.split(newLineRe);
|
2022-06-08 22:10:27 -07:00
|
|
|
|
for (const [ codeLineIndex, codeLine ] of codeLines.entries()) {
|
2021-11-26 05:37:04 +00:00
|
|
|
|
const byLineIndex = lineNumber - 1 + lineIndex + codeLineIndex;
|
|
|
|
|
const inLine = byLine[byLineIndex];
|
|
|
|
|
const codeLineOffset = codeLineIndex ? 0 : column - 1 + tickCount;
|
2020-04-25 15:10:07 -07:00
|
|
|
|
let match = null;
|
|
|
|
|
while ((match = emphasisMarkersRe.exec(codeLine))) {
|
|
|
|
|
inLine.push(codeLineOffset + match.index);
|
|
|
|
|
}
|
2021-11-26 05:37:04 +00:00
|
|
|
|
byLine[byLineIndex] = inLine;
|
2022-06-08 22:10:27 -07:00
|
|
|
|
}
|
2020-04-25 15:10:07 -07:00
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
return byLine;
|
|
|
|
|
}
|
2020-05-08 16:01:42 -07:00
|
|
|
|
module.exports.emphasisMarkersInContent = emphasisMarkersInContent;
|
2020-04-25 15:10:07 -07:00
|
|
|
|
|
2022-06-01 20:23:08 -07:00
|
|
|
|
/**
|
|
|
|
|
* Returns an object with information about reference links and images.
|
|
|
|
|
*
|
|
|
|
|
* @param {Object} lineMetadata Line metadata object.
|
|
|
|
|
* @returns {Object} Reference link/image data.
|
|
|
|
|
*/
|
2022-06-12 18:39:28 -07:00
|
|
|
|
function getReferenceLinkImageData(lineMetadata) {
|
2022-06-01 20:23:08 -07:00
|
|
|
|
// Initialize return values
|
|
|
|
|
const references = new Map();
|
|
|
|
|
const shortcuts = new Set();
|
|
|
|
|
const definitions = new Map();
|
|
|
|
|
const duplicateDefinitions = [];
|
|
|
|
|
// Define helper functions
|
|
|
|
|
const normalizeLabel = (s) => s.toLowerCase().trim().replace(/\s+/g, " ");
|
|
|
|
|
const exclusions = [];
|
|
|
|
|
const excluded = (match) => withinAnyRange(
|
|
|
|
|
exclusions, 0, match.index, match[0].length
|
|
|
|
|
);
|
|
|
|
|
// Convert input to single-line so multi-line links/images are easier
|
|
|
|
|
const lineOffsets = [];
|
|
|
|
|
let currentOffset = 0;
|
|
|
|
|
const contentLines = [];
|
|
|
|
|
forEachLine(lineMetadata, (line, lineIndex, inCode) => {
|
|
|
|
|
lineOffsets[lineIndex] = currentOffset;
|
|
|
|
|
if (!inCode) {
|
|
|
|
|
if (line.trim().length === 0) {
|
2022-06-12 18:00:37 -07:00
|
|
|
|
// Allow RegExp to detect the end of a block
|
|
|
|
|
line = "\0";
|
2022-06-01 20:23:08 -07:00
|
|
|
|
}
|
|
|
|
|
contentLines.push(line);
|
|
|
|
|
currentOffset += line.length + 1;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
lineOffsets.push(currentOffset);
|
|
|
|
|
const contentLine = contentLines.join(" ");
|
|
|
|
|
// Determine single-line exclusions for inline code spans
|
|
|
|
|
forEachInlineCodeSpan(contentLine, (code, lineIndex, columnIndex) => {
|
|
|
|
|
exclusions.push([ 0, columnIndex, code.length ]);
|
|
|
|
|
});
|
|
|
|
|
// Identify all link/image reference definitions
|
|
|
|
|
forEachLine(lineMetadata, (line, lineIndex, inCode) => {
|
|
|
|
|
if (!inCode) {
|
|
|
|
|
const linkReferenceDefinitionMatch = linkReferenceDefinitionRe.exec(line);
|
|
|
|
|
if (linkReferenceDefinitionMatch) {
|
|
|
|
|
const label = normalizeLabel(linkReferenceDefinitionMatch[1]);
|
|
|
|
|
if (definitions.has(label)) {
|
|
|
|
|
duplicateDefinitions.push([ label, lineIndex ]);
|
|
|
|
|
} else {
|
|
|
|
|
definitions.set(label, lineIndex);
|
|
|
|
|
}
|
|
|
|
|
exclusions.push([ 0, lineOffsets[lineIndex], line.length ]);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
// Identify all link and image references
|
|
|
|
|
let lineIndex = 0;
|
|
|
|
|
const pendingContents = [
|
|
|
|
|
{
|
|
|
|
|
"content": contentLine,
|
|
|
|
|
"contentLineIndex": 0,
|
|
|
|
|
"contentIndex": 0,
|
|
|
|
|
"topLevel": true
|
|
|
|
|
}
|
|
|
|
|
];
|
|
|
|
|
let pendingContent = null;
|
|
|
|
|
while ((pendingContent = pendingContents.shift())) {
|
|
|
|
|
const { content, contentLineIndex, contentIndex, topLevel } =
|
|
|
|
|
pendingContent;
|
|
|
|
|
let referenceLinkMatch = null;
|
|
|
|
|
while ((referenceLinkMatch = referenceLinkRe.exec(content)) !== null) {
|
|
|
|
|
const [ matchString, matchText, matchLabel ] = referenceLinkMatch;
|
|
|
|
|
if (
|
|
|
|
|
!matchString.startsWith("\\") &&
|
|
|
|
|
!matchString.startsWith("!\\") &&
|
|
|
|
|
!matchText.endsWith("\\") &&
|
|
|
|
|
!(matchLabel || "").endsWith("\\") &&
|
|
|
|
|
(topLevel || matchString.startsWith("!")) &&
|
|
|
|
|
!excluded(referenceLinkMatch)
|
|
|
|
|
) {
|
|
|
|
|
const shortcutLink = (matchLabel === undefined);
|
|
|
|
|
const collapsedLink =
|
|
|
|
|
(!shortcutLink && (matchLabel.length === 0));
|
|
|
|
|
const label = normalizeLabel(
|
|
|
|
|
(shortcutLink || collapsedLink) ? matchText : matchLabel
|
|
|
|
|
);
|
|
|
|
|
if (label.length > 0) {
|
|
|
|
|
if (shortcutLink) {
|
|
|
|
|
// Track, but don't validate due to ambiguity: "text [text] text"
|
|
|
|
|
shortcuts.add(label);
|
|
|
|
|
} else {
|
|
|
|
|
const referenceindex = referenceLinkMatch.index;
|
|
|
|
|
if (topLevel) {
|
|
|
|
|
// Calculate line index
|
|
|
|
|
while (lineOffsets[lineIndex + 1] <= referenceindex) {
|
|
|
|
|
lineIndex++;
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// Use provided line index
|
|
|
|
|
lineIndex = contentLineIndex;
|
|
|
|
|
}
|
|
|
|
|
const referenceIndex = referenceindex +
|
|
|
|
|
(topLevel ? -lineOffsets[lineIndex] : contentIndex);
|
|
|
|
|
// Track reference and location
|
|
|
|
|
const referenceData = references.get(label) || [];
|
|
|
|
|
referenceData.push([
|
|
|
|
|
lineIndex,
|
|
|
|
|
referenceIndex,
|
|
|
|
|
matchString.length
|
|
|
|
|
]);
|
|
|
|
|
references.set(label, referenceData);
|
|
|
|
|
// Check for images embedded in top-level link text
|
|
|
|
|
if (!matchString.startsWith("!")) {
|
|
|
|
|
pendingContents.push(
|
|
|
|
|
{
|
|
|
|
|
"content": matchText,
|
|
|
|
|
"contentLineIndex": lineIndex,
|
|
|
|
|
"contentIndex": referenceIndex + 1,
|
|
|
|
|
"topLevel": false
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return {
|
|
|
|
|
references,
|
|
|
|
|
shortcuts,
|
|
|
|
|
definitions,
|
|
|
|
|
duplicateDefinitions
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
module.exports.getReferenceLinkImageData = getReferenceLinkImageData;
|
|
|
|
|
|
2020-01-23 19:42:46 -08:00
|
|
|
|
/**
|
|
|
|
|
* Gets the most common line ending, falling back to the platform default.
|
|
|
|
|
*
|
|
|
|
|
* @param {string} input Markdown content to analyze.
|
2022-05-06 21:04:34 -07:00
|
|
|
|
* @param {Object} [os] Node.js "os" module.
|
2020-01-23 19:42:46 -08:00
|
|
|
|
* @returns {string} Preferred line ending.
|
|
|
|
|
*/
|
2022-05-06 21:04:34 -07:00
|
|
|
|
function getPreferredLineEnding(input, os) {
|
2019-09-14 22:31:08 -07:00
|
|
|
|
let cr = 0;
|
|
|
|
|
let lf = 0;
|
|
|
|
|
let crlf = 0;
|
|
|
|
|
const endings = input.match(newLineRe) || [];
|
2022-06-08 22:10:27 -07:00
|
|
|
|
for (const ending of endings) {
|
2019-09-14 22:31:08 -07:00
|
|
|
|
// eslint-disable-next-line default-case
|
|
|
|
|
switch (ending) {
|
|
|
|
|
case "\r":
|
|
|
|
|
cr++;
|
|
|
|
|
break;
|
|
|
|
|
case "\n":
|
|
|
|
|
lf++;
|
|
|
|
|
break;
|
|
|
|
|
case "\r\n":
|
|
|
|
|
crlf++;
|
|
|
|
|
break;
|
|
|
|
|
}
|
2022-06-08 22:10:27 -07:00
|
|
|
|
}
|
2019-09-14 22:31:08 -07:00
|
|
|
|
let preferredLineEnding = null;
|
|
|
|
|
if (!cr && !lf && !crlf) {
|
2022-05-06 21:04:34 -07:00
|
|
|
|
preferredLineEnding = (os && os.EOL) || "\n";
|
2019-09-14 22:31:08 -07:00
|
|
|
|
} else if ((lf >= crlf) && (lf >= cr)) {
|
|
|
|
|
preferredLineEnding = "\n";
|
|
|
|
|
} else if (crlf >= cr) {
|
|
|
|
|
preferredLineEnding = "\r\n";
|
|
|
|
|
} else {
|
|
|
|
|
preferredLineEnding = "\r";
|
|
|
|
|
}
|
|
|
|
|
return preferredLineEnding;
|
|
|
|
|
}
|
|
|
|
|
module.exports.getPreferredLineEnding = getPreferredLineEnding;
|
|
|
|
|
|
2020-01-23 19:42:46 -08:00
|
|
|
|
/**
|
|
|
|
|
* Normalizes the fields of a RuleOnErrorFixInfo instance.
|
|
|
|
|
*
|
|
|
|
|
* @param {Object} fixInfo RuleOnErrorFixInfo instance.
|
|
|
|
|
* @param {number} [lineNumber] Line number.
|
|
|
|
|
* @returns {Object} Normalized RuleOnErrorFixInfo instance.
|
|
|
|
|
*/
|
2019-09-12 21:50:40 -07:00
|
|
|
|
function normalizeFixInfo(fixInfo, lineNumber) {
|
|
|
|
|
return {
|
|
|
|
|
"lineNumber": fixInfo.lineNumber || lineNumber,
|
|
|
|
|
"editColumn": fixInfo.editColumn || 1,
|
|
|
|
|
"deleteCount": fixInfo.deleteCount || 0,
|
|
|
|
|
"insertText": fixInfo.insertText || ""
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2020-01-23 19:42:46 -08:00
|
|
|
|
/**
|
|
|
|
|
* Fixes the specified error on a line of Markdown content.
|
|
|
|
|
*
|
|
|
|
|
* @param {string} line Line of Markdown content.
|
|
|
|
|
* @param {Object} fixInfo RuleOnErrorFixInfo instance.
|
2022-02-09 22:44:49 -08:00
|
|
|
|
* @param {string} [lineEnding] Line ending to use.
|
2022-06-12 17:51:47 -07:00
|
|
|
|
* @returns {string | null} Fixed content.
|
2020-01-23 19:42:46 -08:00
|
|
|
|
*/
|
2019-09-14 22:31:08 -07:00
|
|
|
|
function applyFix(line, fixInfo, lineEnding) {
|
2019-09-12 21:50:40 -07:00
|
|
|
|
const { editColumn, deleteCount, insertText } = normalizeFixInfo(fixInfo);
|
|
|
|
|
const editIndex = editColumn - 1;
|
|
|
|
|
return (deleteCount === -1) ?
|
|
|
|
|
null :
|
|
|
|
|
line.slice(0, editIndex) +
|
2019-09-20 21:50:44 -07:00
|
|
|
|
insertText.replace(/\n/g, lineEnding || "\n") +
|
2019-09-12 21:50:40 -07:00
|
|
|
|
line.slice(editIndex + deleteCount);
|
|
|
|
|
}
|
|
|
|
|
module.exports.applyFix = applyFix;
|
|
|
|
|
|
2022-02-09 22:44:49 -08:00
|
|
|
|
/**
|
|
|
|
|
* Applies as many fixes as possible to Markdown content.
|
|
|
|
|
*
|
|
|
|
|
* @param {string} input Lines of Markdown content.
|
|
|
|
|
* @param {Object[]} errors RuleOnErrorInfo instances.
|
|
|
|
|
* @returns {string} Corrected content.
|
|
|
|
|
*/
|
2022-05-05 23:14:18 -07:00
|
|
|
|
function applyFixes(input, errors) {
|
2022-05-06 21:04:34 -07:00
|
|
|
|
const lineEnding = getPreferredLineEnding(input, require("os"));
|
2019-08-16 19:56:52 -07:00
|
|
|
|
const lines = input.split(newLineRe);
|
2019-08-28 21:47:07 -07:00
|
|
|
|
// Normalize fixInfo objects
|
2019-09-12 21:50:40 -07:00
|
|
|
|
let fixInfos = errors
|
|
|
|
|
.filter((error) => error.fixInfo)
|
|
|
|
|
.map((error) => normalizeFixInfo(error.fixInfo, error.lineNumber));
|
2019-09-02 15:35:43 -07:00
|
|
|
|
// Sort bottom-to-top, line-deletes last, right-to-left, long-to-short
|
|
|
|
|
fixInfos.sort((a, b) => {
|
|
|
|
|
const aDeletingLine = (a.deleteCount === -1);
|
|
|
|
|
const bDeletingLine = (b.deleteCount === -1);
|
|
|
|
|
return (
|
|
|
|
|
(b.lineNumber - a.lineNumber) ||
|
|
|
|
|
(aDeletingLine ? 1 : (bDeletingLine ? -1 : 0)) ||
|
|
|
|
|
(b.editColumn - a.editColumn) ||
|
|
|
|
|
(b.insertText.length - a.insertText.length)
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
// Remove duplicate entries (needed for following collapse step)
|
|
|
|
|
let lastFixInfo = {};
|
|
|
|
|
fixInfos = fixInfos.filter((fixInfo) => {
|
|
|
|
|
const unique = (
|
|
|
|
|
(fixInfo.lineNumber !== lastFixInfo.lineNumber) ||
|
|
|
|
|
(fixInfo.editColumn !== lastFixInfo.editColumn) ||
|
|
|
|
|
(fixInfo.deleteCount !== lastFixInfo.deleteCount) ||
|
|
|
|
|
(fixInfo.insertText !== lastFixInfo.insertText)
|
|
|
|
|
);
|
|
|
|
|
lastFixInfo = fixInfo;
|
|
|
|
|
return unique;
|
|
|
|
|
});
|
|
|
|
|
// Collapse insert/no-delete and no-insert/delete for same line/column
|
2022-06-08 22:10:27 -07:00
|
|
|
|
lastFixInfo = {
|
|
|
|
|
"lineNumber": -1
|
|
|
|
|
};
|
|
|
|
|
for (const fixInfo of fixInfos) {
|
2019-09-02 15:35:43 -07:00
|
|
|
|
if (
|
|
|
|
|
(fixInfo.lineNumber === lastFixInfo.lineNumber) &&
|
|
|
|
|
(fixInfo.editColumn === lastFixInfo.editColumn) &&
|
|
|
|
|
!fixInfo.insertText &&
|
|
|
|
|
(fixInfo.deleteCount > 0) &&
|
|
|
|
|
lastFixInfo.insertText &&
|
|
|
|
|
!lastFixInfo.deleteCount) {
|
|
|
|
|
fixInfo.insertText = lastFixInfo.insertText;
|
|
|
|
|
lastFixInfo.lineNumber = 0;
|
|
|
|
|
}
|
|
|
|
|
lastFixInfo = fixInfo;
|
2022-06-08 22:10:27 -07:00
|
|
|
|
}
|
2019-09-02 15:35:43 -07:00
|
|
|
|
fixInfos = fixInfos.filter((fixInfo) => fixInfo.lineNumber);
|
|
|
|
|
// Apply all (remaining/updated) fixes
|
2019-08-28 21:47:07 -07:00
|
|
|
|
let lastLineIndex = -1;
|
|
|
|
|
let lastEditIndex = -1;
|
2022-06-08 22:10:27 -07:00
|
|
|
|
for (const fixInfo of fixInfos) {
|
2019-09-12 21:50:40 -07:00
|
|
|
|
const { lineNumber, editColumn, deleteCount } = fixInfo;
|
2019-08-16 19:56:52 -07:00
|
|
|
|
const lineIndex = lineNumber - 1;
|
|
|
|
|
const editIndex = editColumn - 1;
|
2019-08-28 21:47:07 -07:00
|
|
|
|
if (
|
|
|
|
|
(lineIndex !== lastLineIndex) ||
|
2021-02-06 15:49:02 -08:00
|
|
|
|
(deleteCount === -1) ||
|
|
|
|
|
((editIndex + deleteCount) <=
|
|
|
|
|
(lastEditIndex - ((deleteCount > 0) ? 0 : 1)))
|
2019-08-28 21:47:07 -07:00
|
|
|
|
) {
|
2022-06-12 18:39:28 -07:00
|
|
|
|
// @ts-ignore
|
2019-09-14 22:31:08 -07:00
|
|
|
|
lines[lineIndex] = applyFix(lines[lineIndex], fixInfo, lineEnding);
|
2019-08-28 21:47:07 -07:00
|
|
|
|
}
|
|
|
|
|
lastLineIndex = lineIndex;
|
|
|
|
|
lastEditIndex = editIndex;
|
2022-06-08 22:10:27 -07:00
|
|
|
|
}
|
2019-08-28 21:47:07 -07:00
|
|
|
|
// Return corrected input
|
2019-09-14 22:31:08 -07:00
|
|
|
|
return lines.filter((line) => line !== null).join(lineEnding);
|
2022-02-09 22:44:49 -08:00
|
|
|
|
}
|
|
|
|
|
module.exports.applyFixes = applyFixes;
|
2021-11-28 23:18:57 -08:00
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Gets the range and fixInfo values for reporting an error if the expected
|
|
|
|
|
* text is found on the specified line.
|
|
|
|
|
*
|
|
|
|
|
* @param {string[]} lines Lines of Markdown content.
|
|
|
|
|
* @param {number} lineIndex Line index to check.
|
|
|
|
|
* @param {string} search Text to search for.
|
|
|
|
|
* @param {string} replace Text to replace with.
|
2022-05-03 21:35:31 -07:00
|
|
|
|
* @param {number} [instance] Instance on the line (1-based).
|
2021-11-28 23:18:57 -08:00
|
|
|
|
* @returns {Object} Range and fixInfo wrapper.
|
|
|
|
|
*/
|
2022-05-03 21:35:31 -07:00
|
|
|
|
module.exports.getRangeAndFixInfoIfFound =
|
|
|
|
|
(lines, lineIndex, search, replace, instance = 1) => {
|
|
|
|
|
let range = null;
|
|
|
|
|
let fixInfo = null;
|
|
|
|
|
let searchIndex = -1;
|
|
|
|
|
while (instance > 0) {
|
|
|
|
|
searchIndex = lines[lineIndex].indexOf(search, searchIndex + 1);
|
|
|
|
|
instance--;
|
|
|
|
|
}
|
|
|
|
|
if (searchIndex !== -1) {
|
|
|
|
|
const column = searchIndex + 1;
|
|
|
|
|
const length = search.length;
|
|
|
|
|
range = [ column, length ];
|
|
|
|
|
fixInfo = {
|
|
|
|
|
"editColumn": column,
|
|
|
|
|
"deleteCount": length,
|
|
|
|
|
"insertText": replace
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
return {
|
|
|
|
|
range,
|
|
|
|
|
fixInfo
|
2021-11-28 23:18:57 -08:00
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Gets the next (subsequent) child token if it is of the expected type.
|
|
|
|
|
*
|
|
|
|
|
* @param {Object} parentToken Parent token.
|
|
|
|
|
* @param {Object} childToken Child token basis.
|
|
|
|
|
* @param {string} nextType Token type of next token.
|
|
|
|
|
* @param {string} nextNextType Token type of next-next token.
|
|
|
|
|
* @returns {Object} Next token.
|
|
|
|
|
*/
|
|
|
|
|
function getNextChildToken(parentToken, childToken, nextType, nextNextType) {
|
|
|
|
|
const { children } = parentToken;
|
|
|
|
|
const index = children.indexOf(childToken);
|
|
|
|
|
if (
|
|
|
|
|
(index !== -1) &&
|
|
|
|
|
(children.length > index + 2) &&
|
|
|
|
|
(children[index + 1].type === nextType) &&
|
|
|
|
|
(children[index + 2].type === nextNextType)
|
|
|
|
|
) {
|
|
|
|
|
return children[index + 1];
|
|
|
|
|
}
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
module.exports.getNextChildToken = getNextChildToken;
|
2021-12-23 04:34:25 +00:00
|
|
|
|
|
2022-05-16 22:57:11 -07:00
|
|
|
|
/**
|
|
|
|
|
* Expands a path with a tilde to an absolute path.
|
|
|
|
|
*
|
|
|
|
|
* @param {string} file Path that may begin with a tilde.
|
|
|
|
|
* @param {Object} os Node.js "os" module.
|
|
|
|
|
* @returns {string} Absolute path (or original path).
|
|
|
|
|
*/
|
|
|
|
|
function expandTildePath(file, os) {
|
|
|
|
|
const homedir = os && os.homedir();
|
|
|
|
|
return homedir ? file.replace(/^~($|\/|\\)/, `${homedir}$1`) : file;
|
|
|
|
|
}
|
|
|
|
|
module.exports.expandTildePath = expandTildePath;
|