2017-12-15 22:55:51 -08:00
|
|
|
|
// @ts-check
|
|
|
|
|
|
2015-03-08 23:08:43 -07:00
|
|
|
|
"use strict";
|
|
|
|
|
|
2023-01-29 20:36:53 -08:00
|
|
|
|
const micromark = require("./micromark.cjs");
|
|
|
|
|
|
2023-10-18 23:20:19 -07:00
|
|
|
|
const { newLineRe, nextLinesRe } = require("./shared.js");
|
|
|
|
|
|
2019-08-16 19:56:52 -07:00
|
|
|
|
module.exports.newLineRe = newLineRe;
|
2023-10-18 23:20:19 -07:00
|
|
|
|
module.exports.nextLinesRe = nextLinesRe;
|
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 =
|
2023-10-16 20:06:30 -07:00
|
|
|
|
/((^---\s*$[\s\S]+?^---\s*)|(^\+\+\+\s*$[\s\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 =
|
2022-12-19 21:36:24 -08:00
|
|
|
|
/(<!--\s*markdownlint-(disable|enable|capture|restore|disable-file|enable-file|disable-line|disable-next-line|configure-file))(?:\s|-->)/gi;
|
2022-02-12 17:46:46 -08:00
|
|
|
|
module.exports.inlineCommentStartRe = inlineCommentStartRe;
|
2015-09-26 16:55:33 -07:00
|
|
|
|
|
2022-08-01 18:48:01 -07:00
|
|
|
|
// Regular expression for blockquote prefixes
|
|
|
|
|
const blockquotePrefixRe = /^[>\s]*/;
|
|
|
|
|
module.exports.blockquotePrefixRe = blockquotePrefixRe;
|
|
|
|
|
|
2022-06-01 20:23:08 -07:00
|
|
|
|
// Regular expression for link reference definitions
|
2022-12-19 21:36:24 -08:00
|
|
|
|
const linkReferenceDefinitionRe = /^ {0,3}\[([^\]]*[^\\])\]:/;
|
2022-06-01 20:23:08 -07:00
|
|
|
|
module.exports.linkReferenceDefinitionRe = linkReferenceDefinitionRe;
|
2020-05-08 16:01:42 -07:00
|
|
|
|
|
2023-06-24 15:45:51 -07:00
|
|
|
|
// Regular expression for identifying an HTML entity at the end of a line
|
|
|
|
|
module.exports.endOfLineHtmlEntityRe =
|
|
|
|
|
/&(?:#\d+|#[xX][\da-fA-F]+|[a-zA-Z]{2,31}|blk\d{2}|emsp1[34]|frac\d{2}|sup\d|there4);$/;
|
|
|
|
|
|
|
|
|
|
// Regular expression for identifying a GitHub emoji code at the end of a line
|
|
|
|
|
module.exports.endOfLineGemojiCodeRe =
|
|
|
|
|
/:(?:[abmovx]|[-+]1|100|1234|(?:1st|2nd|3rd)_place_medal|8ball|clock\d{1,4}|e-mail|non-potable_water|o2|t-rex|u5272|u5408|u55b6|u6307|u6708|u6709|u6e80|u7121|u7533|u7981|u7a7a|[a-z]{2,15}2?|[a-z]{1,14}(?:_[a-z\d]{1,16})+):$/;
|
|
|
|
|
|
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
|
|
|
|
|
2023-07-12 21:58:36 -07:00
|
|
|
|
/**
|
|
|
|
|
* Returns true iff the input is a Number.
|
|
|
|
|
*
|
|
|
|
|
* @param {Object} obj Object of unknown type.
|
|
|
|
|
* @returns {boolean} True iff obj is a Number.
|
|
|
|
|
*/
|
|
|
|
|
function isNumber(obj) {
|
2018-02-27 21:14:02 -08:00
|
|
|
|
return typeof obj === "number";
|
2023-07-12 21:58:36 -07:00
|
|
|
|
}
|
|
|
|
|
module.exports.isNumber = isNumber;
|
2018-02-27 21:14:02 -08:00
|
|
|
|
|
2023-07-12 21:58:36 -07:00
|
|
|
|
/**
|
|
|
|
|
* Returns true iff the input is a String.
|
|
|
|
|
*
|
|
|
|
|
* @param {Object} obj Object of unknown type.
|
|
|
|
|
* @returns {boolean} True iff obj is a String.
|
|
|
|
|
*/
|
|
|
|
|
function isString(obj) {
|
2018-02-25 16:04:13 -08:00
|
|
|
|
return typeof obj === "string";
|
2023-07-12 21:58:36 -07:00
|
|
|
|
}
|
|
|
|
|
module.exports.isString = isString;
|
2018-02-25 16:04:13 -08:00
|
|
|
|
|
2023-07-12 21:58:36 -07:00
|
|
|
|
/**
|
|
|
|
|
* Returns true iff the input String is empty.
|
|
|
|
|
*
|
|
|
|
|
* @param {string} str String of unknown length.
|
|
|
|
|
* @returns {boolean} True iff the input String is empty.
|
|
|
|
|
*/
|
|
|
|
|
function isEmptyString(str) {
|
2018-02-25 16:04:13 -08:00
|
|
|
|
return str.length === 0;
|
2023-07-12 21:58:36 -07:00
|
|
|
|
}
|
|
|
|
|
module.exports.isEmptyString = isEmptyString;
|
2018-02-25 16:04:13 -08:00
|
|
|
|
|
2023-07-12 21:58:36 -07:00
|
|
|
|
/**
|
|
|
|
|
* Returns true iff the input is an Object.
|
|
|
|
|
*
|
|
|
|
|
* @param {Object} obj Object of unknown type.
|
|
|
|
|
* @returns {boolean} True iff obj is an Object.
|
|
|
|
|
*/
|
|
|
|
|
function isObject(obj) {
|
2023-07-11 21:44:45 -07:00
|
|
|
|
return !!obj && (typeof obj === "object") && !Array.isArray(obj);
|
2023-07-12 21:58:36 -07:00
|
|
|
|
}
|
|
|
|
|
module.exports.isObject = isObject;
|
2023-07-11 21:44:45 -07:00
|
|
|
|
|
2023-07-12 21:58:36 -07:00
|
|
|
|
/**
|
|
|
|
|
* Returns true iff the input is a URL.
|
|
|
|
|
*
|
|
|
|
|
* @param {Object} obj Object of unknown type.
|
|
|
|
|
* @returns {boolean} True iff obj is a URL.
|
|
|
|
|
*/
|
|
|
|
|
function isUrl(obj) {
|
2023-07-11 21:44:45 -07:00
|
|
|
|
return !!obj && (Object.getPrototypeOf(obj) === URL.prototype);
|
2023-07-12 21:58:36 -07:00
|
|
|
|
}
|
|
|
|
|
module.exports.isUrl = isUrl;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Clones the input if it is an Array.
|
|
|
|
|
*
|
|
|
|
|
* @param {Object} arr Object of unknown type.
|
|
|
|
|
* @returns {Object} Clone of obj iff obj is an Array.
|
|
|
|
|
*/
|
|
|
|
|
function cloneIfArray(arr) {
|
|
|
|
|
return Array.isArray(arr) ? [ ...arr ] : arr;
|
|
|
|
|
}
|
|
|
|
|
module.exports.cloneIfArray = cloneIfArray;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Clones the input if it is a URL.
|
|
|
|
|
*
|
|
|
|
|
* @param {Object} url Object of unknown type.
|
|
|
|
|
* @returns {Object} Clone of obj iff obj is a URL.
|
|
|
|
|
*/
|
|
|
|
|
function cloneIfUrl(url) {
|
|
|
|
|
return isUrl(url) ? new URL(url) : url;
|
|
|
|
|
}
|
|
|
|
|
module.exports.cloneIfUrl = cloneIfUrl;
|
2019-09-14 13:39:27 -07:00
|
|
|
|
|
2023-10-18 23:45:39 -07:00
|
|
|
|
/**
|
|
|
|
|
* Gets a Regular Expression for matching the specified HTML attribute.
|
|
|
|
|
*
|
|
|
|
|
* @param {string} name HTML attribute name.
|
|
|
|
|
* @returns {RegExp} Regular Expression for matching.
|
|
|
|
|
*/
|
|
|
|
|
module.exports.getHtmlAttributeRe = function getHtmlAttributeRe(name) {
|
|
|
|
|
return new RegExp(`\\s${name}\\s*=\\s*['"]?([^'"\\s>]*)`, "iu");
|
|
|
|
|
};
|
|
|
|
|
|
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) => {
|
|
|
|
|
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
|
|
|
|
|
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 = "-->";
|
2022-10-14 20:59:42 -07:00
|
|
|
|
const safeCommentCharacter = ".";
|
|
|
|
|
const startsWithPipeRe = /^ *\|/;
|
|
|
|
|
const notCrLfRe = /[^\r\n]/g;
|
|
|
|
|
const notSpaceCrLfRe = /[^ \r\n]/g;
|
|
|
|
|
const trailingSpaceRe = / +[\r\n]/g;
|
|
|
|
|
const replaceTrailingSpace = (s) => s.replace(notCrLfRe, safeCommentCharacter);
|
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) {
|
2022-10-14 20:59:42 -07:00
|
|
|
|
const content = text.slice(i + htmlCommentBegin.length, j);
|
|
|
|
|
const lastLf = text.lastIndexOf("\n", i) + 1;
|
|
|
|
|
const preText = text.slice(lastLf, i);
|
|
|
|
|
const isBlock = preText.trim().length === 0;
|
|
|
|
|
const couldBeTable = startsWithPipeRe.test(preText);
|
|
|
|
|
const spansTableCells = couldBeTable && content.includes("\n");
|
|
|
|
|
const isValid =
|
|
|
|
|
isBlock ||
|
|
|
|
|
!(
|
|
|
|
|
spansTableCells ||
|
|
|
|
|
content.startsWith(">") ||
|
|
|
|
|
content.startsWith("->") ||
|
|
|
|
|
content.endsWith("-") ||
|
|
|
|
|
content.includes("--")
|
|
|
|
|
);
|
|
|
|
|
// If a valid block/inline comment...
|
|
|
|
|
if (isValid) {
|
|
|
|
|
const clearedContent = content
|
|
|
|
|
.replace(notSpaceCrLfRe, safeCommentCharacter)
|
|
|
|
|
.replace(trailingSpaceRe, replaceTrailingSpace);
|
|
|
|
|
text =
|
|
|
|
|
text.slice(0, i + htmlCommentBegin.length) +
|
|
|
|
|
clearedContent +
|
|
|
|
|
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, "\\$&");
|
|
|
|
|
};
|
|
|
|
|
|
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.
|
2024-02-29 23:05:27 -08:00
|
|
|
|
* @returns {"asterisk" | "underscore"} String representation.
|
2021-10-21 06:42:48 +02:00
|
|
|
|
*/
|
|
|
|
|
module.exports.emphasisOrStrongStyleFor =
|
|
|
|
|
function emphasisOrStrongStyleFor(markup) {
|
|
|
|
|
switch (markup[0]) {
|
|
|
|
|
case "*":
|
|
|
|
|
return "asterisk";
|
|
|
|
|
default:
|
|
|
|
|
return "underscore";
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
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;
|
|
|
|
|
|
2024-06-01 21:32:10 -07:00
|
|
|
|
/**
|
|
|
|
|
* Adds an error object with details conditionally via the onError callback.
|
|
|
|
|
*
|
|
|
|
|
* @param {Object} onError RuleOnError instance.
|
|
|
|
|
* @param {number} lineNumber Line number.
|
|
|
|
|
* @param {Object} expected Expected value.
|
|
|
|
|
* @param {Object} actual Actual value.
|
|
|
|
|
* @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}
|
|
|
|
|
*/
|
|
|
|
|
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
|
|
|
|
}
|
2024-06-01 21:32:10 -07:00
|
|
|
|
}
|
|
|
|
|
module.exports.addErrorDetailIf = addErrorDetailIf;
|
2018-01-18 21:27:07 -08:00
|
|
|
|
|
2024-06-01 21:32:10 -07:00
|
|
|
|
/**
|
|
|
|
|
* Adds an error object with context via the onError callback.
|
|
|
|
|
*
|
|
|
|
|
* @param {Object} onError RuleOnError instance.
|
|
|
|
|
* @param {number} lineNumber Line number.
|
|
|
|
|
* @param {string} context Error context.
|
|
|
|
|
* @param {boolean} [start] True iff the start of the text is important.
|
|
|
|
|
* @param {boolean} [end] True iff the end of the text is important.
|
|
|
|
|
* @param {number[]} [range] Column and length of error.
|
|
|
|
|
* @param {Object} [fixInfo] RuleOnErrorFixInfo instance.
|
|
|
|
|
* @returns {void}
|
|
|
|
|
*/
|
|
|
|
|
function addErrorContext(
|
|
|
|
|
onError, lineNumber, context, start, end, range, fixInfo) {
|
|
|
|
|
context = ellipsify(context, start, end);
|
2022-06-12 17:51:47 -07:00
|
|
|
|
addError(onError, lineNumber, undefined, context, range, fixInfo);
|
2024-06-01 21:32:10 -07:00
|
|
|
|
}
|
|
|
|
|
module.exports.addErrorContext = addErrorContext;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Adds an error object with context for a construct missing a blank line.
|
|
|
|
|
*
|
|
|
|
|
* @param {Object} onError RuleOnError instance.
|
|
|
|
|
* @param {string[]} lines Lines of Markdown content.
|
|
|
|
|
* @param {number} lineIndex Line index of line.
|
|
|
|
|
* @param {number} [lineNumber] Line number for override.
|
|
|
|
|
* @returns {void}
|
|
|
|
|
*/
|
|
|
|
|
function addErrorContextForLine(onError, lines, lineIndex, lineNumber) {
|
|
|
|
|
const line = lines[lineIndex];
|
|
|
|
|
// @ts-ignore
|
|
|
|
|
const quotePrefix = line.match(blockquotePrefixRe)[0].trimEnd();
|
|
|
|
|
addErrorContext(
|
|
|
|
|
onError,
|
|
|
|
|
lineIndex + 1,
|
|
|
|
|
line.trim(),
|
|
|
|
|
undefined,
|
|
|
|
|
undefined,
|
|
|
|
|
undefined,
|
|
|
|
|
{
|
|
|
|
|
lineNumber,
|
|
|
|
|
"insertText": `${quotePrefix}\n`
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
module.exports.addErrorContextForLine = addErrorContextForLine;
|
2018-01-18 21:27:07 -08:00
|
|
|
|
|
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
|
|
|
|
|
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-06-01 20:23:08 -07:00
|
|
|
|
/**
|
|
|
|
|
* Returns an object with information about reference links and images.
|
|
|
|
|
*
|
2024-03-09 16:17:50 -08:00
|
|
|
|
* @param {import("../helpers/micromark.cjs").Token[]} tokens Micromark tokens.
|
2022-06-01 20:23:08 -07:00
|
|
|
|
* @returns {Object} Reference link/image data.
|
|
|
|
|
*/
|
2024-03-09 16:17:50 -08:00
|
|
|
|
function getReferenceLinkImageData(tokens) {
|
2023-01-29 20:36:53 -08:00
|
|
|
|
const normalizeReference = (s) => s.toLowerCase().trim().replace(/\s+/g, " ");
|
2022-06-01 20:23:08 -07:00
|
|
|
|
const definitions = new Map();
|
2022-11-08 21:40:33 -08:00
|
|
|
|
const definitionLineIndices = [];
|
2023-01-29 20:36:53 -08:00
|
|
|
|
const duplicateDefinitions = [];
|
|
|
|
|
const references = new Map();
|
|
|
|
|
const shortcuts = new Map();
|
|
|
|
|
const filteredTokens =
|
|
|
|
|
micromark.filterByTypes(
|
2024-03-09 16:17:50 -08:00
|
|
|
|
tokens,
|
2023-02-05 16:58:06 -08:00
|
|
|
|
[
|
|
|
|
|
// definitionLineIndices
|
|
|
|
|
"definition", "gfmFootnoteDefinition",
|
|
|
|
|
// definitions and definitionLineIndices
|
|
|
|
|
"definitionLabelString", "gfmFootnoteDefinitionLabelString",
|
|
|
|
|
// references and shortcuts
|
|
|
|
|
"gfmFootnoteCall", "image", "link"
|
|
|
|
|
]
|
2023-01-29 20:36:53 -08:00
|
|
|
|
);
|
|
|
|
|
for (const token of filteredTokens) {
|
|
|
|
|
let labelPrefix = "";
|
|
|
|
|
// eslint-disable-next-line default-case
|
|
|
|
|
switch (token.type) {
|
|
|
|
|
case "definition":
|
|
|
|
|
case "gfmFootnoteDefinition":
|
|
|
|
|
// definitionLineIndices
|
|
|
|
|
for (let i = token.startLine; i <= token.endLine; i++) {
|
|
|
|
|
definitionLineIndices.push(i - 1);
|
2022-11-08 21:40:33 -08:00
|
|
|
|
}
|
2023-01-29 20:36:53 -08:00
|
|
|
|
break;
|
|
|
|
|
case "gfmFootnoteDefinitionLabelString":
|
|
|
|
|
labelPrefix = "^";
|
|
|
|
|
case "definitionLabelString": // eslint-disable-line no-fallthrough
|
|
|
|
|
{
|
|
|
|
|
// definitions and definitionLineIndices
|
|
|
|
|
const reference = normalizeReference(`${labelPrefix}${token.text}`);
|
|
|
|
|
if (definitions.has(reference)) {
|
|
|
|
|
duplicateDefinitions.push([ reference, token.startLine - 1 ]);
|
2022-07-30 16:12:27 -07:00
|
|
|
|
} else {
|
2023-10-25 20:05:19 -07:00
|
|
|
|
const parent =
|
|
|
|
|
micromark.getTokenParentOfType(token, [ "definition" ]);
|
2024-09-24 22:48:14 -07:00
|
|
|
|
const destinationString = parent &&
|
|
|
|
|
micromark.getDescendantsByType(parent, [ "definitionDestination", "definitionDestinationRaw", "definitionDestinationString" ])[0]?.text;
|
2023-10-25 20:05:19 -07:00
|
|
|
|
definitions.set(
|
|
|
|
|
reference,
|
|
|
|
|
[ token.startLine - 1, destinationString ]
|
|
|
|
|
);
|
2022-07-30 16:12:27 -07:00
|
|
|
|
}
|
2023-01-29 20:36:53 -08:00
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
case "gfmFootnoteCall":
|
|
|
|
|
case "image":
|
|
|
|
|
case "link":
|
|
|
|
|
{
|
2024-09-18 21:02:59 -07:00
|
|
|
|
// Identify if shortcut or full/collapsed
|
|
|
|
|
let isShortcut = (token.children.length === 1);
|
|
|
|
|
const isFullOrCollapsed = (token.children.length === 2) && !token.children.some((t) => t.type === "resource");
|
|
|
|
|
const [ labelText ] = micromark.getDescendantsByType(token, [ "label", "labelText" ]);
|
|
|
|
|
const [ referenceString ] = micromark.getDescendantsByType(token, [ "reference", "referenceString" ]);
|
|
|
|
|
let label = labelText?.text;
|
|
|
|
|
// Identify if footnote
|
|
|
|
|
if (!isShortcut && !isFullOrCollapsed) {
|
|
|
|
|
const [ footnoteCallMarker, footnoteCallString ] = token.children.filter(
|
|
|
|
|
(t) => [ "gfmFootnoteCallMarker", "gfmFootnoteCallString" ].includes(t.type)
|
2023-01-29 20:36:53 -08:00
|
|
|
|
);
|
2024-09-18 21:02:59 -07:00
|
|
|
|
if (footnoteCallMarker && footnoteCallString) {
|
|
|
|
|
label = `${footnoteCallMarker.text}${footnoteCallString.text}`;
|
|
|
|
|
isShortcut = true;
|
|
|
|
|
}
|
2023-01-29 20:36:53 -08:00
|
|
|
|
}
|
2024-09-18 21:02:59 -07:00
|
|
|
|
// Track link (handle shortcuts separately due to ambiguity in "text [text] text")
|
2023-01-29 20:36:53 -08:00
|
|
|
|
if (isShortcut || isFullOrCollapsed) {
|
|
|
|
|
const referenceDatum = [
|
|
|
|
|
token.startLine - 1,
|
|
|
|
|
token.startColumn - 1,
|
|
|
|
|
token.text.length,
|
2024-09-18 21:02:59 -07:00
|
|
|
|
label.length,
|
|
|
|
|
(referenceString?.text || "").length
|
2023-01-29 20:36:53 -08:00
|
|
|
|
];
|
|
|
|
|
const reference =
|
2024-09-18 21:02:59 -07:00
|
|
|
|
normalizeReference(referenceString?.text || label);
|
2023-01-29 20:36:53 -08:00
|
|
|
|
const dictionary = isShortcut ? shortcuts : references;
|
|
|
|
|
const referenceData = dictionary.get(reference) || [];
|
|
|
|
|
referenceData.push(referenceDatum);
|
|
|
|
|
dictionary.set(reference, referenceData);
|
2022-06-01 20:23:08 -07:00
|
|
|
|
}
|
|
|
|
|
}
|
2023-01-29 20:36:53 -08:00
|
|
|
|
break;
|
2022-06-01 20:23:08 -07:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return {
|
|
|
|
|
references,
|
|
|
|
|
shortcuts,
|
|
|
|
|
definitions,
|
2022-11-08 21:40:33 -08:00
|
|
|
|
duplicateDefinitions,
|
|
|
|
|
definitionLineIndices
|
2022-06-01 20:23:08 -07:00
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
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-08-16 04:01:53 +00:00
|
|
|
|
const lineEnding = getPreferredLineEnding(input, require("node: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
|
|
|
|
|
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) {
|
2022-07-24 12:22:32 -07:00
|
|
|
|
const homedir = os && os.homedir && os.homedir();
|
2022-05-16 22:57:11 -07:00
|
|
|
|
return homedir ? file.replace(/^~($|\/|\\)/, `${homedir}$1`) : file;
|
|
|
|
|
}
|
|
|
|
|
module.exports.expandTildePath = expandTildePath;
|
2023-09-04 21:41:16 -07:00
|
|
|
|
|
|
|
|
|
// Copied from markdownlint.js to avoid TypeScript compiler import() issue.
|
|
|
|
|
/**
|
|
|
|
|
* @typedef {Object} MarkdownItToken
|
|
|
|
|
* @property {string[][]} attrs HTML attributes.
|
|
|
|
|
* @property {boolean} block Block-level token.
|
|
|
|
|
* @property {MarkdownItToken[]} children Child nodes.
|
|
|
|
|
* @property {string} content Tag contents.
|
|
|
|
|
* @property {boolean} hidden Ignore element.
|
|
|
|
|
* @property {string} info Fence info.
|
|
|
|
|
* @property {number} level Nesting level.
|
|
|
|
|
* @property {number[]} map Beginning/ending line numbers.
|
|
|
|
|
* @property {string} markup Markup text.
|
|
|
|
|
* @property {Object} meta Arbitrary data.
|
|
|
|
|
* @property {number} nesting Level change.
|
|
|
|
|
* @property {string} tag HTML tag name.
|
|
|
|
|
* @property {string} type Token type.
|
|
|
|
|
* @property {number} lineNumber Line number (1-based).
|
|
|
|
|
* @property {string} line Line content.
|
|
|
|
|
*/
|