-
Notifications
You must be signed in to change notification settings - Fork 63
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
7 changed files
with
295 additions
and
28 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
# no-invalid-label-refs | ||
|
||
Disallow invalid label references. | ||
|
||
## Background | ||
|
||
CommonMark allows you to specify a label as a placeholder for a URL in both links and images using square brackets, such as: | ||
|
||
```markdown | ||
[ESLint][eslint] | ||
[eslint][] | ||
[eslint] | ||
|
||
[eslint]: https://eslint.org | ||
``` | ||
|
||
The shorthand form, `[label][]` does not allow any white space between the brackets, and when found, doesn't treat this as a link reference. | ||
|
||
Confusingly, GitHub still treats this as a label reference and will render it as if there is no white space between the brackets. Relying on this behavior could result in errors when using CommonMark-compliant renderers. | ||
|
||
## Rule Details | ||
|
||
This rule warns when it finds text that looks like it's a shorthand label reference and there's white space between the brackets. | ||
|
||
Examples of incorrect code: | ||
|
||
```markdown | ||
[eslint][ ] | ||
|
||
[eslint][ | ||
|
||
] | ||
``` | ||
|
||
## When Not to Use It | ||
|
||
If you publish your Markdown exclusively on GitHub, then you can safely disable this rule. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,140 @@ | ||
/** | ||
* @fileoverview Rule to prevent non-complaint link references. | ||
* @author Nicholas C. Zakas | ||
*/ | ||
|
||
//----------------------------------------------------------------------------- | ||
// Imports | ||
//----------------------------------------------------------------------------- | ||
|
||
import { findOffsets, illegalShorthandTailPattern } from "../util.js"; | ||
|
||
//----------------------------------------------------------------------------- | ||
// Type Definitions | ||
//----------------------------------------------------------------------------- | ||
|
||
/** @typedef {import("unist").Position} Position */ | ||
/** @typedef {import("mdast").Text} TextNode */ | ||
/** @typedef {import("eslint").Rule.RuleModule} RuleModule */ | ||
|
||
//----------------------------------------------------------------------------- | ||
// Helpers | ||
//----------------------------------------------------------------------------- | ||
|
||
// matches i.e., [foo][bar] | ||
const labelPattern = /\]\[([^\]]+)\]/u; | ||
|
||
|
||
/** | ||
* Finds missing references in a node. | ||
* @param {TextNode} node The node to check. | ||
* @param {string} text The text of the node. | ||
* @returns {Array<{label:string,position:Position}>} The missing references. | ||
*/ | ||
function findIllegalLabelReferences(node, text) { | ||
|
||
const invalid = []; | ||
let startIndex = 0; | ||
|
||
while (startIndex < node.value.length) { | ||
|
||
const value = node.value.slice(startIndex); | ||
const match = value.match(labelPattern); | ||
|
||
if (!match) { | ||
break; | ||
} | ||
|
||
if (!illegalShorthandTailPattern.test(match[0])) { | ||
continue; | ||
} | ||
|
||
let columnStart = startIndex + match.index + 1; | ||
|
||
// adding 1 to the index just in case we're in a ![] and need to skip the !. | ||
const startFrom = node.position.start.offset + startIndex + 1; | ||
const lastOpenBracket = text.lastIndexOf("[", startFrom); | ||
|
||
if (lastOpenBracket === -1) { | ||
startIndex += match.index + match[0].length; | ||
continue; | ||
} | ||
|
||
const label = text.slice(lastOpenBracket, match.index + match[0].length).match(/!?\[([^\]]+)\]/u)?.[1]; | ||
|
||
columnStart -= label.length; | ||
|
||
const { | ||
lineOffset, | ||
columnOffset | ||
} = findOffsets(node.value, columnStart); | ||
|
||
const line = node.position.start.line + lineOffset; | ||
|
||
/* | ||
* If the columnOffset is 0, then the column is at the start of the line. | ||
* In that case, we need to adjust the column number to be 1. | ||
*/ | ||
invalid.push({ | ||
label: label.trim(), | ||
position: { | ||
start: { | ||
line, | ||
column: columnOffset || 1 | ||
}, | ||
end: { | ||
line, | ||
column: columnOffset + label.length | ||
} | ||
} | ||
}); | ||
|
||
startIndex += match.index + match[0].length; | ||
} | ||
|
||
return invalid; | ||
|
||
} | ||
|
||
//----------------------------------------------------------------------------- | ||
// Rule Definition | ||
//----------------------------------------------------------------------------- | ||
|
||
/** @type {RuleModule} */ | ||
export default { | ||
meta: { | ||
type: "problem", | ||
|
||
docs: { | ||
recommended: true, | ||
description: "Disallow invalid label references." | ||
}, | ||
|
||
messages: { | ||
illegalLabelRef: "Label reference '{{label}}' is invalid due to white space between [ and ]." | ||
} | ||
}, | ||
|
||
create(context) { | ||
|
||
const { sourceCode } = context; | ||
|
||
return { | ||
|
||
text(node) { | ||
const invalidReferences = findIllegalLabelReferences(node, sourceCode.text); | ||
|
||
for (const invalidReference of invalidReferences) { | ||
context.report({ | ||
loc: invalidReference.position, | ||
messageId: "illegalLabelRef", | ||
data: { | ||
label: invalidReference.label | ||
} | ||
}); | ||
} | ||
} | ||
|
||
}; | ||
} | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
/** | ||
* @fileoverview Utility Library | ||
* @author Nicholas C. Zakas | ||
*/ | ||
|
||
|
||
/* | ||
* CommonMark does not allow any white space between the brackets in a reference link. | ||
* If that pattern is detected, then it's treated as text and not as a link. This pattern | ||
* is used to detect that situation. | ||
*/ | ||
export const illegalShorthandTailPattern = /\]\[\s+\]$/u; | ||
|
||
/** | ||
* Finds the line and column offsets for a given start offset in a string. | ||
* @param {string} text The text to search. | ||
* @param {number} startOffset The offset to find. | ||
* @returns {{lineOffset:number,columnOffset:number}} The location of the offset. | ||
*/ | ||
export function findOffsets(text, startOffset) { | ||
|
||
let lineOffset = 0; | ||
let columnOffset = 0; | ||
|
||
for (let i = 0; i < startOffset; i++) { | ||
if (text[i] === "\n") { | ||
lineOffset++; | ||
columnOffset = 0; | ||
} else { | ||
columnOffset++; | ||
} | ||
} | ||
|
||
return { | ||
lineOffset, | ||
columnOffset | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
/** | ||
* @fileoverview Tests for no-invalid-label-refs rule. | ||
* @author Nicholas C. Zakas | ||
*/ | ||
|
||
//------------------------------------------------------------------------------ | ||
// Imports | ||
//------------------------------------------------------------------------------ | ||
|
||
import rule from "../../src/rules/no-invalid-label-refs.js"; | ||
import markdown from "../../src/index.js"; | ||
import { RuleTester } from "eslint"; | ||
|
||
//------------------------------------------------------------------------------ | ||
// Tests | ||
//------------------------------------------------------------------------------ | ||
|
||
const ruleTester = new RuleTester({ | ||
plugins: { | ||
markdown | ||
}, | ||
language: "markdown/commonmark" | ||
}); | ||
|
||
ruleTester.run("no-invalid-label-refs", rule, { | ||
valid: [ | ||
"[*foo*]", | ||
"[foo]\n\n[foo]: http://bar.com", | ||
"[foo][ foo ]\n\n[foo]: http://bar.com", | ||
"![foo][foo]\n\n[foo]: http://bar.com/image.jpg", | ||
"[foo][]\n\n[foo]: http://bar.com/image.jpg", | ||
"![foo][]\n\n[foo]: http://bar.com/image.jpg", | ||
"[ foo ][]\n\n[foo]: http://bar.com/image.jpg", | ||
], | ||
invalid: [ | ||
{ | ||
code: "[foo][ ]\n\n[foo]: http://bar.com/image.jpg", | ||
errors: [ | ||
{ | ||
messageId: "illegalLabelRef", | ||
data: { label: "foo" }, | ||
line: 1, | ||
column: 2, | ||
endLine: 1, | ||
endColumn: 5 | ||
} | ||
] | ||
}, | ||
{ | ||
code: "[\nfoo\n][\n]\n\n[foo]: http://bar.com/image.jpg", | ||
errors: [ | ||
{ | ||
messageId: "illegalLabelRef", | ||
data: { label: "foo" }, | ||
line: 2, | ||
column: 1, | ||
endLine: 2, | ||
endColumn: 5 | ||
} | ||
] | ||
} | ||
] | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters