Skip to content

Commit

Permalink
remark-lint-media-style: add
Browse files Browse the repository at this point in the history
Closes GH-266.
  • Loading branch information
wooorm committed Jan 14, 2025
1 parent e8e1ac1 commit 86226a4
Show file tree
Hide file tree
Showing 9 changed files with 760 additions and 0 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@
"packages/remark-lint-mdx-jsx-self-close/",
"packages/remark-lint-mdx-jsx-unique-attribute-name/",
"packages/remark-lint-mdx-jsx-shorthand-attribute/",
"packages/remark-lint-media-style/",
"packages/remark-lint-no-blockquote-without-marker/",
"packages/remark-lint-no-consecutive-blank-lines/",
"packages/remark-lint-no-duplicate-defined-urls/",
Expand Down
2 changes: 2 additions & 0 deletions packages/remark-lint-media-style/.npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ignore-scripts=true
package-lock=false
293 changes: 293 additions & 0 deletions packages/remark-lint-media-style/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,293 @@
/**
* remark-lint rule to warn when the style of specifying
* the URL of images and links is incorrect.
*
* ## What is this?
*
* This package checks for media (image and link) style:
* whether references or resources are used.
*
* ## When should I use this?
*
* You can use this package to check that the style of specifying the URL
* of images and links is correct.
*
* ## API
*
* ### `unified().use(remarkLintMediaStyle[, options])`
*
* Warn when the style of specifying the URL of images and links is
* incorrect.
*
* ###### Parameters
*
* * `options` ([`Options`][api-options], default: `'consistent'`)
* — preferred style or whether to detect the first style and warn for
* further differences
*
* ###### Returns
*
* Transform ([`Transformer` from `unified`][github-unified-transformer]).
*
* ### `Options`
*
* Configuration (TypeScript type).
*
* * `'consistent'`
* — detect the first used style and warn when further rules differ;
* “reference-reuse” cannot be detected
* * [`Style`][api-style]
* — style to prefer
*
* ### `Style`
*
* Style (TypeScript type).
*
* * `'reference'`
* — prefer references
* * `'reference-reuse'`
* — allow resources when used once,
* prefer references otherwise
* * `'resource'`
* — prefer resources
*
* [api-options]: #options
* [api-remark-lint-media-style]: #unifieduseremarklintmediastyle-options
* [api-style]: #style
* [github-unified-transformer]: https://github.com/unifiedjs/unified#transformer
*
* @module media-style
* @author Titus Wormer
* @copyright 2025 Titus Wormer
* @license MIT
*
* @example
* {"name": "ok-consistent-reference.md"}
*
* [Mercury][] and [Venus][].
*
* [mercury]: https://example.com/mercury/
* [venus]: https://example.com/venus/
*
* @example
* {"name": "ok-consistent-resource.md"}
*
* [Mercury](https://example.com/mercury/) and
* [Venus](https://example.com/venus/).
*
* @example
* {"config": "reference-reuse", "name": "ok-reference-reuse.md"}
*
* [Mercury](https://example.com/mercury/),
* [Venus][], and [Earth][].
*
* **[Venus][]** is the second planet from the Sun.
*
* [venus]: https://example.com/venus/
* [earth]: https://example.com/earth/
*
* @example
* {"config": "reference", "label": "input", "name": "nok-reference.md"}
*
* [Mercury](https://example.com/mercury/),
* [Venus](https://example.com/venus/), and
* [Earth][].
*
* [earth]: https://example.com/earth/
* @example
* {"config": "reference", "label": "output", "name": "nok-reference.md"}
*
* 1:1-1:40: Unexpected resource for url `https://example.com/mercury/`, expected a definition and a reference to it
* 2:1-2:36: Unexpected resource for url `https://example.com/venus/`, expected a definition and a reference to it
*
* @example
* {"config": "resource", "label": "input", "name": "nok-resource.md"}
*
* [Mercury](https://example.com/mercury/),
* [Venus](https://example.com/venus/), and
* [Earth][].
*
* [earth]: https://example.com/earth/
* @example
* {"config": "resource", "label": "output", "name": "nok-resource.md"}
*
* 3:1-3:10: Unexpected reference for url `https://example.com/earth/`, expected resource
*
* @example
* {"config": "reference-reuse", "label": "input", "name": "nok-reference-reuse.md"}
*
* [Mercury](https://example.com/mercury/),
* [Venus](https://example.com/venus/), and [Earth][].
*
* **[Venus](https://example.com/venus/)** is the second planet from the Sun.
*
* [earth]: https://example.com/earth/
* @example
* {"config": "reference-reuse", "label": "output", "name": "nok-reference-reuse.md"}
*
* 2:1-2:36: Unexpected resource for reused url `https://example.com/venus/`, expected a definition and a reference to it
* 4:3-4:38: Unexpected resource for reused url `https://example.com/venus/`, expected a definition and a reference to it
*
* @example
* {"config": "reference-reuse", "label": "input", "name": "reference-reuse-defined.md"}
*
* [Mercury](https://example.com/mercury/).
*
* [mercury]: https://example.com/mercury/
* @example
* {"config": "reference-reuse", "label": "output", "name": "reference-reuse-defined.md"}
*
* 1:1-1:40: Unexpected resource for reused url `https://example.com/mercury/`, expected a reference to `mercury`
*
* @example
* {"config": "🌍", "label": "output", "name": "not-ok.md", "positionless": true}
*
* 1:1: Unexpected value `🌍` for `options`, expected `'reference-reuse'`, `'reference'`, `'resource'`, or `'consistent'`
*
* @example
* {"name": "definitions.md"}
*
* [mercury]: https://example.com/mercury/
* [venus]: https://example.com/venus/
*/

