Add MD052/reference-links-images and MD053/link-image-reference-definitions for reporting issues with link and image references (fixes #144, fixes #390, fixes #425, fixes #456).

This commit is contained in:
David Anson 2022-06-01 20:23:08 -07:00
parent 2c947abf7b
commit c5ca661b96
21 changed files with 1333 additions and 65 deletions

View file

@ -108,6 +108,8 @@ playground for learning and exploring.
* **[MD049](doc/Rules.md#md049)** *emphasis-style* - Emphasis style should be consistent
* **[MD050](doc/Rules.md#md050)** *strong-style* - Strong style should be consistent
* **[MD051](doc/Rules.md#md051)** *link-fragments* - Link fragments should be valid
* **[MD052](doc/Rules.md#md052)** *reference-links-images* - Reference links and images should use a label that is defined
* **[MD053](doc/Rules.md#md053)** *link-image-reference-definitions* - Link and image reference definitions should be needed
<!-- markdownlint-restore -->
@ -139,11 +141,11 @@ rules at once.
MD023, MD024, MD025, MD026, MD036, MD041, MD043
* **hr** - MD035
* **html** - MD033
* **images** - MD045
* **images** - MD045, MD052, MD053
* **indentation** - MD005, MD006, MD007, MD027
* **language** - MD040
* **line_length** - MD013
* **links** - MD011, MD034, MD039, MD042, MD051
* **links** - MD011, MD034, MD039, MD042, MD051, MD052, MD053
* **ol** - MD029, MD030, MD032
* **spaces** - MD018, MD019, MD020, MD021, MD023
* **spelling** - MD044

View file

@ -52,8 +52,11 @@ module.exports.listItemMarkerRe = /^([\s>]*)(?:[*+-]|\d+[.)])\s+/;
module.exports.orderedListItemMarkerRe = /^[\s>]*0*(\d+)[.)]/;
// Regular expression for all instances of emphasis markers
var emphasisMarkersRe = /[_*]/g;
// Regular expression for link reference definition lines
module.exports.linkReferenceRe = /^ {0,3}\[[^\]]+]:\s.*$/;
// Regular expression for reference links (full and collapsed but not shortcut)
var referenceLinkRe = /!?\\?\[((?:\[[^\]]*]|[^\]])*)](?:(?:\[([^\]]*)\])|[^(]|$)/g;
// Regular expression for link reference definitions
var linkReferenceDefinitionRe = /^ {0,3}\[([^\]]*[^\\])]:/;
module.exports.linkReferenceDefinitionRe = linkReferenceDefinitionRe;
// All punctuation characters (normal and full-width)
var allPunctuation = ".,;:!?。,;:!?";
module.exports.allPunctuation = allPunctuation;
@ -521,6 +524,30 @@ function forEachInlineCodeSpan(input, handler) {
}
}
module.exports.forEachInlineCodeSpan = forEachInlineCodeSpan;
/**
* 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;
/**
* Adds a generic error object via the onError callback.
*
@ -551,18 +578,7 @@ module.exports.addErrorDetailIf = function addErrorDetailIf(onError, lineNumber,
};
// Adds an error object with context via the onError callback
module.exports.addErrorContext = function addErrorContext(onError, lineNumber, context, left, right, range, fixInfo) {
if (context.length <= 30) {
// Nothing to do
}
else if (left && right) {
context = context.substr(0, 15) + "..." + context.substr(-15);
}
else if (right) {
context = "..." + context.substr(-30);
}
else {
context = context.substr(0, 30) + "...";
}
context = ellipsify(context, left, right);
addError(onError, lineNumber, null, context, range, fixInfo);
};
/**
@ -628,6 +644,18 @@ module.exports.htmlElementRanges = function (params, lineMetadata) {
module.exports.overlapsAnyRange = function (ranges, lineIndex, index, length) { return (!ranges.every(function (span) { return ((lineIndex !== span[0]) ||
(index + length < span[1]) ||
(index > span[1] + span[2])); })); };
/**
* 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.
*/
var withinAnyRange = function (ranges, lineIndex, index, length) { return (!ranges.every(function (span) { return ((lineIndex !== span[0]) ||
(index < span[1]) ||
(index + length > span[1] + span[2])); })); };
// Returns a range object for a line by applying a RegExp
module.exports.rangeFromRegExp = function rangeFromRegExp(line, regexp) {
var range = null;
@ -773,6 +801,134 @@ function emphasisMarkersInContent(params) {
return byLine;
}
module.exports.emphasisMarkersInContent = emphasisMarkersInContent;
/**
* Returns an object with information about reference links and images.
*
* @param {Object} params RuleParams instance.
* @param {Object} lineMetadata Line metadata object.
* @returns {Object} Reference link/image data.
*/
function getReferenceLinkImageData(params, lineMetadata) {
// Initialize return values
var references = new Map();
var shortcuts = new Set();
var definitions = new Map();
var duplicateDefinitions = [];
// Define helper functions
var normalizeLabel = function (s) { return s.toLowerCase().trim().replace(/\s+/g, " "); };
var exclusions = [];
var excluded = function (match) { return withinAnyRange(exclusions, 0, match.index, match[0].length); };
// Convert input to single-line so multi-line links/images are easier
var lineOffsets = [];
var currentOffset = 0;
var contentLines = [];
forEachLine(lineMetadata, function (line, lineIndex, inCode) {
lineOffsets[lineIndex] = currentOffset;
if (!inCode) {
if (line.trim().length === 0) {
// Close any unclosed brackets at the end of a block
line = "]";
}
contentLines.push(line);
currentOffset += line.length + 1;
}
});
lineOffsets.push(currentOffset);
var contentLine = contentLines.join(" ");
// Determine single-line exclusions for inline code spans
forEachInlineCodeSpan(contentLine, function (code, lineIndex, columnIndex) {
exclusions.push([0, columnIndex, code.length]);
});
// Identify all link/image reference definitions
forEachLine(lineMetadata, function (line, lineIndex, inCode) {
if (!inCode) {
var linkReferenceDefinitionMatch = linkReferenceDefinitionRe.exec(line);
if (linkReferenceDefinitionMatch) {
var 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
var lineIndex = 0;
var pendingContents = [
{
"content": contentLine,
"contentLineIndex": 0,
"contentIndex": 0,
"topLevel": true
}
];
var pendingContent = null;
while ((pendingContent = pendingContents.shift())) {
var content = pendingContent.content, contentLineIndex = pendingContent.contentLineIndex, contentIndex = pendingContent.contentIndex, topLevel = pendingContent.topLevel;
var referenceLinkMatch = null;
while ((referenceLinkMatch = referenceLinkRe.exec(content)) !== null) {
var matchString = referenceLinkMatch[0], matchText = referenceLinkMatch[1], matchLabel = referenceLinkMatch[2];
if (!matchString.startsWith("\\") &&
!matchString.startsWith("!\\") &&
!matchText.endsWith("\\") &&
!(matchLabel || "").endsWith("\\") &&
(topLevel || matchString.startsWith("!")) &&
!excluded(referenceLinkMatch)) {
var shortcutLink = (matchLabel === undefined);
var collapsedLink = (!shortcutLink && (matchLabel.length === 0));
var 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 {
var referenceindex = referenceLinkMatch.index;
if (topLevel) {
// Calculate line index
while (lineOffsets[lineIndex + 1] <= referenceindex) {
lineIndex++;
}
}
else {
// Use provided line index
lineIndex = contentLineIndex;
}
var referenceIndex = referenceindex +
(topLevel ? -lineOffsets[lineIndex] : contentIndex);
// Track reference and location
var 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: references,
shortcuts: shortcuts,
definitions: definitions,
duplicateDefinitions: duplicateDefinitions
};
}
module.exports.getReferenceLinkImageData = getReferenceLinkImageData;
/**
* Gets the most common line ending, falling back to the platform default.
*
@ -1109,11 +1265,19 @@ module.exports.lineMetadata = function (value) {
}
return lineMetadata;
};
var referenceLinkImageData = null;
module.exports.referenceLinkImageData = function (value) {
if (value) {
referenceLinkImageData = value;
}
return referenceLinkImageData;
};
module.exports.clear = function () {
codeBlockAndSpanRanges = null;
flattenedLists = null;
htmlElementRanges = null;
lineMetadata = null;
referenceLinkImageData = null;
};
@ -1612,10 +1776,11 @@ function lintContent(ruleList, name, content, md, config, frontMatter, handleRul
frontMatterLines: frontMatterLines
});
var lineMetadata = helpers.getLineMetadata(paramsBase);
cache.lineMetadata(lineMetadata);
cache.codeBlockAndSpanRanges(helpers.codeBlockAndSpanRanges(paramsBase, lineMetadata));
cache.htmlElementRanges(helpers.htmlElementRanges(paramsBase, lineMetadata));
cache.flattenedLists(helpers.flattenLists(paramsBase.tokens));
cache.htmlElementRanges(helpers.htmlElementRanges(paramsBase, lineMetadata));
cache.lineMetadata(lineMetadata);
cache.referenceLinkImageData(helpers.getReferenceLinkImageData(paramsBase, lineMetadata));
// Function to run for each rule
var results = [];
// eslint-disable-next-line jsdoc/require-jsdoc
@ -2723,12 +2888,11 @@ module.exports = {
"use strict";
// @ts-check
var _a = __webpack_require__(/*! ../helpers */ "../helpers/helpers.js"), addErrorDetailIf = _a.addErrorDetailIf, filterTokens = _a.filterTokens, forEachHeading = _a.forEachHeading, forEachLine = _a.forEachLine, includesSorted = _a.includesSorted;
var _a = __webpack_require__(/*! ../helpers */ "../helpers/helpers.js"), addErrorDetailIf = _a.addErrorDetailIf, filterTokens = _a.filterTokens, forEachHeading = _a.forEachHeading, forEachLine = _a.forEachLine, includesSorted = _a.includesSorted, linkReferenceDefinitionRe = _a.linkReferenceDefinitionRe;
var lineMetadata = (__webpack_require__(/*! ./cache */ "../lib/cache.js").lineMetadata);
var longLineRePrefix = "^.{";
var longLineRePostfixRelaxed = "}.*\\s.*$";
var longLineRePostfixStrict = "}.+$";
var labelRe = /^\s*\[.*[^\\]]:/;
var linkOrImageOnlyLineRe = /^[es]*(lT?L|I)[ES]*$/;
var sternModeRe = /^([#>\s]*\s)?\S*$/;
var tokenTypeMap = {
@ -2795,7 +2959,7 @@ module.exports = {
(strict ||
(!(stern && sternModeRe.test(line)) &&
!includesSorted(linkOnlyLineNumbers, lineNumber) &&
!labelRe.test(line))) &&
!linkReferenceDefinitionRe.test(line))) &&
lengthRe.test(line)) {
addErrorDetailIf(onError, lineNumber, length, line.length, null, null, [length + 1, line.length - length]);
}
@ -4243,7 +4407,7 @@ module.exports = {
"use strict";
// @ts-check
var _a = __webpack_require__(/*! ../helpers */ "../helpers/helpers.js"), addErrorDetailIf = _a.addErrorDetailIf, bareUrlRe = _a.bareUrlRe, escapeForRegExp = _a.escapeForRegExp, forEachLine = _a.forEachLine, forEachLink = _a.forEachLink, overlapsAnyRange = _a.overlapsAnyRange, linkReferenceRe = _a.linkReferenceRe;
var _a = __webpack_require__(/*! ../helpers */ "../helpers/helpers.js"), addErrorDetailIf = _a.addErrorDetailIf, bareUrlRe = _a.bareUrlRe, escapeForRegExp = _a.escapeForRegExp, forEachLine = _a.forEachLine, forEachLink = _a.forEachLink, overlapsAnyRange = _a.overlapsAnyRange, linkReferenceDefinitionRe = _a.linkReferenceDefinitionRe;
var _b = __webpack_require__(/*! ./cache */ "../lib/cache.js"), codeBlockAndSpanRanges = _b.codeBlockAndSpanRanges, htmlElementRanges = _b.htmlElementRanges, lineMetadata = _b.lineMetadata;
module.exports = {
"names": ["MD044", "proper-names"],
@ -4259,7 +4423,7 @@ module.exports = {
var includeHtmlElements = (htmlElements === undefined) ? true : !!htmlElements;
var exclusions = [];
forEachLine(lineMetadata(), function (line, lineIndex) {
if (linkReferenceRe.test(line)) {
if (linkReferenceDefinitionRe.test(line)) {
exclusions.push([lineIndex, 0, line.length]);
}
else {
@ -4581,6 +4745,88 @@ module.exports = {
};
/***/ }),
/***/ "../lib/md052.js":
/*!***********************!*\
!*** ../lib/md052.js ***!
\***********************/
/***/ ((module, __unused_webpack_exports, __webpack_require__) => {
"use strict";
// @ts-check
var addError = (__webpack_require__(/*! ../helpers */ "../helpers/helpers.js").addError);
var referenceLinkImageData = (__webpack_require__(/*! ./cache */ "../lib/cache.js").referenceLinkImageData);
module.exports = {
"names": ["MD052", "reference-links-images"],
"description": "Reference links and images should use a label that is defined",
"tags": ["images", "links"],
"function": function MD052(params, onError) {
var lines = params.lines;
var _a = referenceLinkImageData(), references = _a.references, definitions = _a.definitions;
// Look for links/images that use an undefined link reference
for (var _i = 0, _b = references.entries(); _i < _b.length; _i++) {
var reference = _b[_i];
var label = reference[0], datas = reference[1];
if (!definitions.has(label)) {
for (var _c = 0, datas_1 = datas; _c < datas_1.length; _c++) {
var data = datas_1[_c];
var lineIndex = data[0], index = data[1], length = data[2];
// Context will be incomplete if reporting for a multi-line link
var context = lines[lineIndex].slice(index, index + length);
addError(onError, lineIndex + 1, "Missing link or image reference definition: \"".concat(label, "\""), context, [index + 1, context.length]);
}
}
}
}
};
/***/ }),
/***/ "../lib/md053.js":
/*!***********************!*\
!*** ../lib/md053.js ***!
\***********************/
/***/ ((module, __unused_webpack_exports, __webpack_require__) => {
"use strict";
// @ts-check
var _a = __webpack_require__(/*! ../helpers */ "../helpers/helpers.js"), addError = _a.addError, ellipsify = _a.ellipsify, linkReferenceDefinitionRe = _a.linkReferenceDefinitionRe;
var referenceLinkImageData = (__webpack_require__(/*! ./cache */ "../lib/cache.js").referenceLinkImageData);
module.exports = {
"names": ["MD053", "link-image-reference-definitions"],
"description": "Link and image reference definitions should be needed",
"tags": ["images", "links"],
"function": function MD053(params, onError) {
var lines = params.lines;
var _a = referenceLinkImageData(), references = _a.references, shortcuts = _a.shortcuts, definitions = _a.definitions, duplicateDefinitions = _a.duplicateDefinitions;
var singleLineDefinition = function (line) { return (line.replace(linkReferenceDefinitionRe, "").trim().length > 0); };
var deleteFixInfo = {
"deleteCount": -1
};
// Look for unused link references (unreferenced by any link/image)
for (var _i = 0, _b = definitions.entries(); _i < _b.length; _i++) {
var definition = _b[_i];
var label = definition[0], lineIndex = definition[1];
if (!references.has(label) && !shortcuts.has(label)) {
var line = lines[lineIndex];
addError(onError, lineIndex + 1, "Unused link or image reference definition: \"".concat(label, "\""), ellipsify(line), [1, line.length], singleLineDefinition(line) ? deleteFixInfo : 0);
}
}
// Look for duplicate link references (defined more than once)
for (var _c = 0, duplicateDefinitions_1 = duplicateDefinitions; _c < duplicateDefinitions_1.length; _c++) {
var duplicateDefinition = duplicateDefinitions_1[_c];
var label = duplicateDefinition[0], lineIndex = duplicateDefinition[1];
var line = lines[lineIndex];
addError(onError, lineIndex + 1, "Duplicate link or image reference definition: \"".concat(label, "\""), ellipsify(line), [1, line.length], singleLineDefinition(line) ? deleteFixInfo : 0);
}
}
};
/***/ }),
/***/ "../lib/rules.js":
@ -4648,7 +4894,9 @@ var rules = __spreadArray(__spreadArray([
__webpack_require__(/*! ./md047 */ "../lib/md047.js"),
__webpack_require__(/*! ./md048 */ "../lib/md048.js")
], __webpack_require__(/*! ./md049-md050 */ "../lib/md049-md050.js"), true), [
__webpack_require__(/*! ./md051 */ "../lib/md051.js")
__webpack_require__(/*! ./md051 */ "../lib/md051.js"),
__webpack_require__(/*! ./md052 */ "../lib/md052.js"),
__webpack_require__(/*! ./md053 */ "../lib/md053.js")
], false);
rules.forEach(function (rule) {
var name = rule.names[0].toLowerCase();

View file

@ -2031,6 +2031,70 @@ Note: Creating anchors for headings is not part of the CommonMark specification.
[github-section-links]: https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax#section-links
<a name="md052"></a>
## MD052 - Reference links and images should use a label that is defined
Tags: images, links
Aliases: reference-links-images
Links and images in Markdown can provide the link destination or image source
at the time of use or can define it elsewhere and use a label for reference.
The reference format is convenient for keeping paragraph text clutter-free
and makes it easy to reuse the same URL in multiple places.
There are three kinds of reference links and images:
```markdown
Full: [text][label]
Collapsed: [label][]
Shortcut: [label]
Full: ![text][image]
Collapsed: ![image][]
Shortcut: ![image]
[label]: https://example.com/label
[image]: https://example.com/image
```
A link or image renders correctly when a corresponding label is defined, but
the text displays with brackets if the label is not present. This rule warns
of undefined labels for "full" and "collapsed" reference syntax.
> "Shortcut" syntax is ambiguous and a missing label will not generate an
error. For example, `[shortcut]` could be a shortcut link or the text
"shortcut" in brackets.
<a name="md053"></a>
## MD053 - Link and image reference definitions should be needed
Tags: images, links
Aliases: link-image-reference-definitions
Fixable: Most violations can be fixed by tooling
Links and images in Markdown can provide the link destination or image source
at the time of use or can define it elsewhere and use a label for reference.
The reference format is convenient for keeping paragraph text clutter-free
and makes it easy to reuse the same URL in multiple places.
Because link and image reference definitions are located separately from
where they are used, there are two scenarios where a definition can be
unnecessary:
1. If a label is not referenced by any link or image in a document, that
definition is unused and can be deleted.
1. If a label is defined multiple times in a document, the first definition is
used and the others can be deleted.
This rule considers a reference definition to be used if any link or image
reference has the corresponding label. "Full", "collapsed", and "shortcut"
formats are all supported.
<!-- markdownlint-configure-file {
"no-inline-html": {
"allowed_elements": [

View file

@ -30,8 +30,13 @@ module.exports.orderedListItemMarkerRe = /^[\s>]*0*(\d+)[.)]/;
// Regular expression for all instances of emphasis markers
const emphasisMarkersRe = /[_*]/g;
// Regular expression for link reference definition lines
module.exports.linkReferenceRe = /^ {0,3}\[[^\]]+]:\s.*$/;
// Regular expression for reference links (full and collapsed but not shortcut)
const referenceLinkRe =
/!?\\?\[((?:\[[^\]]*]|[^\]])*)](?:(?:\[([^\]]*)\])|[^(]|$)/g;
// Regular expression for link reference definitions
const linkReferenceDefinitionRe = /^ {0,3}\[([^\]]*[^\\])]:/;
module.exports.linkReferenceDefinitionRe = linkReferenceDefinitionRe;
// All punctuation characters (normal and full-width)
const allPunctuation = ".,;:!?。,;:!?";
@ -515,6 +520,28 @@ function forEachInlineCodeSpan(input, handler) {
}
module.exports.forEachInlineCodeSpan = forEachInlineCodeSpan;
/**
* 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;
/**
* Adds a generic error object via the onError callback.
*
@ -555,15 +582,7 @@ module.exports.addErrorDetailIf = function addErrorDetailIf(
// Adds an error object with context via the onError callback
module.exports.addErrorContext = function addErrorContext(
onError, lineNumber, context, left, right, range, fixInfo) {
if (context.length <= 30) {
// Nothing to do
} else if (left && right) {
context = context.substr(0, 15) + "..." + context.substr(-15);
} else if (right) {
context = "..." + context.substr(-30);
} else {
context = context.substr(0, 30) + "...";
}
context = ellipsify(context, left, right);
addError(onError, lineNumber, null, context, range, fixInfo);
};
@ -640,6 +659,23 @@ module.exports.overlapsAnyRange = (ranges, lineIndex, index, length) => (
))
);
/**
* 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])
))
);
// Returns a range object for a line by applying a RegExp
module.exports.rangeFromRegExp = function rangeFromRegExp(line, regexp) {
let range = null;
@ -789,6 +825,142 @@ function emphasisMarkersInContent(params) {
}
module.exports.emphasisMarkersInContent = emphasisMarkersInContent;
/**
* Returns an object with information about reference links and images.
*
* @param {Object} params RuleParams instance.
* @param {Object} lineMetadata Line metadata object.
* @returns {Object} Reference link/image data.
*/
function getReferenceLinkImageData(params, lineMetadata) {
// 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) {
// Close any unclosed brackets at the end of a block
line = "]";
}
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;
/**
* Gets the most common line ending, falling back to the platform default.
*

View file

@ -34,9 +34,18 @@ module.exports.lineMetadata = (value) => {
return lineMetadata;
};
let referenceLinkImageData = null;
module.exports.referenceLinkImageData = (value) => {
if (value) {
referenceLinkImageData = value;
}
return referenceLinkImageData;
};
module.exports.clear = () => {
codeBlockAndSpanRanges = null;
flattenedLists = null;
htmlElementRanges = null;
lineMetadata = null;
referenceLinkImageData = null;
};

View file

@ -499,14 +499,17 @@ function lintContent(
frontMatterLines
});
const lineMetadata = helpers.getLineMetadata(paramsBase);
cache.lineMetadata(lineMetadata);
cache.codeBlockAndSpanRanges(
helpers.codeBlockAndSpanRanges(paramsBase, lineMetadata)
);
cache.flattenedLists(helpers.flattenLists(paramsBase.tokens));
cache.htmlElementRanges(
helpers.htmlElementRanges(paramsBase, lineMetadata)
);
cache.flattenedLists(helpers.flattenLists(paramsBase.tokens));
cache.lineMetadata(lineMetadata);
cache.referenceLinkImageData(
helpers.getReferenceLinkImageData(paramsBase, lineMetadata)
);
// Function to run for each rule
let results = [];
// eslint-disable-next-line jsdoc/require-jsdoc

View file

@ -3,13 +3,12 @@
"use strict";
const { addErrorDetailIf, filterTokens, forEachHeading, forEachLine,
includesSorted } = require("../helpers");
includesSorted, linkReferenceDefinitionRe } = require("../helpers");
const { lineMetadata } = require("./cache");
const longLineRePrefix = "^.{";
const longLineRePostfixRelaxed = "}.*\\s.*$";
const longLineRePostfixStrict = "}.+$";
const labelRe = /^\s*\[.*[^\\]]:/;
const linkOrImageOnlyLineRe = /^[es]*(lT?L|I)[ES]*$/;
const sternModeRe = /^([#>\s]*\s)?\S*$/;
const tokenTypeMap = {
@ -83,7 +82,7 @@ module.exports = {
(strict ||
(!(stern && sternModeRe.test(line)) &&
!includesSorted(linkOnlyLineNumbers, lineNumber) &&
!labelRe.test(line))) &&
!linkReferenceDefinitionRe.test(line))) &&
lengthRe.test(line)) {
addErrorDetailIf(
onError,

View file

@ -3,7 +3,8 @@
"use strict";
const { addErrorDetailIf, bareUrlRe, escapeForRegExp, forEachLine,
forEachLink, overlapsAnyRange, linkReferenceRe } = require("../helpers");
forEachLink, overlapsAnyRange, linkReferenceDefinitionRe } =
require("../helpers");
const { codeBlockAndSpanRanges, htmlElementRanges, lineMetadata } =
require("./cache");
@ -23,7 +24,7 @@ module.exports = {
(htmlElements === undefined) ? true : !!htmlElements;
const exclusions = [];
forEachLine(lineMetadata(), (line, lineIndex) => {
if (linkReferenceRe.test(line)) {
if (linkReferenceDefinitionRe.test(line)) {
exclusions.push([ lineIndex, 0, line.length ]);
} else {
let match = null;

35
lib/md052.js Normal file
View file

@ -0,0 +1,35 @@
// @ts-check
"use strict";
const { addError } = require("../helpers");
const { referenceLinkImageData } = require("./cache");
module.exports = {
"names": [ "MD052", "reference-links-images" ],
"description":
"Reference links and images should use a label that is defined",
"tags": [ "images", "links" ],
"function": function MD052(params, onError) {
const { lines } = params;
const { references, definitions } = referenceLinkImageData();
// Look for links/images that use an undefined link reference
for (const reference of references.entries()) {
const [ label, datas ] = reference;
if (!definitions.has(label)) {
for (const data of datas) {
const [ lineIndex, index, length ] = data;
// Context will be incomplete if reporting for a multi-line link
const context = lines[lineIndex].slice(index, index + length);
addError(
onError,
lineIndex + 1,
`Missing link or image reference definition: "${label}"`,
context,
[ index + 1, context.length ]
);
}
}
}
}
};

52
lib/md053.js Normal file
View file

@ -0,0 +1,52 @@
// @ts-check
"use strict";
const { addError, ellipsify, linkReferenceDefinitionRe } =
require("../helpers");
const { referenceLinkImageData } = require("./cache");
module.exports = {
"names": [ "MD053", "link-image-reference-definitions" ],
"description": "Link and image reference definitions should be needed",
"tags": [ "images", "links" ],
"function": function MD053(params, onError) {
const { lines } = params;
const { references, shortcuts, definitions, duplicateDefinitions } =
referenceLinkImageData();
const singleLineDefinition = (line) => (
line.replace(linkReferenceDefinitionRe, "").trim().length > 0
);
const deleteFixInfo = {
"deleteCount": -1
};
// Look for unused link references (unreferenced by any link/image)
for (const definition of definitions.entries()) {
const [ label, lineIndex ] = definition;
if (!references.has(label) && !shortcuts.has(label)) {
const line = lines[lineIndex];
addError(
onError,
lineIndex + 1,
`Unused link or image reference definition: "${label}"`,
ellipsify(line),
[ 1, line.length ],
singleLineDefinition(line) ? deleteFixInfo : 0
);
}
}
// Look for duplicate link references (defined more than once)
for (const duplicateDefinition of duplicateDefinitions) {
const [ label, lineIndex ] = duplicateDefinition;
const line = lines[lineIndex];
addError(
onError,
lineIndex + 1,
`Duplicate link or image reference definition: "${label}"`,
ellipsify(line),
[ 1, line.length ],
singleLineDefinition(line) ? deleteFixInfo : 0
);
}
}
};

View file

@ -50,7 +50,9 @@ const rules = [
require("./md047"),
require("./md048"),
...require("./md049-md050"),
require("./md051")
require("./md051"),
require("./md052"),
require("./md053")
];
rules.forEach((rule) => {
const name = rule.names[0].toLowerCase();

View file

@ -269,5 +269,11 @@
},
// MD051/link-fragments - Link fragments should be valid
"MD051": true
"MD051": true,
// MD052/reference-links-images - Reference links and images should use a label that is defined
"MD052": true,
// MD053/link-image-reference-definitions - Link and image reference definitions should be needed
"MD053": true
}

View file

@ -244,3 +244,9 @@ MD050:
# MD051/link-fragments - Link fragments should be valid
MD051: true
# MD052/reference-links-images - Reference links and images should use a label that is defined
MD052: true
# MD053/link-image-reference-definitions - Link and image reference definitions should be needed
MD053: true

View file

@ -899,6 +899,22 @@
"link-fragments": {
"$ref": "#/properties/MD051"
},
"MD052": {
"description": "MD052/reference-links-images - Reference links and images should use a label that is defined",
"type": "boolean",
"default": true
},
"reference-links-images": {
"$ref": "#/properties/MD052"
},
"MD053": {
"description": "MD053/link-image-reference-definitions - Link and image reference definitions should be needed",
"type": "boolean",
"default": true
},
"link-image-reference-definitions": {
"$ref": "#/properties/MD053"
},
"headings": {
"description": "headings - MD001, MD002, MD003, MD018, MD019, MD020, MD021, MD022, MD023, MD024, MD025, MD026, MD036, MD041, MD043",
"type": "boolean",
@ -935,7 +951,7 @@
"default": true
},
"links": {
"description": "links - MD011, MD034, MD039, MD042, MD051",
"description": "links - MD011, MD034, MD039, MD042, MD051, MD052, MD053",
"type": "boolean",
"default": true
},
@ -1015,7 +1031,7 @@
"default": true
},
"images": {
"description": "images - MD045",
"description": "images - MD045, MD052, MD053",
"type": "boolean",
"default": true
}

View file

@ -95,4 +95,7 @@ Strong **with** different style {MD050}
[Missing link fragment](#missing) {MD051}
[Missing link][label] {MD052}
[unused]: link-destination {MD053}
EOF {MD047}

View file

@ -24,7 +24,7 @@ This long line includes a simple [reference][label] link and is long enough to v
[label]: https://example.org "Title for a link reference that is itself long enough to violate the rule"
[Link to broken label][notlabel]
[Link to broken label][notlabel] {MD052}
[notlabel\]: notlink "Invalid syntax for a link label because the right bracket is backslash-escaped {MD013}"

View file

@ -125,12 +125,16 @@ function excludeGlobs(rootDir, ...globs) {
}
// Run markdownlint the same way the corresponding repositories do
/* eslint-disable max-len */
test("https://github.com/eslint/eslint", (t) => {
const rootDir = "./test-repos/eslint-eslint";
const globPatterns = [ join(rootDir, "docs/**/*.md") ];
const configPath = join(rootDir, ".markdownlint.yml");
const ignoreRes = [ /^[^:]+: \d+: MD051\/.*$\r?\n?/gm ];
const ignoreRes = [
/^[^:]+: \d+: MD051\/.*$\r?\n?/gm,
/^test-repos\/eslint-eslint\/docs\/src\/developer-guide\/nodejs-api\.md: \d+: MD053\/.*$\r?\n?/gm
];
return lintTestRepo(t, globPatterns, configPath, ignoreRes);
});
@ -148,7 +152,19 @@ test("https://github.com/mkdocs/mkdocs", (t) => {
)
];
const configPath = join(rootDir, ".markdownlintrc");
const ignoreRes = [ /^[^:]+: \d+: MD051\/.*$\r?\n?/gm ];
const ignoreRes = [
/^[^:]+: \d+: MD051\/.*$\r?\n?/gm,
/^test-repos\/mkdocs-mkdocs\/docs\/about\/release-notes\.md: \d+: MD053\/.*$\r?\n?/gm,
/^test-repos\/mkdocs-mkdocs\/docs\/dev-guide\/plugins\.md: \d+: MD053\/.*$\r?\n?/gm,
/^test-repos\/mkdocs-mkdocs\/docs\/dev-guide\/themes\.md: \d+: MD053\/.*$\r?\n?/gm,
/^test-repos\/mkdocs-mkdocs\/docs\/dev-guide\/translations\.md: \d+: MD053\/.*$\r?\n?/gm,
/^test-repos\/mkdocs-mkdocs\/docs\/getting-started\.md: \d+: MD053\/.*$\r?\n?/gm,
/^test-repos\/mkdocs-mkdocs\/docs\/user-guide\/configuration\.md: \d+: MD053\/.*$\r?\n?/gm,
/^test-repos\/mkdocs-mkdocs\/docs\/user-guide\/customizing-your-theme\.md: \d+: MD053\/.*$\r?\n?/gm,
/^test-repos\/mkdocs-mkdocs\/docs\/user-guide\/deploying-your-docs\.md: \d+: MD053\/.*$\r?\n?/gm,
/^test-repos\/mkdocs-mkdocs\/docs\/user-guide\/installation\.md: \d+: MD053\/.*$\r?\n?/gm,
/^test-repos\/mkdocs-mkdocs\/docs\/user-guide\/writing-your-docs\.md: \d+: MD053\/.*$\r?\n?/gm
];
return lintTestRepo(t, globPatterns, configPath, ignoreRes);
});
@ -163,7 +179,10 @@ test("https://github.com/mochajs/mocha", (t) => {
join(rootDir, "example/**/*.md")
];
const configPath = join(rootDir, ".markdownlint.json");
const ignoreRes = [ /^[^:]+: \d+: MD051\/.*$\r?\n?/gm ];
const ignoreRes = [
/^[^:]+: \d+: MD051\/.*$\r?\n?/gm,
/^test-repos\/mochajs-mocha\/docs\/index\.md: \d+: MD053\/.*$\r?\n?/gm
];
return lintTestRepo(t, globPatterns, configPath, ignoreRes);
});
@ -171,7 +190,10 @@ test("https://github.com/pi-hole/docs", (t) => {
const rootDir = "./test-repos/pi-hole-docs";
const globPatterns = [ join(rootDir, "**/*.md") ];
const configPath = join(rootDir, ".markdownlint.json");
const ignoreRes = [ /^[^:]+: \d+: MD051\/.*$\r?\n?/gm ];
const ignoreRes = [
/^[^:]+: \d+: MD051\/.*$\r?\n?/gm,
/^test-repos\/pi-hole-docs\/docs\/guides\/dns\/cloudflared\.md: \d+: MD053\/.*$\r?\n?/gm
];
return lintTestRepo(t, globPatterns, configPath, ignoreRes);
});
@ -182,14 +204,38 @@ test("https://github.com/webhintio/hint", (t) => {
...excludeGlobs(rootDir, "**/CHANGELOG.md")
];
const configPath = join(rootDir, ".markdownlintrc");
return lintTestRepo(t, globPatterns, configPath);
const ignoreRes = [
/test-repos\/webhintio-hint\/packages\/hint-apple-touch-icons\/README\.md: \d+: MD053\/.*$\r?\n?/gm,
/test-repos\/webhintio-hint\/packages\/hint-axe\/README\.md: \d+: MD053\/.*$\r?\n?/gm,
/test-repos\/webhintio-hint\/packages\/hint-compat-api\/README\.md: \d+: MD053\/.*$\r?\n?/gm,
/test-repos\/webhintio-hint\/packages\/hint-compat-api\/docs\/html\.md: \d+: MD053\/.*$\r?\n?/gm,
/test-repos\/webhintio-hint\/packages\/hint-doctype\/README\.md: \d+: MD053\/.*$\r?\n?/gm,
/test-repos\/webhintio-hint\/packages\/hint-highest-available-document-mode\/README\.md: \d+: MD053\/.*$\r?\n?/gm,
/test-repos\/webhintio-hint\/packages\/hint-http-compression\/README\.md: \d+: MD053\/.*$\r?\n?/gm,
/test-repos\/webhintio-hint\/packages\/hint-meta-viewport\/README\.md: \d+: MD053\/.*$\r?\n?/gm,
/test-repos\/webhintio-hint\/packages\/hint-minified-js\/README\.md: \d+: MD053\/.*$\r?\n?/gm,
/test-repos\/webhintio-hint\/packages\/hint-no-p3p\/README\.md: \d+: MD053\/.*$\r?\n?/gm,
/test-repos\/webhintio-hint\/packages\/hint-performance-budget\/README\.md: \d+: MD053\/.*$\r?\n?/gm,
/test-repos\/webhintio-hint\/packages\/hint-strict-transport-security\/README\.md: \d+: MD053\/.*$\r?\n?/gm,
/test-repos\/webhintio-hint\/packages\/hint-x-content-type-options\/README\.md: \d+: MD053\/.*$\r?\n?/gm,
/test-repos\/webhintio-hint\/packages\/hint\/docs\/about\/GOVERNANCE.md: \d+: MD053\/.*$\r?\n?/gm,
/test-repos\/webhintio-hint\/packages\/hint\/docs\/contributor-guide\/getting-started\/architecture\.md: \d+: MD053\/.*$\r?\n?/gm,
/test-repos\/webhintio-hint\/packages\/hint\/docs\/contributor-guide\/getting-started\/development-environment\.md: \d+: MD053\/.*$\r?\n?/gm,
/test-repos\/webhintio-hint\/packages\/hint\/docs\/contributor-guide\/how-to\/hint\.md: \d+: MD053\/.*$\r?\n?/gm,
/test-repos\/webhintio-hint\/packages\/hint\/docs\/user-guide\/development-flow-integration\/local-server\.md: \d+: MD053\/.*$\r?\n?/gm,
/test-repos\/webhintio-hint\/packages\/hint\/docs\/user-guide\/index\.md: \d+: MD053\/.*$\r?\n?/gm,
/test-repos\/webhintio-hint\/packages\/hint\/docs\/user-guide\/troubleshoot\/summary\.md: \d+: MD053\/.*$\r?\n?/gm,
/test-repos\/webhintio-hint\/packages\/parser-html\/README\.md: \d+: MD053\/.*$\r?\n?/gm
];
return lintTestRepo(t, globPatterns, configPath, ignoreRes);
});
test("https://github.com/webpack/webpack.js.org", (t) => {
const rootDir = "./test-repos/webpack-webpack-js-org";
const globPatterns = [ join(rootDir, "**/*.md") ];
const configPath = join(rootDir, ".markdownlint.json");
return lintTestRepo(t, globPatterns, configPath);
const ignoreRes = [ /^test-repos\/webpack-webpack-js-org\/README\.md: \d+: MD053\/.*$\r?\n?/gm ];
return lintTestRepo(t, globPatterns, configPath, ignoreRes);
});
// Optional repositories (very large)
@ -200,7 +246,19 @@ if (existsSync(dotnetDocsDir)) {
const rootDir = dotnetDocsDir;
const globPatterns = [ join(rootDir, "**/*.md") ];
const configPath = join(rootDir, ".markdownlint.json");
const ignoreRes = [ /^[^:]+: \d+: MD051\/.*$\r?\n?/gm ];
const ignoreRes = [
/^[^:]+: \d+: MD051\/.*$\r?\n?/gm,
/^test-repos\/dotnet-docs\/docs\/core\/diagnostics\/sos-debugging-extension\.md: \d+: MD052\/.*$\r?\n?/gm,
/^test-repos\/dotnet-docs\/docs\/core\/install\/macos\.md: \d+: MD053\/.*$\r?\n?/gm,
/^test-repos\/dotnet-docs\/docs\/framework\/data\/adonet\/dataset-datatable-dataview\/diffgrams\.md: \d+: MD052\/.*$\r?\n?/gm,
/^test-repos\/dotnet-docs\/docs\/framework\/tools\/al-exe-assembly-linker\.md: \d+: MD052\/.*$\r?\n?/gm,
/^test-repos\/dotnet-docs\/docs\/framework\/tools\/sos-dll-sos-debugging-extension\.md: \d+: MD052\/.*$\r?\n?/gm,
/^test-repos\/dotnet-docs\/docs\/fsharp\/language-reference\/compiler-options\.md: \d+: MD052\/.*$\r?\n?/gm,
/^test-repos\/dotnet-docs\/docs\/fundamentals\/code-analysis\/quality-rules\/ca2241\.md: \d+: MD052\/.*$\r?\n?/gm,
/^test-repos\/dotnet-docs\/docs\/standard\/base-types\/composite-formatting\.md: \d+: MD052\/.*$\r?\n?/gm,
/^test-repos\/dotnet-docs\/docs\/standard\/base-types\/standard-timespan-format-strings\.md: \d+: MD052\/.*$\r?\n?/gm,
/^test-repos\/dotnet-docs\/docs\/standard\/native-interop\/tutorial-comwrappers\.md: \d+: MD053\/.*$\r?\n?/gm
];
return lintTestRepo(t, globPatterns, configPath, ignoreRes);
});
}
@ -211,7 +269,10 @@ if (existsSync(v8v8DevDir)) {
const rootDir = v8v8DevDir;
const globPatterns = [ join(rootDir, "src/**/*.md") ];
const configPath = join(rootDir, ".markdownlint.json");
const ignoreRes = [ /^[^:]+: \d+: (MD049|MD051)\/.*$\r?\n?/gm ];
const ignoreRes = [
/^[^:]+: \d+: (MD049|MD051)\/.*$\r?\n?/gm,
/^test-repos\/v8-v8-dev\/src\/blog\/oilpan-library\.md: \d+: MD053\/.*$\r?\n?/gm
];
return lintTestRepo(t, globPatterns, configPath, ignoreRes);
});
}

View file

@ -488,11 +488,13 @@ test.cb("styleAll", (t) => {
"MD042": [ 81 ],
"MD045": [ 85 ],
"MD046": [ 49, 73, 77 ],
"MD047": [ 98 ],
"MD047": [ 101 ],
"MD048": [ 77 ],
"MD049": [ 90 ],
"MD050": [ 94 ],
"MD051": [ 96 ]
"MD051": [ 96 ],
"MD052": [ 98 ],
"MD053": [ 99 ]
}
};
// @ts-ignore
@ -534,11 +536,13 @@ test.cb("styleRelaxed", (t) => {
"MD042": [ 81 ],
"MD045": [ 85 ],
"MD046": [ 49, 73, 77 ],
"MD047": [ 98 ],
"MD047": [ 101 ],
"MD048": [ 77 ],
"MD049": [ 90 ],
"MD050": [ 94 ],
"MD051": [ 96 ]
"MD051": [ 96 ],
"MD052": [ 98 ],
"MD053": [ 99 ]
}
};
// @ts-ignore
@ -840,7 +844,7 @@ test.cb("customFileSystemAsync", (t) => {
});
test.cb("readme", (t) => {
t.plan(121);
t.plan(125);
const tagToRules = {};
rules.forEach(function forRule(rule) {
rule.tags.forEach(function forTag(tag) {
@ -916,7 +920,7 @@ test.cb("readme", (t) => {
});
test.cb("rules", (t) => {
t.plan(359);
t.plan(373);
fs.readFile("doc/Rules.md", "utf8",
(err, contents) => {
t.falsy(err);
@ -1093,7 +1097,7 @@ test("validateConfigExampleJson", async(t) => {
});
test("allBuiltInRulesHaveValidUrl", (t) => {
t.plan(141);
t.plan(147);
rules.forEach(function forRule(rule) {
t.truthy(rule.information);
t.true(Object.getPrototypeOf(rule.information) === URL.prototype);

View file

@ -0,0 +1,164 @@
# Reference Links and Images
## Valid Links
Full reference link: [text][label]
Collapsed reference link: [label][]
Shortcut reference link: [label]
Same line: [text][label] [label][] [label]
Mixed case: [TEXT][LABEL] [LABEL][] [LABEL]
With spaces: [text][label with spaces] [text][ label with spaces ]
With nested brackets: [t[ex]t][label]
With inline content: [*text*][label]
With inline code span: [`code`][label]
Shortcut inline code span: [`code`]
Multi-line full text: [multi
line][multi line full text]
Multi-line full label: [text][multi
line full label]
Multi-line collapsed label: [multi
line collapsed label][]
Multi-line shortcut label: [multi line
shortcut label]
Dedicated line:
[text][label]
Dedicated line with trailing colon:
[text][label]:
Shortcut ending in colon: [colon]:
Use of multi-line label: [multi-line-label][]
Standard link: [text](https://example.com/standard)
## Invalid Links
Missing label: [text][missing] {MD052}
Mixed valid/invalid: [text][label] [text][missing] {MD052}
Missing multi-line label {MD052}: [text][missing
label]
## Non-Links
Space: [text] [wrong]
Empty: [text][ ]
Code span: `[text][wrong]`
Escaped left text: \[text][wrong]
Escaped right text: [text\][wrong]
Escaped left label: [text]\[wrong]
Escaped right label: [text][wrong\]
## Valid Images
Full style: ![text][image]
Collapsed style: ![image][]
Shortcut style: ![image]
Image in link: [![text][image]][label] [![image][]][label] [![image]][label]
## Invalid Images
Image only: ![text][missing] {MD052}
Image in link: [![text][missing]][label] {MD052}
## Non-Images
Escaped left text: !\[text][wrong]
Escaped right text: ![text\][wrong]
Escaped left label: ![text]\[wrong]
Escaped right label: ![text][wrong\]
## Valid Footnotes
Footnote[^1]
## Invalid Footnotes
Missing[^2]
## Valid Labels
[label]: https://example.com/label
[ label with spaces ]: https://example.com/label-with-spaces
[image]:https://example.com/image
[`code`]: https://example.com/code
[^1]: https://example.com/footnote
[multi line full text]: https://example.com/multi-line-full-text
[multi line full label]: https://example.com/multi-line-full-label
[multi line collapsed label]: https://example.com/multi-line-collapsed-label
[multi line shortcut label]: https://example.com/multi-line-shortcut-label
[colon]: https://example.com/colon
[multi-line-label]:
https://example.com/multi-line-label
## Invalid Labels
Duplicate:
[label]: {MD053}
Unused:
[unused]: {MD053}
Unused footnote:
[^3]: {MD053}
[Duplicate unused multi-line label {MD053}]:
https://example.com/duplicate-unused-multi-line-label
[Duplicate unused multi-line label {MD053}]:
https://example.com/duplicate-unused-multi-line-label
\[Escaped left]: text
[Escaped right\]: text
## Valid Links and Images after Labels
Link and image: [text][label] [![text][image]][label]
## More Invalid Links and Images after Labels
Bad link with image [![text][image]][missing] {MD052}
## Shortcut One-Way Handling
Validates the label: [shortcut]
[shortcut]: https://example.com/shortcut
Not flagged due to ambiguity: [ignored]
## Open Bracket Pairs
Unmatched [ in text
Hidden reference: [hidden][] {MD052}

View file

@ -5750,7 +5750,7 @@ Generated by [AVA](https://avajs.dev).
insertText: `␊
`,
},
lineNumber: 98,
lineNumber: 101,
ruleDescription: 'Files should end with a single newline character',
ruleInformation: 'https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/Rules.md#md047',
ruleNames: [
@ -5827,6 +5827,40 @@ Generated by [AVA](https://avajs.dev).
'link-fragments',
],
},
{
errorContext: '[Missing link][label]',
errorDetail: 'Missing link or image reference definition: "label"',
errorRange: [
1,
21,
],
fixInfo: null,
lineNumber: 98,
ruleDescription: 'Reference links and images should use a label that is defined',
ruleInformation: 'https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/Rules.md#md052',
ruleNames: [
'MD052',
'reference-links-images',
],
},
{
errorContext: '[unused]: link-destination {MD...',
errorDetail: 'Unused link or image reference definition: "unused"',
errorRange: [
1,
34,
],
fixInfo: {
deleteCount: -1,
},
lineNumber: 99,
ruleDescription: 'Link and image reference definitions should be needed',
ruleInformation: 'https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/Rules.md#md053',
ruleNames: [
'MD053',
'link-image-reference-definitions',
],
},
],
fixed: `## Heading 1 {MD002} {MD041}␊
@ -5927,6 +5961,8 @@ Generated by [AVA](https://avajs.dev).
[Missing link fragment](#missing) {MD051}␊
[Missing link][label] {MD052}␊
EOF {MD047}␊
`,
}
@ -27459,6 +27495,22 @@ Generated by [AVA](https://avajs.dev).
'strong-style',
],
},
{
errorContext: '[Link to broken label][notlabel]',
errorDetail: 'Missing link or image reference definition: "notlabel"',
errorRange: [
1,
32,
],
fixInfo: null,
lineNumber: 27,
ruleDescription: 'Reference links and images should use a label that is defined',
ruleInformation: 'https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/Rules.md#md052',
ruleNames: [
'MD052',
'reference-links-images',
],
},
],
fixed: `# Long Lines␊
@ -27486,7 +27538,7 @@ Generated by [AVA](https://avajs.dev).
[label]: https://example.org "Title for a link reference that is itself long enough to violate the rule"␊
[Link to broken label][notlabel]␊
[Link to broken label][notlabel] {MD052}
[notlabel\\]: notlink "Invalid syntax for a link label because the right bracket is backslash-escaped {MD013}"␊
@ -33027,6 +33079,375 @@ Generated by [AVA](https://avajs.dev).
`,
}
## reference-links-and-images.md
> Snapshot 1
{
errors: [
{
errorContext: '[text][missing]',
errorDetail: 'Missing link or image reference definition: "missing"',
errorRange: [
16,
15,
],
fixInfo: null,
lineNumber: 51,
ruleDescription: 'Reference links and images should use a label that is defined',
ruleInformation: 'https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/Rules.md#md052',
ruleNames: [
'MD052',
'reference-links-images',
],
},
{
errorContext: '[text][missing]',
errorDetail: 'Missing link or image reference definition: "missing"',
errorRange: [
36,
15,
],
fixInfo: null,
lineNumber: 53,
ruleDescription: 'Reference links and images should use a label that is defined',
ruleInformation: 'https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/Rules.md#md052',
ruleNames: [
'MD052',
'reference-links-images',
],
},
{
errorContext: '[text][missing',
errorDetail: 'Missing link or image reference definition: "missing label"',
errorRange: [
35,
14,
],
fixInfo: null,
lineNumber: 55,
ruleDescription: 'Reference links and images should use a label that is defined',
ruleInformation: 'https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/Rules.md#md052',
ruleNames: [
'MD052',
'reference-links-images',
],
},
{
errorContext: '![text][missing]',
errorDetail: 'Missing link or image reference definition: "missing"',
errorRange: [
13,
16,
],
fixInfo: null,
lineNumber: 86,
ruleDescription: 'Reference links and images should use a label that is defined',
ruleInformation: 'https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/Rules.md#md052',
ruleNames: [
'MD052',
'reference-links-images',
],
},
{
errorContext: '![text][missing]',
errorDetail: 'Missing link or image reference definition: "missing"',
errorRange: [
17,
16,
],
fixInfo: null,
lineNumber: 88,
ruleDescription: 'Reference links and images should use a label that is defined',
ruleInformation: 'https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/Rules.md#md052',
ruleNames: [
'MD052',
'reference-links-images',
],
},
{
errorContext: '[![text][image]][missing]',
errorDetail: 'Missing link or image reference definition: "missing"',
errorRange: [
21,
25,
],
fixInfo: null,
lineNumber: 150,
ruleDescription: 'Reference links and images should use a label that is defined',
ruleInformation: 'https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/Rules.md#md052',
ruleNames: [
'MD052',
'reference-links-images',
],
},
{
errorContext: '[hidden][]',
errorDetail: 'Missing link or image reference definition: "hidden"',
errorRange: [
19,
10,
],
fixInfo: null,
lineNumber: 164,
ruleDescription: 'Reference links and images should use a label that is defined',
ruleInformation: 'https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/Rules.md#md052',
ruleNames: [
'MD052',
'reference-links-images',
],
},
{
errorContext: '[label]: {MD053}',
errorDetail: 'Duplicate link or image reference definition: "label"',
errorRange: [
1,
16,
],
fixInfo: {
deleteCount: -1,
},
lineNumber: 126,
ruleDescription: 'Link and image reference definitions should be needed',
ruleInformation: 'https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/Rules.md#md053',
ruleNames: [
'MD053',
'link-image-reference-definitions',
],
},
{
errorContext: '[unused]: {MD053}',
errorDetail: 'Unused link or image reference definition: "unused"',
errorRange: [
1,
17,
],
fixInfo: {
deleteCount: -1,
},
lineNumber: 129,
ruleDescription: 'Link and image reference definitions should be needed',
ruleInformation: 'https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/Rules.md#md053',
ruleNames: [
'MD053',
'link-image-reference-definitions',
],
},
{
errorContext: '[^3]: {MD053}',
errorDetail: 'Unused link or image reference definition: "^3"',
errorRange: [
1,
13,
],
fixInfo: {
deleteCount: -1,
},
lineNumber: 132,
ruleDescription: 'Link and image reference definitions should be needed',
ruleInformation: 'https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/Rules.md#md053',
ruleNames: [
'MD053',
'link-image-reference-definitions',
],
},
{
errorContext: '[Duplicate unused multi-line l...',
errorDetail: 'Unused link or image reference definition: "duplicate unused multi-line label {md053}"',
errorRange: [
1,
44,
],
fixInfo: null,
lineNumber: 134,
ruleDescription: 'Link and image reference definitions should be needed',
ruleInformation: 'https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/Rules.md#md053',
ruleNames: [
'MD053',
'link-image-reference-definitions',
],
},
{
errorContext: '[Duplicate unused multi-line l...',
errorDetail: 'Duplicate link or image reference definition: "duplicate unused multi-line label {md053}"',
errorRange: [
1,
44,
],
fixInfo: null,
lineNumber: 137,
ruleDescription: 'Link and image reference definitions should be needed',
ruleInformation: 'https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/Rules.md#md053',
ruleNames: [
'MD053',
'link-image-reference-definitions',
],
},
],
fixed: `# Reference Links and Images␊
## Valid Links␊
Full reference link: [text][label]␊
Collapsed reference link: [label][]␊
Shortcut reference link: [label]␊
Same line: [text][label] [label][] [label]␊
Mixed case: [TEXT][LABEL] [LABEL][] [LABEL]␊
With spaces: [text][label with spaces] [text][ label with spaces ]␊
With nested brackets: [t[ex]t][label]␊
With inline content: [*text*][label]␊
With inline code span: [\`code\`][label]␊
Shortcut inline code span: [\`code\`]␊
Multi-line full text: [multi␊
line][multi line full text]␊
Multi-line full label: [text][multi␊
line full label]␊
Multi-line collapsed label: [multi␊
line collapsed label][]␊
Multi-line shortcut label: [multi line␊
shortcut label]␊
Dedicated line:␊
[text][label]␊
Dedicated line with trailing colon:␊
[text][label]:␊
Shortcut ending in colon: [colon]:␊
Use of multi-line label: [multi-line-label][]␊
Standard link: [text](https://example.com/standard)␊
## Invalid Links␊
Missing label: [text][missing] {MD052}␊
Mixed valid/invalid: [text][label] [text][missing] {MD052}␊
Missing multi-line label {MD052}: [text][missing␊
label]␊
## Non-Links␊
Space: [text] [wrong]␊
Empty: [text][ ]␊
Code span: \`[text][wrong]\`␊
Escaped left text: \\[text][wrong]␊
Escaped right text: [text\\][wrong]␊
Escaped left label: [text]\\[wrong]␊
Escaped right label: [text][wrong\\]␊
## Valid Images␊
Full style: ![text][image]␊
Collapsed style: ![image][]␊
Shortcut style: ![image]␊
Image in link: [![text][image]][label] [![image][]][label] [![image]][label]␊
## Invalid Images␊
Image only: ![text][missing] {MD052}␊
Image in link: [![text][missing]][label] {MD052}␊
## Non-Images␊
Escaped left text: !\\[text][wrong]␊
Escaped right text: ![text\\][wrong]␊
Escaped left label: ![text]\\[wrong]␊
Escaped right label: ![text][wrong\\]␊
## Valid Footnotes␊
Footnote[^1]␊
## Invalid Footnotes␊
Missing[^2]␊
## Valid Labels␊
[label]: https://example.com/label␊
[ label with spaces ]: https://example.com/label-with-spaces␊
[image]:https://example.com/image␊
[\`code\`]: https://example.com/code␊
[^1]: https://example.com/footnote␊
[multi line full text]: https://example.com/multi-line-full-text␊
[multi line full label]: https://example.com/multi-line-full-label␊
[multi line collapsed label]: https://example.com/multi-line-collapsed-label␊
[multi line shortcut label]: https://example.com/multi-line-shortcut-label␊
[colon]: https://example.com/colon␊
[multi-line-label]:␊
https://example.com/multi-line-label␊
## Invalid Labels␊
Duplicate:␊
Unused:␊
Unused footnote:␊
[Duplicate unused multi-line label {MD053}]:␊
https://example.com/duplicate-unused-multi-line-label␊
[Duplicate unused multi-line label {MD053}]:␊
https://example.com/duplicate-unused-multi-line-label␊
\\[Escaped left]: text␊
[Escaped right\\]: text␊
## Valid Links and Images after Labels␊
Link and image: [text][label] [![text][image]][label]␊
## More Invalid Links and Images after Labels␊
Bad link with image [![text][image]][missing] {MD052}␊
## Shortcut One-Way Handling␊
Validates the label: [shortcut]␊
[shortcut]: https://example.com/shortcut␊
Not flagged due to ambiguity: [ignored]␊
## Open Bracket Pairs␊
Unmatched [ in text␊
Hidden reference: [hidden][] {MD052}␊
`,
}
## required-headings-all-optional-at-least-one.md
> Snapshot 1