mirror of
https://github.com/DavidAnson/markdownlint.git
synced 2025-09-22 05:40:48 +02:00
Reimplement MD042/no-empty-links using micromark tokens.
This commit is contained in:
parent
7377b75383
commit
964e4c80ab
6 changed files with 219 additions and 97 deletions
|
@ -1523,6 +1523,21 @@ function filterByTypes(tokens, types, htmlFlow) {
|
||||||
return filterByPredicate(tokens, predicate);
|
return filterByPredicate(tokens, predicate);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a list of nested Micromark token descendants by type path.
|
||||||
|
*
|
||||||
|
* @param {Token|Token[]} parent Micromark token parent or parents.
|
||||||
|
* @param {TokenType[]} typePath Micromark token type path.
|
||||||
|
* @returns {Token[]} Micromark token descendants.
|
||||||
|
*/
|
||||||
|
function getDescendantsByType(parent, typePath) {
|
||||||
|
let tokens = Array.isArray(parent) ? parent : [ parent ];
|
||||||
|
for (const type of typePath) {
|
||||||
|
tokens = tokens.flatMap((t) => t.children).filter((t) => t.type === type);
|
||||||
|
}
|
||||||
|
return tokens;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the heading level of a Micromark heading tokan.
|
* Gets the heading level of a Micromark heading tokan.
|
||||||
*
|
*
|
||||||
|
@ -1691,6 +1706,7 @@ module.exports = {
|
||||||
"parse": micromarkParse,
|
"parse": micromarkParse,
|
||||||
filterByPredicate,
|
filterByPredicate,
|
||||||
filterByTypes,
|
filterByTypes,
|
||||||
|
getDescendantsByType,
|
||||||
getHeadingLevel,
|
getHeadingLevel,
|
||||||
getHeadingStyle,
|
getHeadingStyle,
|
||||||
getHeadingText,
|
getHeadingText,
|
||||||
|
@ -5813,8 +5829,9 @@ module.exports = {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const { addErrorContext, escapeForRegExp, filterTokens } =
|
const { addErrorContext } = __webpack_require__(/*! ../helpers */ "../helpers/helpers.js");
|
||||||
__webpack_require__(/*! ../helpers */ "../helpers/helpers.js");
|
const { getDescendantsByType, filterByTypes } = __webpack_require__(/*! ../helpers/micromark.cjs */ "../helpers/micromark.cjs");
|
||||||
|
const { referenceLinkImageData } = __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 */
|
||||||
|
@ -5822,44 +5839,56 @@ module.exports = {
|
||||||
"names": [ "MD042", "no-empty-links" ],
|
"names": [ "MD042", "no-empty-links" ],
|
||||||
"description": "No empty links",
|
"description": "No empty links",
|
||||||
"tags": [ "links" ],
|
"tags": [ "links" ],
|
||||||
"parser": "markdownit",
|
"parser": "micromark",
|
||||||
"function": function MD042(params, onError) {
|
"function": function MD042(params, onError) {
|
||||||
filterTokens(params, "inline", function forToken(token) {
|
const { definitions } = referenceLinkImageData();
|
||||||
let inLink = false;
|
const isReferenceDefinitionHash = (token) => {
|
||||||
let linkText = "";
|
const definition = definitions.get(token.text.trim());
|
||||||
let emptyLink = false;
|
return (definition && (definition[1] === "#"));
|
||||||
for (const child of token.children) {
|
};
|
||||||
if (child.type === "link_open") {
|
const links = filterByTypes(
|
||||||
inLink = true;
|
params.parsers.micromark.tokens,
|
||||||
linkText = "";
|
[ "link" ]
|
||||||
for (const attr of child.attrs) {
|
);
|
||||||
if (attr[0] === "href" && (!attr[1] || (attr[1] === "#"))) {
|
for (const link of links) {
|
||||||
emptyLink = true;
|
const labelText = getDescendantsByType(link, [ "label", "labelText" ]);
|
||||||
}
|
const reference = getDescendantsByType(link, [ "reference" ]);
|
||||||
}
|
const resource = getDescendantsByType(link, [ "resource" ]);
|
||||||
} else if (child.type === "link_close") {
|
const referenceString = getDescendantsByType(reference, [ "referenceString" ]);
|
||||||
inLink = false;
|
const resourceDestination = getDescendantsByType(resource, [ "resourceDestination" ]);
|
||||||
if (emptyLink) {
|
const resourceDestinationString = [
|
||||||
let context = `[${linkText}]`;
|
...getDescendantsByType(resourceDestination, [ "resourceDestinationRaw", "resourceDestinationString" ]),
|
||||||
let range = null;
|
...getDescendantsByType(resourceDestination, [ "resourceDestinationLiteral", "resourceDestinationString" ])
|
||||||
const match = child.line.match(
|
];
|
||||||
new RegExp(`${escapeForRegExp(context)}\\((?:|#|<>)\\)`)
|
const hasLabelText = labelText.length > 0;
|
||||||
);
|
const hasReference = reference.length > 0;
|
||||||
if (match) {
|
const hasResource = resource.length > 0;
|
||||||
context = match[0];
|
const hasReferenceString = referenceString.length > 0;
|
||||||
// @ts-ignore
|
const hasResourceDestinationString = resourceDestinationString.length > 0;
|
||||||
range = [ match.index + 1, match[0].length ];
|
let error = false;
|
||||||
}
|
if (
|
||||||
addErrorContext(
|
hasLabelText &&
|
||||||
onError, child.lineNumber, context, null, null, range
|
((!hasReference && !hasResource) || (hasReference && !hasReferenceString))
|
||||||
);
|
) {
|
||||||
emptyLink = false;
|
error = isReferenceDefinitionHash(labelText[0]);
|
||||||
}
|
} else if (hasReferenceString && !hasResourceDestinationString) {
|
||||||
} else if (inLink) {
|
error = isReferenceDefinitionHash(referenceString[0]);
|
||||||
linkText += child.content;
|
} else if (!hasReferenceString && hasResourceDestinationString) {
|
||||||
}
|
error = (resourceDestinationString[0].text.trim() === "#");
|
||||||
|
} else if (!hasReferenceString && !hasResourceDestinationString) {
|
||||||
|
error = true;
|
||||||
}
|
}
|
||||||
});
|
if (error) {
|
||||||
|
addErrorContext(
|
||||||
|
onError,
|
||||||
|
link.startLine,
|
||||||
|
link.text,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
[ link.startColumn, link.endColumn - link.startColumn ]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -304,6 +304,21 @@ function filterByTypes(tokens, types, htmlFlow) {
|
||||||
return filterByPredicate(tokens, predicate);
|
return filterByPredicate(tokens, predicate);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a list of nested Micromark token descendants by type path.
|
||||||
|
*
|
||||||
|
* @param {Token|Token[]} parent Micromark token parent or parents.
|
||||||
|
* @param {TokenType[]} typePath Micromark token type path.
|
||||||
|
* @returns {Token[]} Micromark token descendants.
|
||||||
|
*/
|
||||||
|
function getDescendantsByType(parent, typePath) {
|
||||||
|
let tokens = Array.isArray(parent) ? parent : [ parent ];
|
||||||
|
for (const type of typePath) {
|
||||||
|
tokens = tokens.flatMap((t) => t.children).filter((t) => t.type === type);
|
||||||
|
}
|
||||||
|
return tokens;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the heading level of a Micromark heading tokan.
|
* Gets the heading level of a Micromark heading tokan.
|
||||||
*
|
*
|
||||||
|
@ -472,6 +487,7 @@ module.exports = {
|
||||||
"parse": micromarkParse,
|
"parse": micromarkParse,
|
||||||
filterByPredicate,
|
filterByPredicate,
|
||||||
filterByTypes,
|
filterByTypes,
|
||||||
|
getDescendantsByType,
|
||||||
getHeadingLevel,
|
getHeadingLevel,
|
||||||
getHeadingStyle,
|
getHeadingStyle,
|
||||||
getHeadingText,
|
getHeadingText,
|
||||||
|
|
89
lib/md042.js
89
lib/md042.js
|
@ -2,8 +2,9 @@
|
||||||
|
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
const { addErrorContext, escapeForRegExp, filterTokens } =
|
const { addErrorContext } = require("../helpers");
|
||||||
require("../helpers");
|
const { getDescendantsByType, filterByTypes } = require("../helpers/micromark.cjs");
|
||||||
|
const { referenceLinkImageData } = require("./cache");
|
||||||
|
|
||||||
// eslint-disable-next-line jsdoc/valid-types
|
// eslint-disable-next-line jsdoc/valid-types
|
||||||
/** @type import("./markdownlint").Rule */
|
/** @type import("./markdownlint").Rule */
|
||||||
|
@ -11,43 +12,55 @@ module.exports = {
|
||||||
"names": [ "MD042", "no-empty-links" ],
|
"names": [ "MD042", "no-empty-links" ],
|
||||||
"description": "No empty links",
|
"description": "No empty links",
|
||||||
"tags": [ "links" ],
|
"tags": [ "links" ],
|
||||||
"parser": "markdownit",
|
"parser": "micromark",
|
||||||
"function": function MD042(params, onError) {
|
"function": function MD042(params, onError) {
|
||||||
filterTokens(params, "inline", function forToken(token) {
|
const { definitions } = referenceLinkImageData();
|
||||||
let inLink = false;
|
const isReferenceDefinitionHash = (token) => {
|
||||||
let linkText = "";
|
const definition = definitions.get(token.text.trim());
|
||||||
let emptyLink = false;
|
return (definition && (definition[1] === "#"));
|
||||||
for (const child of token.children) {
|
};
|
||||||
if (child.type === "link_open") {
|
const links = filterByTypes(
|
||||||
inLink = true;
|
params.parsers.micromark.tokens,
|
||||||
linkText = "";
|
[ "link" ]
|
||||||
for (const attr of child.attrs) {
|
);
|
||||||
if (attr[0] === "href" && (!attr[1] || (attr[1] === "#"))) {
|
for (const link of links) {
|
||||||
emptyLink = true;
|
const labelText = getDescendantsByType(link, [ "label", "labelText" ]);
|
||||||
}
|
const reference = getDescendantsByType(link, [ "reference" ]);
|
||||||
}
|
const resource = getDescendantsByType(link, [ "resource" ]);
|
||||||
} else if (child.type === "link_close") {
|
const referenceString = getDescendantsByType(reference, [ "referenceString" ]);
|
||||||
inLink = false;
|
const resourceDestination = getDescendantsByType(resource, [ "resourceDestination" ]);
|
||||||
if (emptyLink) {
|
const resourceDestinationString = [
|
||||||
let context = `[${linkText}]`;
|
...getDescendantsByType(resourceDestination, [ "resourceDestinationRaw", "resourceDestinationString" ]),
|
||||||
let range = null;
|
...getDescendantsByType(resourceDestination, [ "resourceDestinationLiteral", "resourceDestinationString" ])
|
||||||
const match = child.line.match(
|
];
|
||||||
new RegExp(`${escapeForRegExp(context)}\\((?:|#|<>)\\)`)
|
const hasLabelText = labelText.length > 0;
|
||||||
);
|
const hasReference = reference.length > 0;
|
||||||
if (match) {
|
const hasResource = resource.length > 0;
|
||||||
context = match[0];
|
const hasReferenceString = referenceString.length > 0;
|
||||||
// @ts-ignore
|
const hasResourceDestinationString = resourceDestinationString.length > 0;
|
||||||
range = [ match.index + 1, match[0].length ];
|
let error = false;
|
||||||
}
|
if (
|
||||||
addErrorContext(
|
hasLabelText &&
|
||||||
onError, child.lineNumber, context, null, null, range
|
((!hasReference && !hasResource) || (hasReference && !hasReferenceString))
|
||||||
);
|
) {
|
||||||
emptyLink = false;
|
error = isReferenceDefinitionHash(labelText[0]);
|
||||||
}
|
} else if (hasReferenceString && !hasResourceDestinationString) {
|
||||||
} else if (inLink) {
|
error = isReferenceDefinitionHash(referenceString[0]);
|
||||||
linkText += child.content;
|
} else if (!hasReferenceString && hasResourceDestinationString) {
|
||||||
}
|
error = (resourceDestinationString[0].text.trim() === "#");
|
||||||
|
} else if (!hasReferenceString && !hasResourceDestinationString) {
|
||||||
|
error = true;
|
||||||
}
|
}
|
||||||
});
|
if (error) {
|
||||||
|
addErrorContext(
|
||||||
|
onError,
|
||||||
|
link.startLine,
|
||||||
|
link.text,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
[ link.startColumn, link.endColumn - link.startColumn ]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -24,6 +24,10 @@
|
||||||
|
|
||||||
[text][ frag ] {MD042}
|
[text][ frag ] {MD042}
|
||||||
|
|
||||||
|
[frag][] {MD042}
|
||||||
|
|
||||||
|
[frag] {MD042}
|
||||||
|
|
||||||
[frag]: #
|
[frag]: #
|
||||||
|
|
||||||
## Non-empty links
|
## Non-empty links
|
||||||
|
|
|
@ -11837,9 +11837,12 @@ Generated by [AVA](https://avajs.dev).
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
errorContext: '[text]',
|
errorContext: '[text]( <> )',
|
||||||
errorDetail: null,
|
errorDetail: null,
|
||||||
errorRange: null,
|
errorRange: [
|
||||||
|
1,
|
||||||
|
12,
|
||||||
|
],
|
||||||
fixInfo: null,
|
fixInfo: null,
|
||||||
lineNumber: 9,
|
lineNumber: 9,
|
||||||
ruleDescription: 'No empty links',
|
ruleDescription: 'No empty links',
|
||||||
|
@ -11850,9 +11853,12 @@ Generated by [AVA](https://avajs.dev).
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
errorContext: '[text]',
|
errorContext: '[text](<> "title")',
|
||||||
errorDetail: null,
|
errorDetail: null,
|
||||||
errorRange: null,
|
errorRange: [
|
||||||
|
1,
|
||||||
|
18,
|
||||||
|
],
|
||||||
fixInfo: null,
|
fixInfo: null,
|
||||||
lineNumber: 11,
|
lineNumber: 11,
|
||||||
ruleDescription: 'No empty links',
|
ruleDescription: 'No empty links',
|
||||||
|
@ -11863,9 +11869,12 @@ Generated by [AVA](https://avajs.dev).
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
errorContext: '[text]',
|
errorContext: '[text]( <> "title" )',
|
||||||
errorDetail: null,
|
errorDetail: null,
|
||||||
errorRange: null,
|
errorRange: [
|
||||||
|
1,
|
||||||
|
20,
|
||||||
|
],
|
||||||
fixInfo: null,
|
fixInfo: null,
|
||||||
lineNumber: 13,
|
lineNumber: 13,
|
||||||
ruleDescription: 'No empty links',
|
ruleDescription: 'No empty links',
|
||||||
|
@ -11892,9 +11901,12 @@ Generated by [AVA](https://avajs.dev).
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
errorContext: '[text]',
|
errorContext: '[text]( # )',
|
||||||
errorDetail: null,
|
errorDetail: null,
|
||||||
errorRange: null,
|
errorRange: [
|
||||||
|
1,
|
||||||
|
11,
|
||||||
|
],
|
||||||
fixInfo: null,
|
fixInfo: null,
|
||||||
lineNumber: 17,
|
lineNumber: 17,
|
||||||
ruleDescription: 'No empty links',
|
ruleDescription: 'No empty links',
|
||||||
|
@ -11905,9 +11917,12 @@ Generated by [AVA](https://avajs.dev).
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
errorContext: '[text]',
|
errorContext: '[text](# "title")',
|
||||||
errorDetail: null,
|
errorDetail: null,
|
||||||
errorRange: null,
|
errorRange: [
|
||||||
|
1,
|
||||||
|
17,
|
||||||
|
],
|
||||||
fixInfo: null,
|
fixInfo: null,
|
||||||
lineNumber: 19,
|
lineNumber: 19,
|
||||||
ruleDescription: 'No empty links',
|
ruleDescription: 'No empty links',
|
||||||
|
@ -11918,9 +11933,12 @@ Generated by [AVA](https://avajs.dev).
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
errorContext: '[text]',
|
errorContext: '[text]( # "title" )',
|
||||||
errorDetail: null,
|
errorDetail: null,
|
||||||
errorRange: null,
|
errorRange: [
|
||||||
|
1,
|
||||||
|
19,
|
||||||
|
],
|
||||||
fixInfo: null,
|
fixInfo: null,
|
||||||
lineNumber: 21,
|
lineNumber: 21,
|
||||||
ruleDescription: 'No empty links',
|
ruleDescription: 'No empty links',
|
||||||
|
@ -11931,9 +11949,12 @@ Generated by [AVA](https://avajs.dev).
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
errorContext: '[text]',
|
errorContext: '[text][frag]',
|
||||||
errorDetail: null,
|
errorDetail: null,
|
||||||
errorRange: null,
|
errorRange: [
|
||||||
|
1,
|
||||||
|
12,
|
||||||
|
],
|
||||||
fixInfo: null,
|
fixInfo: null,
|
||||||
lineNumber: 23,
|
lineNumber: 23,
|
||||||
ruleDescription: 'No empty links',
|
ruleDescription: 'No empty links',
|
||||||
|
@ -11944,9 +11965,12 @@ Generated by [AVA](https://avajs.dev).
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
errorContext: '[text]',
|
errorContext: '[text][ frag ]',
|
||||||
errorDetail: null,
|
errorDetail: null,
|
||||||
errorRange: null,
|
errorRange: [
|
||||||
|
1,
|
||||||
|
14,
|
||||||
|
],
|
||||||
fixInfo: null,
|
fixInfo: null,
|
||||||
lineNumber: 25,
|
lineNumber: 25,
|
||||||
ruleDescription: 'No empty links',
|
ruleDescription: 'No empty links',
|
||||||
|
@ -11957,14 +11981,30 @@ Generated by [AVA](https://avajs.dev).
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
errorContext: '[text]()',
|
errorContext: '[frag][]',
|
||||||
errorDetail: null,
|
errorDetail: null,
|
||||||
errorRange: [
|
errorRange: [
|
||||||
1,
|
1,
|
||||||
8,
|
8,
|
||||||
],
|
],
|
||||||
fixInfo: null,
|
fixInfo: null,
|
||||||
lineNumber: 70,
|
lineNumber: 27,
|
||||||
|
ruleDescription: 'No empty links',
|
||||||
|
ruleInformation: 'https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/md042.md',
|
||||||
|
ruleNames: [
|
||||||
|
'MD042',
|
||||||
|
'no-empty-links',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
errorContext: '[frag]',
|
||||||
|
errorDetail: null,
|
||||||
|
errorRange: [
|
||||||
|
1,
|
||||||
|
6,
|
||||||
|
],
|
||||||
|
fixInfo: null,
|
||||||
|
lineNumber: 29,
|
||||||
ruleDescription: 'No empty links',
|
ruleDescription: 'No empty links',
|
||||||
ruleInformation: 'https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/md042.md',
|
ruleInformation: 'https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/md042.md',
|
||||||
ruleNames: [
|
ruleNames: [
|
||||||
|
@ -11996,7 +12036,7 @@ Generated by [AVA](https://avajs.dev).
|
||||||
8,
|
8,
|
||||||
],
|
],
|
||||||
fixInfo: null,
|
fixInfo: null,
|
||||||
lineNumber: 76,
|
lineNumber: 78,
|
||||||
ruleDescription: 'No empty links',
|
ruleDescription: 'No empty links',
|
||||||
ruleInformation: 'https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/md042.md',
|
ruleInformation: 'https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/md042.md',
|
||||||
ruleNames: [
|
ruleNames: [
|
||||||
|
@ -12012,7 +12052,7 @@ Generated by [AVA](https://avajs.dev).
|
||||||
8,
|
8,
|
||||||
],
|
],
|
||||||
fixInfo: null,
|
fixInfo: null,
|
||||||
lineNumber: 79,
|
lineNumber: 80,
|
||||||
ruleDescription: 'No empty links',
|
ruleDescription: 'No empty links',
|
||||||
ruleInformation: 'https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/md042.md',
|
ruleInformation: 'https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/md042.md',
|
||||||
ruleNames: [
|
ruleNames: [
|
||||||
|
@ -12028,7 +12068,23 @@ Generated by [AVA](https://avajs.dev).
|
||||||
8,
|
8,
|
||||||
],
|
],
|
||||||
fixInfo: null,
|
fixInfo: null,
|
||||||
lineNumber: 81,
|
lineNumber: 83,
|
||||||
|
ruleDescription: 'No empty links',
|
||||||
|
ruleInformation: 'https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/md042.md',
|
||||||
|
ruleNames: [
|
||||||
|
'MD042',
|
||||||
|
'no-empty-links',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
errorContext: '[text]()',
|
||||||
|
errorDetail: null,
|
||||||
|
errorRange: [
|
||||||
|
1,
|
||||||
|
8,
|
||||||
|
],
|
||||||
|
fixInfo: null,
|
||||||
|
lineNumber: 85,
|
||||||
ruleDescription: 'No empty links',
|
ruleDescription: 'No empty links',
|
||||||
ruleInformation: 'https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/md042.md',
|
ruleInformation: 'https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/md042.md',
|
||||||
ruleNames: [
|
ruleNames: [
|
||||||
|
@ -12063,6 +12119,10 @@ Generated by [AVA](https://avajs.dev).
|
||||||
␊
|
␊
|
||||||
[text][ frag ] {MD042}␊
|
[text][ frag ] {MD042}␊
|
||||||
␊
|
␊
|
||||||
|
[frag][] {MD042}␊
|
||||||
|
␊
|
||||||
|
[frag] {MD042}␊
|
||||||
|
␊
|
||||||
[frag]: #␊
|
[frag]: #␊
|
||||||
␊
|
␊
|
||||||
## Non-empty links␊
|
## Non-empty links␊
|
||||||
|
|
Binary file not shown.
Loading…
Add table
Add a link
Reference in a new issue