/**
* @import {ImageReference, Image, LinkReference, Link, Nodes, Parents, Reference, Resource, Root} from 'mdast'
*/

/**
* @typedef {Style | 'consistent'} Options
* Configuration.
*
* @typedef {'reference-reuse' | 'reference' | 'resource'} Style
* Style.
*/

import {lintRule} from 'unified-lint-rule'
import {visitParents} from 'unist-util-visit-parents'

const remarkLintMediaStyle = lintRule(
{
origin: 'remark-lint:media-style',
url: 'https://github.com/remarkjs/remark-lint/tree/main/packages/remark-lint-media-style#readme'
},
/**
* @param {Root} tree
* Tree.
* @param {Options | null | undefined} [options='consistent']
* Configuration (default: `'consistent'`).
* @returns {undefined}
* Nothing.
*/
function (tree, file, options) {
/** @type {Style | undefined} */
let expected

if (
options === 'reference-reuse' ||
options === 'reference' ||
options === 'resource'
) {
expected = options
} else if (
options === 'consistent' ||
options === null ||
options === undefined
) {
// Empty
} else {
file.fail(
'Unexpected value `' +
options +
"` for `options`, expected `'reference-reuse'`, `'reference'`, `'resource'`, or `'consistent'`"
)
}

/** @type {Map<string, Array<[node: Extract<Nodes, Reference>, parents: Array<Parents>]>>} */
const references = new Map()
/** @type {Map<string, Array<[node: Extract<Nodes, Resource>, parents: Array<Parents>]>>} */
const resources = new Map()

visitParents(tree, function (node, parents) {
if ('url' in node) {
if (!expected && node.type !== 'definition') expected = 'resource'
const current = resources.get(node.url)
if (current) {
current.push([node, parents])
} else {
resources.set(node.url, [[node, parents]])
}
}

if ('referenceType' in node) {
if (!expected) expected = 'reference'
const current = references.get(node.identifier)
if (current) {
current.push([node, parents])
} else {
references.set(node.identifier, [[node, parents]])
}
}
})

// No media found.
if (!expected) return

for (const [url, nodes] of resources.entries()) {
/** @type {Array<[node: Extract<Nodes, Reference>, parents: Array<Parents>]>} */
const reference = []
/** @type {Array<[node: Image | Link, parents: Array<Parents>]>} */
const resource = []
/** @type {string | undefined} */
let identifier

for (const [node, parents] of nodes) {
if (node.position) {
if (node.type === 'definition') {
identifier = node.identifier
const results = references.get(identifier)
if (results) reference.push(...results)
} else {
resource.push([node, parents])
}
}
}

/** @type {Array<[node: ImageReference | Image | LinkReference | Link, parents: Array<Parents>]>} */
const problems = []

if (expected === 'reference') {
problems.push(...resource)
} else if (expected === 'reference-reuse') {
// If there is a definition,
// there should not be *any* resource to that same url.
if (resource.length > 1 || identifier) {
problems.push(...resource)
}
} else {
problems.push(...reference)
}

for (const [node, parents] of problems) {
file.message(
'Unexpected ' +
('url' in node ? 'resource' : 'reference') +
' for' +
(expected === 'reference-reuse' ? ' reused' : '') +
' url `' +
url +
'`' +
', expected ' +
(expected === 'reference-reuse' || expected === 'reference'
? identifier
? 'a reference to `' + identifier + '`'
: 'a definition and a reference to it'
: expected),
{ancestors: [...parents, node], place: node.position}
)
}
}
}
)

export default remarkLintMediaStyle
55 changes: 55 additions & 0 deletions packages/remark-lint-media-style/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
{
"name": "remark-lint-media-style",
"version": "0.0.0",
"description": "remark-lint rule to check for media (image and link) style: whether references or resources are used",
"license": "MIT",
"keywords": [
"image",
"link",
"lint",
"media",
"reference",
"remark",
"remark-lint",
"remark-lint-rule",
"resource",
"rule",
"style"
],
"repository": "https://github.com/remarkjs/remark-lint/tree/main/packages/remark-lint-media-style",
"bugs": "https://github.com/remarkjs/remark-lint/issues",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
},
"author": "Titus Wormer <[email protected]> (https://wooorm.com)",
"contributors": [
"Titus Wormer <[email protected]> (https://wooorm.com)"
],
"sideEffects": false,
"type": "module",
"exports": "./index.js",
"files": [
"index.d.ts",
"index.d.ts.map",
"index.js"
],
"dependencies": {
"@types/mdast": "^4.0.0",
"unified-lint-rule": "^3.0.0",
"unist-util-visit-parents": "^6.0.0"
},
"scripts": {},
"typeCoverage": {
"atLeast": 100,
"strict": true
},
"xo": {
"prettier": true,
"rules": {
"capitalized-comments": "off",
"complexity": "off",
"logical-assignment-operators": "off"
}
}
}
Loading

0 comments on commit 86226a4

Please sign in to comment.