mirror of
https://github.com/DavidAnson/markdownlint.git
synced 2025-12-16 22:10:13 +01:00
Refactor use of micromark so token stream is authentic by shimming undefined link reference handling, remove no-longer-necessary parse operation in MD034.
This commit is contained in:
parent
1d9bd4725d
commit
38b4ec0c2f
6 changed files with 369 additions and 122 deletions
|
|
@ -381,11 +381,23 @@ module.exports.frontMatterHasTitle =
|
||||||
*/
|
*/
|
||||||
function getReferenceLinkImageData(tokens) {
|
function getReferenceLinkImageData(tokens) {
|
||||||
const normalizeReference = (s) => s.toLowerCase().trim().replace(/\s+/g, " ");
|
const normalizeReference = (s) => s.toLowerCase().trim().replace(/\s+/g, " ");
|
||||||
|
const references = new Map();
|
||||||
|
const shortcuts = new Map();
|
||||||
|
const addReferenceToDictionary = (token, label, isShortcut) => {
|
||||||
|
const referenceDatum = [
|
||||||
|
token.startLine - 1,
|
||||||
|
token.startColumn - 1,
|
||||||
|
token.text.length
|
||||||
|
];
|
||||||
|
const reference = normalizeReference(label);
|
||||||
|
const dictionary = isShortcut ? shortcuts : references;
|
||||||
|
const referenceData = dictionary.get(reference) || [];
|
||||||
|
referenceData.push(referenceDatum);
|
||||||
|
dictionary.set(reference, referenceData);
|
||||||
|
};
|
||||||
const definitions = new Map();
|
const definitions = new Map();
|
||||||
const definitionLineIndices = [];
|
const definitionLineIndices = [];
|
||||||
const duplicateDefinitions = [];
|
const duplicateDefinitions = [];
|
||||||
const references = new Map();
|
|
||||||
const shortcuts = new Map();
|
|
||||||
const filteredTokens =
|
const filteredTokens =
|
||||||
micromark.filterByTypes(
|
micromark.filterByTypes(
|
||||||
tokens,
|
tokens,
|
||||||
|
|
@ -395,7 +407,9 @@ function getReferenceLinkImageData(tokens) {
|
||||||
// definitions and definitionLineIndices
|
// definitions and definitionLineIndices
|
||||||
"definitionLabelString", "gfmFootnoteDefinitionLabelString",
|
"definitionLabelString", "gfmFootnoteDefinitionLabelString",
|
||||||
// references and shortcuts
|
// references and shortcuts
|
||||||
"gfmFootnoteCall", "image", "link"
|
"gfmFootnoteCall", "image", "link",
|
||||||
|
// undefined link labels
|
||||||
|
"undefinedReferenceCollapsed", "undefinedReferenceFull", "undefinedReferenceShortcut"
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
for (const token of filteredTokens) {
|
for (const token of filteredTokens) {
|
||||||
|
|
@ -451,22 +465,20 @@ function getReferenceLinkImageData(tokens) {
|
||||||
}
|
}
|
||||||
// Track link (handle shortcuts separately due to ambiguity in "text [text] text")
|
// Track link (handle shortcuts separately due to ambiguity in "text [text] text")
|
||||||
if (isShortcut || isFullOrCollapsed) {
|
if (isShortcut || isFullOrCollapsed) {
|
||||||
const referenceDatum = [
|
addReferenceToDictionary(token, referenceString?.text || label, isShortcut);
|
||||||
token.startLine - 1,
|
|
||||||
token.startColumn - 1,
|
|
||||||
token.text.length,
|
|
||||||
label.length,
|
|
||||||
(referenceString?.text || "").length
|
|
||||||
];
|
|
||||||
const reference =
|
|
||||||
normalizeReference(referenceString?.text || label);
|
|
||||||
const dictionary = isShortcut ? shortcuts : references;
|
|
||||||
const referenceData = dictionary.get(reference) || [];
|
|
||||||
referenceData.push(referenceDatum);
|
|
||||||
dictionary.set(reference, referenceData);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case "undefinedReferenceCollapsed":
|
||||||
|
case "undefinedReferenceFull":
|
||||||
|
case "undefinedReferenceShortcut":
|
||||||
|
{
|
||||||
|
const undefinedReference = micromark.getDescendantsByType(token, [ "undefinedReference" ])[0];
|
||||||
|
const label = undefinedReference.children.map((t) => t.text).join("");
|
||||||
|
const isShortcut = (token.type === "undefinedReferenceShortcut");
|
||||||
|
addReferenceToDictionary(token, label, isShortcut);
|
||||||
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
|
|
@ -985,8 +997,12 @@ const micromark = __webpack_require__(/*! markdownlint-micromark */ "markdownlin
|
||||||
const { isHtmlFlowComment } = __webpack_require__(/*! ./micromark-helpers.cjs */ "../helpers/micromark-helpers.cjs");
|
const { isHtmlFlowComment } = __webpack_require__(/*! ./micromark-helpers.cjs */ "../helpers/micromark-helpers.cjs");
|
||||||
const { flatTokensSymbol, htmlFlowSymbol, newLineRe } = __webpack_require__(/*! ./shared.js */ "../helpers/shared.js");
|
const { flatTokensSymbol, htmlFlowSymbol, newLineRe } = __webpack_require__(/*! ./shared.js */ "../helpers/shared.js");
|
||||||
|
|
||||||
|
/** @typedef {import("markdownlint-micromark").Construct} Construct */
|
||||||
/** @typedef {import("markdownlint-micromark").Event} Event */
|
/** @typedef {import("markdownlint-micromark").Event} Event */
|
||||||
/** @typedef {import("markdownlint-micromark").ParseOptions} MicromarkParseOptions */
|
/** @typedef {import("markdownlint-micromark").ParseOptions} MicromarkParseOptions */
|
||||||
|
/** @typedef {import("markdownlint-micromark").State} State */
|
||||||
|
/** @typedef {import("markdownlint-micromark").Token} Token */
|
||||||
|
/** @typedef {import("markdownlint-micromark").Tokenizer} Tokenizer */
|
||||||
/** @typedef {import("../lib/markdownlint.js").MicromarkToken} MicromarkToken */
|
/** @typedef {import("../lib/markdownlint.js").MicromarkToken} MicromarkToken */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -994,25 +1010,19 @@ const { flatTokensSymbol, htmlFlowSymbol, newLineRe } = __webpack_require__(/*!
|
||||||
*
|
*
|
||||||
* @typedef {Object} ParseOptions
|
* @typedef {Object} ParseOptions
|
||||||
* @property {boolean} [freezeTokens] Whether to freeze output Tokens.
|
* @property {boolean} [freezeTokens] Whether to freeze output Tokens.
|
||||||
* @property {boolean} [shimReferences] Whether to shim missing references.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parses a Markdown document and returns Micromark events.
|
* Parses a Markdown document and returns Micromark events.
|
||||||
*
|
*
|
||||||
* @param {string} markdown Markdown document.
|
* @param {string} markdown Markdown document.
|
||||||
* @param {ParseOptions} [parseOptions] Options.
|
|
||||||
* @param {MicromarkParseOptions} [micromarkParseOptions] Options for micromark.
|
* @param {MicromarkParseOptions} [micromarkParseOptions] Options for micromark.
|
||||||
* @returns {Event[]} Micromark events.
|
* @returns {Event[]} Micromark events.
|
||||||
*/
|
*/
|
||||||
function getEvents(
|
function getEvents(
|
||||||
markdown,
|
markdown,
|
||||||
parseOptions = {},
|
|
||||||
micromarkParseOptions = {}
|
micromarkParseOptions = {}
|
||||||
) {
|
) {
|
||||||
// Get options
|
|
||||||
const shimReferences = Boolean(parseOptions.shimReferences);
|
|
||||||
|
|
||||||
// Customize options object to add useful extensions
|
// Customize options object to add useful extensions
|
||||||
micromarkParseOptions.extensions = micromarkParseOptions.extensions || [];
|
micromarkParseOptions.extensions = micromarkParseOptions.extensions || [];
|
||||||
micromarkParseOptions.extensions.push(
|
micromarkParseOptions.extensions.push(
|
||||||
|
|
@ -1023,17 +1033,137 @@ function getEvents(
|
||||||
micromark.math()
|
micromark.math()
|
||||||
);
|
);
|
||||||
|
|
||||||
// Use micromark to parse document into Events
|
// // Shim labelEnd to identify undefined link labels
|
||||||
const encoding = undefined;
|
/** @type {Event[][]} */
|
||||||
const eol = true;
|
const artificialEventLists = [];
|
||||||
const parseContext = micromark.parse(micromarkParseOptions);
|
/** @type {Construct} */
|
||||||
if (shimReferences) {
|
const labelEnd =
|
||||||
// Customize ParseContext to treat all references as defined
|
// @ts-ignore
|
||||||
parseContext.defined.includes = (searchElement) => searchElement.length > 0;
|
micromark.labelEnd;
|
||||||
|
const tokenizeOriginal = labelEnd.tokenize;
|
||||||
|
|
||||||
|
/** @type {Tokenizer} */
|
||||||
|
function tokenizeShim(effects, okOriginal, nokOriginal) {
|
||||||
|
// eslint-disable-next-line consistent-this, unicorn/no-this-assignment, no-invalid-this
|
||||||
|
const tokenizeContext = this;
|
||||||
|
const events = tokenizeContext.events;
|
||||||
|
|
||||||
|
/** @type {State} */
|
||||||
|
const nokShim = (code) => {
|
||||||
|
// Find start of label (image or link)
|
||||||
|
let indexStart = events.length;
|
||||||
|
while (--indexStart >= 0) {
|
||||||
|
const event = events[indexStart];
|
||||||
|
const [ kind, token ] = event;
|
||||||
|
if (kind === "enter") {
|
||||||
|
const { type } = token;
|
||||||
|
if ((type === "labelImage") || (type === "labelLink")) {
|
||||||
|
// Found it
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If found...
|
||||||
|
if (indexStart >= 0) {
|
||||||
|
// Create artificial enter/exit events and replicate all data/lineEnding events within
|
||||||
|
const eventStart = events[indexStart];
|
||||||
|
const [ , eventStartToken ] = eventStart;
|
||||||
|
const eventEnd = events[events.length - 1];
|
||||||
|
const [ , eventEndToken ] = eventEnd;
|
||||||
|
/** @type {Token} */
|
||||||
|
const undefinedReferenceType = {
|
||||||
|
"type": "undefinedReferenceShortcut",
|
||||||
|
"start": eventStartToken.start,
|
||||||
|
"end": eventEndToken.end
|
||||||
|
};
|
||||||
|
/** @type {Token} */
|
||||||
|
const undefinedReference = {
|
||||||
|
"type": "undefinedReference",
|
||||||
|
"start": eventStartToken.start,
|
||||||
|
"end": eventEndToken.end
|
||||||
|
};
|
||||||
|
const eventsToReplicate = events
|
||||||
|
.slice(indexStart)
|
||||||
|
.filter((event) => {
|
||||||
|
const [ , eventToken ] = event;
|
||||||
|
const { type } = eventToken;
|
||||||
|
return (type === "data") || (type === "lineEnding");
|
||||||
|
});
|
||||||
|
|
||||||
|
// Determine the type of the undefined reference
|
||||||
|
const previousUndefinedEvent = (artificialEventLists.length > 0) && artificialEventLists[artificialEventLists.length - 1][0];
|
||||||
|
const previousUndefinedToken = previousUndefinedEvent && previousUndefinedEvent[1];
|
||||||
|
if (
|
||||||
|
previousUndefinedToken &&
|
||||||
|
(previousUndefinedToken.end.line === undefinedReferenceType.start.line) &&
|
||||||
|
(previousUndefinedToken.end.column === undefinedReferenceType.start.column)
|
||||||
|
) {
|
||||||
|
// Previous undefined reference event is immediately before this one
|
||||||
|
if (eventsToReplicate.length === 0) {
|
||||||
|
// The pair represent a collapsed reference (ex: [...][])
|
||||||
|
previousUndefinedToken.type = "undefinedReferenceCollapsed";
|
||||||
|
previousUndefinedToken.end = eventEndToken.end;
|
||||||
|
} else {
|
||||||
|
// The pair represent a full reference (ex: [...][...])
|
||||||
|
undefinedReferenceType.type = "undefinedReferenceFull";
|
||||||
|
undefinedReferenceType.start = previousUndefinedToken.start;
|
||||||
|
artificialEventLists.pop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create artificial event list and replicate content
|
||||||
|
const text = eventsToReplicate
|
||||||
|
.filter((event) => event[0] === "enter")
|
||||||
|
.map((event) => tokenizeContext.sliceSerialize(event[1]))
|
||||||
|
.join("")
|
||||||
|
.trim();
|
||||||
|
if ((text.length > 0) && !text.includes("]")) {
|
||||||
|
/** @type {Event[]} */
|
||||||
|
const artificialEvents = [];
|
||||||
|
artificialEvents.push(
|
||||||
|
[ "enter", undefinedReferenceType, tokenizeContext ],
|
||||||
|
[ "enter", undefinedReference, tokenizeContext ]
|
||||||
|
);
|
||||||
|
for (const event of eventsToReplicate) {
|
||||||
|
const [ kind, token ] = event;
|
||||||
|
// Copy token because the current object will get modified by the parser
|
||||||
|
artificialEvents.push([ kind, { ...token }, tokenizeContext ]);
|
||||||
|
}
|
||||||
|
artificialEvents.push(
|
||||||
|
[ "exit", undefinedReference, tokenizeContext ],
|
||||||
|
[ "exit", undefinedReferenceType, tokenizeContext ]
|
||||||
|
);
|
||||||
|
artificialEventLists.push(artificialEvents);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Continue with original behavior
|
||||||
|
return nokOriginal(code);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Shim nok handler of labelEnd's tokenize
|
||||||
|
return tokenizeOriginal.call(tokenizeContext, effects, okOriginal, nokShim);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Shim labelEnd behavior to detect undefined references
|
||||||
|
labelEnd.tokenize = tokenizeShim;
|
||||||
|
|
||||||
|
// Use micromark to parse document into Events
|
||||||
|
const encoding = undefined;
|
||||||
|
const eol = true;
|
||||||
|
const parseContext = micromark.parse(micromarkParseOptions);
|
||||||
|
const chunks = micromark.preprocess()(markdown, encoding, eol);
|
||||||
|
const events = micromark.postprocess(parseContext.document().write(chunks));
|
||||||
|
|
||||||
|
// Append artificial events and return all events
|
||||||
|
// eslint-disable-next-line unicorn/prefer-spread
|
||||||
|
return events.concat(...artificialEventLists);
|
||||||
|
} finally {
|
||||||
|
// Restore shimmed labelEnd behavior
|
||||||
|
labelEnd.tokenize = tokenizeOriginal;
|
||||||
}
|
}
|
||||||
const chunks = micromark.preprocess()(markdown, encoding, eol);
|
|
||||||
const events = micromark.postprocess(parseContext.document().write(chunks));
|
|
||||||
return events;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -1057,7 +1187,7 @@ function parseInternal(
|
||||||
const freezeTokens = Boolean(parseOptions.freezeTokens);
|
const freezeTokens = Boolean(parseOptions.freezeTokens);
|
||||||
|
|
||||||
// Use micromark to parse document into Events
|
// Use micromark to parse document into Events
|
||||||
const events = getEvents(markdown, parseOptions, micromarkParseOptions);
|
const events = getEvents(markdown, micromarkParseOptions);
|
||||||
|
|
||||||
// Create Token objects
|
// Create Token objects
|
||||||
const document = [];
|
const document = [];
|
||||||
|
|
@ -1974,7 +2104,7 @@ function lintContent(
|
||||||
// Parse content into parser tokens
|
// Parse content into parser tokens
|
||||||
const micromarkTokens = micromark.parse(
|
const micromarkTokens = micromark.parse(
|
||||||
content,
|
content,
|
||||||
{ "freezeTokens": customRulesPresent, "shimReferences": true }
|
{ "freezeTokens": customRulesPresent }
|
||||||
);
|
);
|
||||||
// Hide the content of HTML comments from rules
|
// Hide the content of HTML comments from rules
|
||||||
const preClearedContent = content;
|
const preClearedContent = content;
|
||||||
|
|
@ -5019,8 +5149,6 @@ module.exports = {
|
||||||
|
|
||||||
const { addErrorContext } = __webpack_require__(/*! ../helpers */ "../helpers/helpers.js");
|
const { addErrorContext } = __webpack_require__(/*! ../helpers */ "../helpers/helpers.js");
|
||||||
const { filterByPredicate, getHtmlTagInfo, inHtmlFlow } = __webpack_require__(/*! ../helpers/micromark-helpers.cjs */ "../helpers/micromark-helpers.cjs");
|
const { filterByPredicate, getHtmlTagInfo, inHtmlFlow } = __webpack_require__(/*! ../helpers/micromark-helpers.cjs */ "../helpers/micromark-helpers.cjs");
|
||||||
const { parse } = __webpack_require__(/*! ../helpers/micromark-parse.cjs */ "../helpers/micromark-parse.cjs");
|
|
||||||
const { filterByTypesCached } = __webpack_require__(/*! ./cache */ "../lib/cache.js");
|
|
||||||
|
|
||||||
// eslint-disable-next-line jsdoc/valid-types
|
// eslint-disable-next-line jsdoc/valid-types
|
||||||
/** @type import("./markdownlint").Rule */
|
/** @type import("./markdownlint").Rule */
|
||||||
|
|
@ -5054,6 +5182,7 @@ module.exports = {
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
(token) => {
|
(token) => {
|
||||||
|
// Ignore content of inline HTML tags
|
||||||
const { children } = token;
|
const { children } = token;
|
||||||
const result = [];
|
const result = [];
|
||||||
for (let i = 0; i < children.length; i++) {
|
for (let i = 0; i < children.length; i++) {
|
||||||
|
|
@ -5084,31 +5213,25 @@ module.exports = {
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
const autoLinks = filterByTypesCached([ "literalAutolink" ]);
|
for (const token of literalAutolinks(params.parsers.micromark.tokens)) {
|
||||||
if (autoLinks.length > 0) {
|
const range = [
|
||||||
// Re-parse with correct link/image reference definition handling
|
token.startColumn,
|
||||||
const document = params.lines.join("\n");
|
token.endColumn - token.startColumn
|
||||||
const tokens = parse(document);
|
];
|
||||||
for (const token of literalAutolinks(tokens)) {
|
const fixInfo = {
|
||||||
const range = [
|
"editColumn": range[0],
|
||||||
token.startColumn,
|
"deleteCount": range[1],
|
||||||
token.endColumn - token.startColumn
|
"insertText": `<${token.text}>`
|
||||||
];
|
};
|
||||||
const fixInfo = {
|
addErrorContext(
|
||||||
"editColumn": range[0],
|
onError,
|
||||||
"deleteCount": range[1],
|
token.startLine,
|
||||||
"insertText": `<${token.text}>`
|
token.text,
|
||||||
};
|
undefined,
|
||||||
addErrorContext(
|
undefined,
|
||||||
onError,
|
range,
|
||||||
token.startLine,
|
fixInfo
|
||||||
token.text,
|
);
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
range,
|
|
||||||
fixInfo
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -369,11 +369,23 @@ module.exports.frontMatterHasTitle =
|
||||||
*/
|
*/
|
||||||
function getReferenceLinkImageData(tokens) {
|
function getReferenceLinkImageData(tokens) {
|
||||||
const normalizeReference = (s) => s.toLowerCase().trim().replace(/\s+/g, " ");
|
const normalizeReference = (s) => s.toLowerCase().trim().replace(/\s+/g, " ");
|
||||||
|
const references = new Map();
|
||||||
|
const shortcuts = new Map();
|
||||||
|
const addReferenceToDictionary = (token, label, isShortcut) => {
|
||||||
|
const referenceDatum = [
|
||||||
|
token.startLine - 1,
|
||||||
|
token.startColumn - 1,
|
||||||
|
token.text.length
|
||||||
|
];
|
||||||
|
const reference = normalizeReference(label);
|
||||||
|
const dictionary = isShortcut ? shortcuts : references;
|
||||||
|
const referenceData = dictionary.get(reference) || [];
|
||||||
|
referenceData.push(referenceDatum);
|
||||||
|
dictionary.set(reference, referenceData);
|
||||||
|
};
|
||||||
const definitions = new Map();
|
const definitions = new Map();
|
||||||
const definitionLineIndices = [];
|
const definitionLineIndices = [];
|
||||||
const duplicateDefinitions = [];
|
const duplicateDefinitions = [];
|
||||||
const references = new Map();
|
|
||||||
const shortcuts = new Map();
|
|
||||||
const filteredTokens =
|
const filteredTokens =
|
||||||
micromark.filterByTypes(
|
micromark.filterByTypes(
|
||||||
tokens,
|
tokens,
|
||||||
|
|
@ -383,7 +395,9 @@ function getReferenceLinkImageData(tokens) {
|
||||||
// definitions and definitionLineIndices
|
// definitions and definitionLineIndices
|
||||||
"definitionLabelString", "gfmFootnoteDefinitionLabelString",
|
"definitionLabelString", "gfmFootnoteDefinitionLabelString",
|
||||||
// references and shortcuts
|
// references and shortcuts
|
||||||
"gfmFootnoteCall", "image", "link"
|
"gfmFootnoteCall", "image", "link",
|
||||||
|
// undefined link labels
|
||||||
|
"undefinedReferenceCollapsed", "undefinedReferenceFull", "undefinedReferenceShortcut"
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
for (const token of filteredTokens) {
|
for (const token of filteredTokens) {
|
||||||
|
|
@ -439,22 +453,20 @@ function getReferenceLinkImageData(tokens) {
|
||||||
}
|
}
|
||||||
// Track link (handle shortcuts separately due to ambiguity in "text [text] text")
|
// Track link (handle shortcuts separately due to ambiguity in "text [text] text")
|
||||||
if (isShortcut || isFullOrCollapsed) {
|
if (isShortcut || isFullOrCollapsed) {
|
||||||
const referenceDatum = [
|
addReferenceToDictionary(token, referenceString?.text || label, isShortcut);
|
||||||
token.startLine - 1,
|
|
||||||
token.startColumn - 1,
|
|
||||||
token.text.length,
|
|
||||||
label.length,
|
|
||||||
(referenceString?.text || "").length
|
|
||||||
];
|
|
||||||
const reference =
|
|
||||||
normalizeReference(referenceString?.text || label);
|
|
||||||
const dictionary = isShortcut ? shortcuts : references;
|
|
||||||
const referenceData = dictionary.get(reference) || [];
|
|
||||||
referenceData.push(referenceDatum);
|
|
||||||
dictionary.set(reference, referenceData);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case "undefinedReferenceCollapsed":
|
||||||
|
case "undefinedReferenceFull":
|
||||||
|
case "undefinedReferenceShortcut":
|
||||||
|
{
|
||||||
|
const undefinedReference = micromark.getDescendantsByType(token, [ "undefinedReference" ])[0];
|
||||||
|
const label = undefinedReference.children.map((t) => t.text).join("");
|
||||||
|
const isShortcut = (token.type === "undefinedReferenceShortcut");
|
||||||
|
addReferenceToDictionary(token, label, isShortcut);
|
||||||
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,12 @@ const micromark = require("markdownlint-micromark");
|
||||||
const { isHtmlFlowComment } = require("./micromark-helpers.cjs");
|
const { isHtmlFlowComment } = require("./micromark-helpers.cjs");
|
||||||
const { flatTokensSymbol, htmlFlowSymbol, newLineRe } = require("./shared.js");
|
const { flatTokensSymbol, htmlFlowSymbol, newLineRe } = require("./shared.js");
|
||||||
|
|
||||||
|
/** @typedef {import("markdownlint-micromark").Construct} Construct */
|
||||||
/** @typedef {import("markdownlint-micromark").Event} Event */
|
/** @typedef {import("markdownlint-micromark").Event} Event */
|
||||||
/** @typedef {import("markdownlint-micromark").ParseOptions} MicromarkParseOptions */
|
/** @typedef {import("markdownlint-micromark").ParseOptions} MicromarkParseOptions */
|
||||||
|
/** @typedef {import("markdownlint-micromark").State} State */
|
||||||
|
/** @typedef {import("markdownlint-micromark").Token} Token */
|
||||||
|
/** @typedef {import("markdownlint-micromark").Tokenizer} Tokenizer */
|
||||||
/** @typedef {import("../lib/markdownlint.js").MicromarkToken} MicromarkToken */
|
/** @typedef {import("../lib/markdownlint.js").MicromarkToken} MicromarkToken */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -15,25 +19,19 @@ const { flatTokensSymbol, htmlFlowSymbol, newLineRe } = require("./shared.js");
|
||||||
*
|
*
|
||||||
* @typedef {Object} ParseOptions
|
* @typedef {Object} ParseOptions
|
||||||
* @property {boolean} [freezeTokens] Whether to freeze output Tokens.
|
* @property {boolean} [freezeTokens] Whether to freeze output Tokens.
|
||||||
* @property {boolean} [shimReferences] Whether to shim missing references.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parses a Markdown document and returns Micromark events.
|
* Parses a Markdown document and returns Micromark events.
|
||||||
*
|
*
|
||||||
* @param {string} markdown Markdown document.
|
* @param {string} markdown Markdown document.
|
||||||
* @param {ParseOptions} [parseOptions] Options.
|
|
||||||
* @param {MicromarkParseOptions} [micromarkParseOptions] Options for micromark.
|
* @param {MicromarkParseOptions} [micromarkParseOptions] Options for micromark.
|
||||||
* @returns {Event[]} Micromark events.
|
* @returns {Event[]} Micromark events.
|
||||||
*/
|
*/
|
||||||
function getEvents(
|
function getEvents(
|
||||||
markdown,
|
markdown,
|
||||||
parseOptions = {},
|
|
||||||
micromarkParseOptions = {}
|
micromarkParseOptions = {}
|
||||||
) {
|
) {
|
||||||
// Get options
|
|
||||||
const shimReferences = Boolean(parseOptions.shimReferences);
|
|
||||||
|
|
||||||
// Customize options object to add useful extensions
|
// Customize options object to add useful extensions
|
||||||
micromarkParseOptions.extensions = micromarkParseOptions.extensions || [];
|
micromarkParseOptions.extensions = micromarkParseOptions.extensions || [];
|
||||||
micromarkParseOptions.extensions.push(
|
micromarkParseOptions.extensions.push(
|
||||||
|
|
@ -44,17 +42,137 @@ function getEvents(
|
||||||
micromark.math()
|
micromark.math()
|
||||||
);
|
);
|
||||||
|
|
||||||
// Use micromark to parse document into Events
|
// // Shim labelEnd to identify undefined link labels
|
||||||
const encoding = undefined;
|
/** @type {Event[][]} */
|
||||||
const eol = true;
|
const artificialEventLists = [];
|
||||||
const parseContext = micromark.parse(micromarkParseOptions);
|
/** @type {Construct} */
|
||||||
if (shimReferences) {
|
const labelEnd =
|
||||||
// Customize ParseContext to treat all references as defined
|
// @ts-ignore
|
||||||
parseContext.defined.includes = (searchElement) => searchElement.length > 0;
|
micromark.labelEnd;
|
||||||
|
const tokenizeOriginal = labelEnd.tokenize;
|
||||||
|
|
||||||
|
/** @type {Tokenizer} */
|
||||||
|
function tokenizeShim(effects, okOriginal, nokOriginal) {
|
||||||
|
// eslint-disable-next-line consistent-this, unicorn/no-this-assignment, no-invalid-this
|
||||||
|
const tokenizeContext = this;
|
||||||
|
const events = tokenizeContext.events;
|
||||||
|
|
||||||
|
/** @type {State} */
|
||||||
|
const nokShim = (code) => {
|
||||||
|
// Find start of label (image or link)
|
||||||
|
let indexStart = events.length;
|
||||||
|
while (--indexStart >= 0) {
|
||||||
|
const event = events[indexStart];
|
||||||
|
const [ kind, token ] = event;
|
||||||
|
if (kind === "enter") {
|
||||||
|
const { type } = token;
|
||||||
|
if ((type === "labelImage") || (type === "labelLink")) {
|
||||||
|
// Found it
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If found...
|
||||||
|
if (indexStart >= 0) {
|
||||||
|
// Create artificial enter/exit events and replicate all data/lineEnding events within
|
||||||
|
const eventStart = events[indexStart];
|
||||||
|
const [ , eventStartToken ] = eventStart;
|
||||||
|
const eventEnd = events[events.length - 1];
|
||||||
|
const [ , eventEndToken ] = eventEnd;
|
||||||
|
/** @type {Token} */
|
||||||
|
const undefinedReferenceType = {
|
||||||
|
"type": "undefinedReferenceShortcut",
|
||||||
|
"start": eventStartToken.start,
|
||||||
|
"end": eventEndToken.end
|
||||||
|
};
|
||||||
|
/** @type {Token} */
|
||||||
|
const undefinedReference = {
|
||||||
|
"type": "undefinedReference",
|
||||||
|
"start": eventStartToken.start,
|
||||||
|
"end": eventEndToken.end
|
||||||
|
};
|
||||||
|
const eventsToReplicate = events
|
||||||
|
.slice(indexStart)
|
||||||
|
.filter((event) => {
|
||||||
|
const [ , eventToken ] = event;
|
||||||
|
const { type } = eventToken;
|
||||||
|
return (type === "data") || (type === "lineEnding");
|
||||||
|
});
|
||||||
|
|
||||||
|
// Determine the type of the undefined reference
|
||||||
|
const previousUndefinedEvent = (artificialEventLists.length > 0) && artificialEventLists[artificialEventLists.length - 1][0];
|
||||||
|
const previousUndefinedToken = previousUndefinedEvent && previousUndefinedEvent[1];
|
||||||
|
if (
|
||||||
|
previousUndefinedToken &&
|
||||||
|
(previousUndefinedToken.end.line === undefinedReferenceType.start.line) &&
|
||||||
|
(previousUndefinedToken.end.column === undefinedReferenceType.start.column)
|
||||||
|
) {
|
||||||
|
// Previous undefined reference event is immediately before this one
|
||||||
|
if (eventsToReplicate.length === 0) {
|
||||||
|
// The pair represent a collapsed reference (ex: [...][])
|
||||||
|
previousUndefinedToken.type = "undefinedReferenceCollapsed";
|
||||||
|
previousUndefinedToken.end = eventEndToken.end;
|
||||||
|
} else {
|
||||||
|
// The pair represent a full reference (ex: [...][...])
|
||||||
|
undefinedReferenceType.type = "undefinedReferenceFull";
|
||||||
|
undefinedReferenceType.start = previousUndefinedToken.start;
|
||||||
|
artificialEventLists.pop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create artificial event list and replicate content
|
||||||
|
const text = eventsToReplicate
|
||||||
|
.filter((event) => event[0] === "enter")
|
||||||
|
.map((event) => tokenizeContext.sliceSerialize(event[1]))
|
||||||
|
.join("")
|
||||||
|
.trim();
|
||||||
|
if ((text.length > 0) && !text.includes("]")) {
|
||||||
|
/** @type {Event[]} */
|
||||||
|
const artificialEvents = [];
|
||||||
|
artificialEvents.push(
|
||||||
|
[ "enter", undefinedReferenceType, tokenizeContext ],
|
||||||
|
[ "enter", undefinedReference, tokenizeContext ]
|
||||||
|
);
|
||||||
|
for (const event of eventsToReplicate) {
|
||||||
|
const [ kind, token ] = event;
|
||||||
|
// Copy token because the current object will get modified by the parser
|
||||||
|
artificialEvents.push([ kind, { ...token }, tokenizeContext ]);
|
||||||
|
}
|
||||||
|
artificialEvents.push(
|
||||||
|
[ "exit", undefinedReference, tokenizeContext ],
|
||||||
|
[ "exit", undefinedReferenceType, tokenizeContext ]
|
||||||
|
);
|
||||||
|
artificialEventLists.push(artificialEvents);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Continue with original behavior
|
||||||
|
return nokOriginal(code);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Shim nok handler of labelEnd's tokenize
|
||||||
|
return tokenizeOriginal.call(tokenizeContext, effects, okOriginal, nokShim);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Shim labelEnd behavior to detect undefined references
|
||||||
|
labelEnd.tokenize = tokenizeShim;
|
||||||
|
|
||||||
|
// Use micromark to parse document into Events
|
||||||
|
const encoding = undefined;
|
||||||
|
const eol = true;
|
||||||
|
const parseContext = micromark.parse(micromarkParseOptions);
|
||||||
|
const chunks = micromark.preprocess()(markdown, encoding, eol);
|
||||||
|
const events = micromark.postprocess(parseContext.document().write(chunks));
|
||||||
|
|
||||||
|
// Append artificial events and return all events
|
||||||
|
// eslint-disable-next-line unicorn/prefer-spread
|
||||||
|
return events.concat(...artificialEventLists);
|
||||||
|
} finally {
|
||||||
|
// Restore shimmed labelEnd behavior
|
||||||
|
labelEnd.tokenize = tokenizeOriginal;
|
||||||
}
|
}
|
||||||
const chunks = micromark.preprocess()(markdown, encoding, eol);
|
|
||||||
const events = micromark.postprocess(parseContext.document().write(chunks));
|
|
||||||
return events;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -78,7 +196,7 @@ function parseInternal(
|
||||||
const freezeTokens = Boolean(parseOptions.freezeTokens);
|
const freezeTokens = Boolean(parseOptions.freezeTokens);
|
||||||
|
|
||||||
// Use micromark to parse document into Events
|
// Use micromark to parse document into Events
|
||||||
const events = getEvents(markdown, parseOptions, micromarkParseOptions);
|
const events = getEvents(markdown, micromarkParseOptions);
|
||||||
|
|
||||||
// Create Token objects
|
// Create Token objects
|
||||||
const document = [];
|
const document = [];
|
||||||
|
|
|
||||||
|
|
@ -499,7 +499,7 @@ function lintContent(
|
||||||
// Parse content into parser tokens
|
// Parse content into parser tokens
|
||||||
const micromarkTokens = micromark.parse(
|
const micromarkTokens = micromark.parse(
|
||||||
content,
|
content,
|
||||||
{ "freezeTokens": customRulesPresent, "shimReferences": true }
|
{ "freezeTokens": customRulesPresent }
|
||||||
);
|
);
|
||||||
// Hide the content of HTML comments from rules
|
// Hide the content of HTML comments from rules
|
||||||
const preClearedContent = content;
|
const preClearedContent = content;
|
||||||
|
|
|
||||||
47
lib/md034.js
47
lib/md034.js
|
|
@ -4,8 +4,6 @@
|
||||||
|
|
||||||
const { addErrorContext } = require("../helpers");
|
const { addErrorContext } = require("../helpers");
|
||||||
const { filterByPredicate, getHtmlTagInfo, inHtmlFlow } = require("../helpers/micromark-helpers.cjs");
|
const { filterByPredicate, getHtmlTagInfo, inHtmlFlow } = require("../helpers/micromark-helpers.cjs");
|
||||||
const { parse } = require("../helpers/micromark-parse.cjs");
|
|
||||||
const { filterByTypesCached } = require("./cache");
|
|
||||||
|
|
||||||
// eslint-disable-next-line jsdoc/valid-types
|
// eslint-disable-next-line jsdoc/valid-types
|
||||||
/** @type import("./markdownlint").Rule */
|
/** @type import("./markdownlint").Rule */
|
||||||
|
|
@ -39,6 +37,7 @@ module.exports = {
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
(token) => {
|
(token) => {
|
||||||
|
// Ignore content of inline HTML tags
|
||||||
const { children } = token;
|
const { children } = token;
|
||||||
const result = [];
|
const result = [];
|
||||||
for (let i = 0; i < children.length; i++) {
|
for (let i = 0; i < children.length; i++) {
|
||||||
|
|
@ -69,31 +68,25 @@ module.exports = {
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
const autoLinks = filterByTypesCached([ "literalAutolink" ]);
|
for (const token of literalAutolinks(params.parsers.micromark.tokens)) {
|
||||||
if (autoLinks.length > 0) {
|
const range = [
|
||||||
// Re-parse with correct link/image reference definition handling
|
token.startColumn,
|
||||||
const document = params.lines.join("\n");
|
token.endColumn - token.startColumn
|
||||||
const tokens = parse(document);
|
];
|
||||||
for (const token of literalAutolinks(tokens)) {
|
const fixInfo = {
|
||||||
const range = [
|
"editColumn": range[0],
|
||||||
token.startColumn,
|
"deleteCount": range[1],
|
||||||
token.endColumn - token.startColumn
|
"insertText": `<${token.text}>`
|
||||||
];
|
};
|
||||||
const fixInfo = {
|
addErrorContext(
|
||||||
"editColumn": range[0],
|
onError,
|
||||||
"deleteCount": range[1],
|
token.startLine,
|
||||||
"insertText": `<${token.text}>`
|
token.text,
|
||||||
};
|
undefined,
|
||||||
addErrorContext(
|
undefined,
|
||||||
onError,
|
range,
|
||||||
token.startLine,
|
fixInfo
|
||||||
token.text,
|
);
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
range,
|
|
||||||
fixInfo
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -410,6 +410,7 @@ Collapsed reference link: [label][]
|
||||||
Nested empty brackets: [text1[]](https://example.com/)
|
Nested empty brackets: [text1[]](https://example.com/)
|
||||||
Missing close bracket, empty text: [text2[](https://example.com/)
|
Missing close bracket, empty text: [text2[](https://example.com/)
|
||||||
Empty bracket pairs: [text3[]][]
|
Empty bracket pairs: [text3[]][]
|
||||||
|
Empty bracket pair: [text4[]]
|
||||||
|
|
||||||
[label]: https://example.com/label
|
[label]: https://example.com/label
|
||||||
`
|
`
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue