markuplint 5.2.0 → 5.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +25 -0
- package/lib/api/ml-engine.d.ts +3 -0
- package/lib/api/ml-engine.js +62 -17
- package/lib/api/types.d.ts +7 -0
- package/lib/debug.d.ts +4 -0
- package/lib/debug.js +4 -0
- package/lib/index.d.ts +6 -0
- package/lib/index.js +6 -0
- package/lib/suppressions/apply-suppressions.js +51 -36
- package/lib/suppressions/compute-scope.d.ts +59 -0
- package/lib/suppressions/compute-scope.js +195 -9
- package/lib/suppressions/downgrade-severity.js +1 -1
- package/lib/suppressions/types.d.ts +3 -0
- package/package.json +15 -16
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,31 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
# [5.4.0](https://github.com/markuplint/markuplint/compare/v5.3.0...v5.4.0) (2026-10-29)
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
* **markuplint:** skip the config search when it is disabled ([e2e8da9](https://github.com/markuplint/markuplint/commit/e2e8da9a81fb113b726fc30877b16cfab4d53736)), closes [#4409](https://github.com/markuplint/markuplint/issues/4409)
|
|
11
|
+
* **markuplint:** watch a config file the search found broken ([1a51145](https://github.com/markuplint/markuplint/commit/1a51145b3eb34c18c0475205466b608787352b66)), closes [#4413](https://github.com/markuplint/markuplint/issues/4413)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# [5.3.0](https://github.com/markuplint/markuplint/compare/v5.2.0...v5.3.0) (2026-10-21)
|
|
15
|
+
|
|
16
|
+
### Bug Fixes
|
|
17
|
+
|
|
18
|
+
* make published packages loadable from require() and put types first ([723106f](https://github.com/markuplint/markuplint/commit/723106f51aac4e60dc02db2c1a894617368b0253)), closes [#4198](https://github.com/markuplint/markuplint/issues/4198)
|
|
19
|
+
* **markuplint:** build the lint AST mapping only when debug is on ([abdbfeb](https://github.com/markuplint/markuplint/commit/abdbfeb9f88ca11e07b47c279a9d793cc46c47c2))
|
|
20
|
+
* **markuplint:** honor the debug option in the MLEngine constructor ([d33d3c8](https://github.com/markuplint/markuplint/commit/d33d3c87a1c8e0c04ee921945ed7488a51ab79c4)), closes [this.#options](https://github.com/this./issues/options)
|
|
21
|
+
* **markuplint:** match a suppression scope of the form tag[attr] with a capital or a digit ([372f313](https://github.com/markuplint/markuplint/commit/372f3138addfbd700b076871e38e1b4fd64e4430))
|
|
22
|
+
* **markuplint:** write and read a scope of a suppression for a name with a `.` ([62c0f3c](https://github.com/markuplint/markuplint/commit/62c0f3c451b45a74491b46be5dc645cbe8b60f18))
|
|
23
|
+
* **markuplint:** write and read the names of a scope as identifiers of CSS ([447fee3](https://github.com/markuplint/markuplint/commit/447fee3362774f273b045fa7996846aa4294360a))
|
|
24
|
+
|
|
25
|
+
### Performance Improvements
|
|
26
|
+
|
|
27
|
+
* **markuplint:** group the engine debug calls under one guard ([15c0888](https://github.com/markuplint/markuplint/commit/15c088879400cf1aa0355c260c63a8eb3af4e86a))
|
|
28
|
+
* **markuplint:** skip debug maps without lint listeners ([205bff7](https://github.com/markuplint/markuplint/commit/205bff712bb4a7d42d92b6927ebef5eeb7401dd5))
|
|
29
|
+
|
|
30
|
+
|
|
6
31
|
# [5.2.0](https://github.com/markuplint/markuplint/compare/v5.1.1...v5.2.0) (2026-10-13)
|
|
7
32
|
|
|
8
33
|
### Bug Fixes
|
package/lib/api/ml-engine.d.ts
CHANGED
|
@@ -6,6 +6,9 @@ import type { Document, RuleConfigValue } from '@markuplint/ml-core';
|
|
|
6
6
|
import { ConfigProvider } from '@markuplint/file-resolver';
|
|
7
7
|
import { Emitter } from 'strict-event-emitter';
|
|
8
8
|
type MLEngineOptions = {
|
|
9
|
+
/**
|
|
10
|
+
* Enables verbose logging and populates the `debug` payload of the `lint` event.
|
|
11
|
+
*/
|
|
9
12
|
readonly debug?: boolean;
|
|
10
13
|
readonly watch?: boolean;
|
|
11
14
|
/**
|
package/lib/api/ml-engine.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ConfigProvider, createPretenderResolver, disambiguatePretendersForFile, invalidatePretenderResolutionCaches, resolveFiles, resolveParser, resolveRules, resolveSpecs, } from '@markuplint/file-resolver';
|
|
1
|
+
import { ConfigProvider, configLoadErrorFiles, createPretenderResolver, disambiguatePretendersForFile, invalidatePretenderResolutionCaches, resolveFiles, resolveParser, resolveRules, resolveSpecs, } from '@markuplint/file-resolver';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { applyRuleAliasesToConfig, mergeConfig } from '@markuplint/ml-config';
|
|
4
4
|
import { CONFIG_ERROR_RULE_ID, MLCore, convertRuleset } from '@markuplint/ml-core';
|
|
@@ -132,7 +132,7 @@ export class MLEngine extends Emitter {
|
|
|
132
132
|
#rerunAgain = false;
|
|
133
133
|
constructor(file, options) {
|
|
134
134
|
super();
|
|
135
|
-
if (
|
|
135
|
+
if (options?.debug) {
|
|
136
136
|
verbosely();
|
|
137
137
|
}
|
|
138
138
|
this.#file = file;
|
|
@@ -224,7 +224,10 @@ export class MLEngine extends Emitter {
|
|
|
224
224
|
};
|
|
225
225
|
}
|
|
226
226
|
const { violations, fixedCode, fixSummary } = verifyResult;
|
|
227
|
-
|
|
227
|
+
// The AST mapping is a debugging aid: the language server listens to every
|
|
228
|
+
// lint but only reads the payload when the user asked for debugging, so
|
|
229
|
+
// a listener alone is no reason to walk the whole document.
|
|
230
|
+
const debugMap = this.#options?.debug && this.listenerCount('lint') > 0 ? (this.document?.debugMap() ?? null) : null;
|
|
228
231
|
const resolvedFixedCode = fixedCode ?? sourceCode;
|
|
229
232
|
this.emit('lint', this.#file.path, sourceCode, violations, resolvedFixedCode, debugMap, fixSummary ?? null);
|
|
230
233
|
log('exec: end');
|
|
@@ -333,9 +336,10 @@ export class MLEngine extends Emitter {
|
|
|
333
336
|
async #createCore(fabric) {
|
|
334
337
|
fileLog('Get source code');
|
|
335
338
|
const sourceCode = await this.#file.getCode();
|
|
336
|
-
fileLog
|
|
337
|
-
|
|
338
|
-
|
|
339
|
+
if (fileLog.enabled) {
|
|
340
|
+
fileLog('Source code path: %s', this.#file.path);
|
|
341
|
+
fileLog('Source code size: %dbyte', sourceCode.length);
|
|
342
|
+
}
|
|
339
343
|
this.emit('code', this.#file.path, sourceCode);
|
|
340
344
|
const core = new MLCore({
|
|
341
345
|
sourceCode,
|
|
@@ -522,10 +526,12 @@ export class MLEngine extends Emitter {
|
|
|
522
526
|
}
|
|
523
527
|
return this.#failConfig(error);
|
|
524
528
|
}
|
|
525
|
-
fileLog
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
+
if (fileLog.enabled) {
|
|
530
|
+
fileLog('Fetched Config files: %O', configSet.files);
|
|
531
|
+
fileLog('Resolved Config: %O', configSet.config);
|
|
532
|
+
fileLog('Resolved Plugins: %O', configSet.plugins);
|
|
533
|
+
fileLog('Resolve Errors: %O', configSet.errs);
|
|
534
|
+
}
|
|
529
535
|
const loadError = configSet.errs.find(error => error instanceof ConfigLoadError);
|
|
530
536
|
if (loadError) {
|
|
531
537
|
return this.#failConfig(loadError);
|
|
@@ -614,7 +620,7 @@ export class MLEngine extends Emitter {
|
|
|
614
620
|
// in the middle of this one's `set()`/`search()` calls, which would
|
|
615
621
|
// wipe the keys just registered below before `resolve()` gets to use
|
|
616
622
|
// them. See #4015.
|
|
617
|
-
const resolvedConfigSet = await this.#
|
|
623
|
+
const resolvedConfigSet = await this.#runResolution(async () => {
|
|
618
624
|
if (!cache) {
|
|
619
625
|
// Must run before any `set()` call below — `ConfigProvider#resolve()`
|
|
620
626
|
// no longer clears its own store on `cache: false`, so invalidating
|
|
@@ -626,17 +632,24 @@ export class MLEngine extends Emitter {
|
|
|
626
632
|
this.#configProvider.set(mergeConfig(this.#options.defaultConfig), undefined, this.#options.defaultConfig);
|
|
627
633
|
configLog('defaultConfigKey: %s', defaultConfigKey ?? 'N/A');
|
|
628
634
|
this.emit('log', 'defaultConfigKey', defaultConfigKey ?? 'N/A');
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
635
|
+
// Skipped, not run and discarded: the search reads and parses every
|
|
636
|
+
// config file it reaches, so a malformed one would still surface as a
|
|
637
|
+
// `config-error` in a run that asked not to search. See #4409.
|
|
638
|
+
let targetConfig = null;
|
|
639
|
+
if (this.#options?.noSearchConfig || this.#options?.configFile) {
|
|
640
|
+
this.emit('log', 'targetConfig', 'skipped (search is disabled)');
|
|
641
|
+
}
|
|
642
|
+
else {
|
|
643
|
+
targetConfig = await this.#configProvider.search(this.#file);
|
|
644
|
+
this.emit('log', 'targetConfig', targetConfig ?? 'N/A');
|
|
645
|
+
}
|
|
646
|
+
const configFilePathsFromTarget = targetConfig ?? defaultConfigKey ?? null;
|
|
634
647
|
configLog('configFilePathsFromTarget: %s', configFilePathsFromTarget ?? 'N/A');
|
|
635
648
|
this.emit('log', 'configFilePathsFromTarget', configFilePathsFromTarget ?? 'N/A');
|
|
636
649
|
const configKey = this.#options?.config &&
|
|
637
650
|
this.#configProvider.set(mergeConfig(this.#options.config), undefined, this.#options.config);
|
|
638
651
|
configLog('option.config: %s', configKey ?? 'N/A');
|
|
639
|
-
this.emit('log', 'option.config',
|
|
652
|
+
this.emit('log', 'option.config', configKey ?? 'N/A');
|
|
640
653
|
let defaultRecommended = null;
|
|
641
654
|
if (!defaultConfigKey && !configFilePathsFromTarget && !configKey && !this.#options?.configFile) {
|
|
642
655
|
// No configured
|
|
@@ -669,6 +682,38 @@ export class MLEngine extends Emitter {
|
|
|
669
682
|
await this.#syncWatch();
|
|
670
683
|
return configSet;
|
|
671
684
|
}
|
|
685
|
+
/**
|
|
686
|
+
* Runs `resolve` exclusively on the provider (see {@link resolveConfig}).
|
|
687
|
+
* When it throws a `ConfigLoadError` (a config file that the search found
|
|
688
|
+
* cannot be loaded, or a preset that is not found), the files whose change
|
|
689
|
+
* can fix it ({@link configLoadErrorFiles}: the file it names, and a
|
|
690
|
+
* `$import` target that does not exist yet) are added to the watched files
|
|
691
|
+
* before the error goes on: there is no config set to take
|
|
692
|
+
* {@link ConfigSet.files} from, and the files of the previous resolution
|
|
693
|
+
* alone would miss the fix of a file that was broken from the start. They
|
|
694
|
+
* stay watched, as the error names only the first thing that failed, not
|
|
695
|
+
* every file the config depends on. A directory that the search cannot
|
|
696
|
+
* examine is not among them, as the shared watcher takes file paths (see
|
|
697
|
+
* its module JSDoc in `shared-watcher.ts`).
|
|
698
|
+
*
|
|
699
|
+
* @param resolve - The resolution, which runs inside `runExclusive`
|
|
700
|
+
* @returns The config set that `resolve` returns
|
|
701
|
+
*/
|
|
702
|
+
async #runResolution(resolve) {
|
|
703
|
+
try {
|
|
704
|
+
return await this.#configProvider.runExclusive(resolve);
|
|
705
|
+
}
|
|
706
|
+
catch (error) {
|
|
707
|
+
if (isFatalError(error)) {
|
|
708
|
+
throw error;
|
|
709
|
+
}
|
|
710
|
+
if (error instanceof ConfigLoadError) {
|
|
711
|
+
this.#configFiles = new Set([...this.#configFiles, ...configLoadErrorFiles(error)]);
|
|
712
|
+
await this.#syncWatch();
|
|
713
|
+
}
|
|
714
|
+
throw error;
|
|
715
|
+
}
|
|
716
|
+
}
|
|
672
717
|
async #resolveParser(
|
|
673
718
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
674
719
|
configSet) {
|
package/lib/api/types.d.ts
CHANGED
|
@@ -37,6 +37,13 @@ export type MLEngineEventMap = {
|
|
|
37
37
|
sourceCode: string,
|
|
38
38
|
violations: readonly Violation[],
|
|
39
39
|
fixedCode: string,
|
|
40
|
+
/**
|
|
41
|
+
* The AST mapping dump. It is built only when the engine's `debug`
|
|
42
|
+
* option is on and a `lint` listener is registered at the time the
|
|
43
|
+
* engine emits, because the dump costs a full walk of the document
|
|
44
|
+
* that nobody reads unless debugging was requested. Otherwise, or when
|
|
45
|
+
* the engine has no parsed document, it is `null`.
|
|
46
|
+
*/
|
|
40
47
|
debug: readonly string[] | null,
|
|
41
48
|
fixSummary: FixSummary | null
|
|
42
49
|
];
|
package/lib/debug.d.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
1
|
import debug from 'debug';
|
|
2
|
+
/**
|
|
3
|
+
* Read `.enabled` at call time; never cache it at module load. `debug.enable()` (including via `verbosely()` /
|
|
4
|
+
* `enableDebug()`) can switch a namespace on after import.
|
|
5
|
+
*/
|
|
2
6
|
export declare const log: debug.Debugger;
|
|
3
7
|
export declare function verbosely(): void;
|
package/lib/debug.js
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { enableDebug } from '@markuplint/ml-core';
|
|
2
2
|
import debug from 'debug';
|
|
3
|
+
/**
|
|
4
|
+
* Read `.enabled` at call time; never cache it at module load. `debug.enable()` (including via `verbosely()` /
|
|
5
|
+
* `enableDebug()`) can switch a namespace on after import.
|
|
6
|
+
*/
|
|
3
7
|
export const log = debug('markuplint-cli');
|
|
4
8
|
export function verbosely() {
|
|
5
9
|
if (!log.enabled) {
|
package/lib/index.d.ts
CHANGED
|
@@ -4,6 +4,12 @@
|
|
|
4
4
|
* Main package entry point for markuplint.
|
|
5
5
|
* Re-exports the public API surface including the lint engine, types,
|
|
6
6
|
* internationalization utilities, testing tools, and version information.
|
|
7
|
+
*
|
|
8
|
+
* Invariant for every published package: the ESM-only build must stay
|
|
9
|
+
* loadable from CommonJS through `require()`. Node does that synchronously only
|
|
10
|
+
* while the static module graph has no top-level `await`, so none may be
|
|
11
|
+
* introduced. `packages/@markuplint/test-tools/src/package-exports.spec.ts`
|
|
12
|
+
* enforces it.
|
|
7
13
|
*/
|
|
8
14
|
/** The primary linting engine and options for linting from source code strings. */
|
|
9
15
|
export { MLEngine, FromCodeOptions } from './api/index.js';
|
package/lib/index.js
CHANGED
|
@@ -4,6 +4,12 @@
|
|
|
4
4
|
* Main package entry point for markuplint.
|
|
5
5
|
* Re-exports the public API surface including the lint engine, types,
|
|
6
6
|
* internationalization utilities, testing tools, and version information.
|
|
7
|
+
*
|
|
8
|
+
* Invariant for every published package: the ESM-only build must stay
|
|
9
|
+
* loadable from CommonJS through `require()`. Node does that synchronously only
|
|
10
|
+
* while the static module graph has no top-level `await`, so none may be
|
|
11
|
+
* introduced. `packages/@markuplint/test-tools/src/package-exports.spec.ts`
|
|
12
|
+
* enforces it.
|
|
7
13
|
*/
|
|
8
14
|
/** The primary linting engine and options for linting from source code strings. */
|
|
9
15
|
export { MLEngine } from './api/index.js';
|
|
@@ -1,28 +1,58 @@
|
|
|
1
|
-
import { findNodeAtPosition } from './compute-scope.js';
|
|
1
|
+
import { findNodeAtPosition, splitScopeAttr, splitScopeIdents, splitScopeNth, unescapeIdentifier, } from './compute-scope.js';
|
|
2
2
|
import { toRelativePath } from './suppressions-file.js';
|
|
3
3
|
function isViolationInScope(
|
|
4
4
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
5
|
-
nodeList, violation,
|
|
5
|
+
nodeList, violation, segments) {
|
|
6
6
|
const violationNode = findNodeAtPosition(nodeList, violation.line, violation.col);
|
|
7
7
|
if (!violationNode) {
|
|
8
8
|
return false;
|
|
9
9
|
}
|
|
10
10
|
// Check the node itself and walk up ancestors
|
|
11
|
-
if (matchesScopeSelector(violationNode,
|
|
11
|
+
if (matchesScopeSelector(violationNode, segments)) {
|
|
12
12
|
return true;
|
|
13
13
|
}
|
|
14
14
|
let current = violationNode.parentElement;
|
|
15
15
|
while (current) {
|
|
16
|
-
if (matchesScopeSelector(current,
|
|
16
|
+
if (matchesScopeSelector(current, segments)) {
|
|
17
17
|
return true;
|
|
18
18
|
}
|
|
19
19
|
current = current.parentElement;
|
|
20
20
|
}
|
|
21
21
|
return false;
|
|
22
22
|
}
|
|
23
|
+
function parseSegment(segment) {
|
|
24
|
+
if (segment.length === 0) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
// #id
|
|
28
|
+
if (segment.startsWith('#')) {
|
|
29
|
+
return { kind: 'id', id: unescapeIdentifier(segment.slice(1)) };
|
|
30
|
+
}
|
|
31
|
+
// tag[attr="value"]: the tag is the `localName` with a capital letter (`FooBar`), a digit (`h1`) or an escaped character
|
|
32
|
+
// A segment with no `[` has no attribute, so it is not scanned for one
|
|
33
|
+
const attr = segment.includes('[') ? splitScopeAttr(segment) : null;
|
|
34
|
+
if (attr) {
|
|
35
|
+
return {
|
|
36
|
+
kind: 'attr',
|
|
37
|
+
tag: splitScopeIdents(attr.tag).join('.'),
|
|
38
|
+
attrName: attr.attrName,
|
|
39
|
+
attrValue: attr.attrValue,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
// Extract :nth-of-type(n) if present and unescaped
|
|
43
|
+
const { base: baseSegment, nth } = segment.includes(':') ? splitScopeNth(segment) : { base: segment, nth: null };
|
|
44
|
+
// tag.classA.classB
|
|
45
|
+
// A `.` of a name is escaped (`Tabs\.Root`), so only an unescaped one starts a class.
|
|
46
|
+
const [tag, ...classes] = splitScopeIdents(baseSegment);
|
|
47
|
+
return {
|
|
48
|
+
kind: 'compound',
|
|
49
|
+
tag: tag,
|
|
50
|
+
classes,
|
|
51
|
+
nth,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
23
54
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
24
|
-
function matchesScopeSelector(node,
|
|
25
|
-
const segments = scope.split(' > ');
|
|
55
|
+
function matchesScopeSelector(node, segments) {
|
|
26
56
|
let current = node;
|
|
27
57
|
// Match segments from right (most specific) to left (ancestors)
|
|
28
58
|
for (let i = segments.length - 1; i >= 0; i--) {
|
|
@@ -38,49 +68,30 @@ function matchesScopeSelector(node, scope) {
|
|
|
38
68
|
}
|
|
39
69
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
40
70
|
function matchesSegment(node, segment) {
|
|
41
|
-
if (segment
|
|
71
|
+
if (segment === null) {
|
|
42
72
|
return false;
|
|
43
73
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
const id = segment.slice(1);
|
|
47
|
-
return id.length > 0 && node.id === id;
|
|
74
|
+
if (segment.kind === 'id') {
|
|
75
|
+
return segment.id.length > 0 && node.id === segment.id;
|
|
48
76
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
if (attrMatch) {
|
|
52
|
-
const [, tag, attrName, attrValue] = attrMatch;
|
|
53
|
-
if (node.localName !== tag) {
|
|
77
|
+
if (segment.kind === 'attr') {
|
|
78
|
+
if (node.localName !== segment.tag) {
|
|
54
79
|
return false;
|
|
55
80
|
}
|
|
56
81
|
for (const attr of node.attributes) {
|
|
57
|
-
if (attr.name === attrName && attr.value === attrValue) {
|
|
82
|
+
if (attr.name === segment.attrName && attr.value === segment.attrValue) {
|
|
58
83
|
return true;
|
|
59
84
|
}
|
|
60
85
|
}
|
|
61
86
|
return false;
|
|
62
87
|
}
|
|
63
|
-
|
|
64
|
-
const nthMatch = /:nth-of-type\((\d+)\)$/.exec(segment);
|
|
65
|
-
const baseSegment = nthMatch ? segment.slice(0, nthMatch.index) : segment;
|
|
66
|
-
// tag.classA.classB
|
|
67
|
-
const dotIndex = baseSegment.indexOf('.');
|
|
68
|
-
let tagMatches;
|
|
69
|
-
if (dotIndex === -1) {
|
|
70
|
-
// tag only
|
|
71
|
-
tagMatches = node.localName === baseSegment;
|
|
72
|
-
}
|
|
73
|
-
else {
|
|
74
|
-
const tag = baseSegment.slice(0, dotIndex);
|
|
75
|
-
const classes = baseSegment.slice(dotIndex + 1).split('.');
|
|
76
|
-
tagMatches = node.localName === tag && classes.every(cls => node.classList.contains(cls));
|
|
77
|
-
}
|
|
88
|
+
const tagMatches = node.localName === segment.tag && segment.classes.every(cls => node.classList.contains(cls));
|
|
78
89
|
if (!tagMatches) {
|
|
79
90
|
return false;
|
|
80
91
|
}
|
|
81
92
|
// Check nth-of-type position
|
|
82
|
-
if (
|
|
83
|
-
const expectedPosition =
|
|
93
|
+
if (segment.nth !== null) {
|
|
94
|
+
const expectedPosition = segment.nth;
|
|
84
95
|
const parent = node.parentElement;
|
|
85
96
|
if (!parent) {
|
|
86
97
|
return false;
|
|
@@ -187,13 +198,17 @@ function getScopedErrorInfo(violations, ruleId, entry,
|
|
|
187
198
|
nodeList) {
|
|
188
199
|
let count = 0;
|
|
189
200
|
const indices = [];
|
|
201
|
+
// The scope is read once, not once per violation.
|
|
202
|
+
// It is split at ` > ` without looking at escapes: `serializeIdentifier` writes every `>` of a name as `\>`
|
|
203
|
+
// (and a space as `\ `), so the only ` > ` of a scope is a separator.
|
|
204
|
+
const segments = entry.scope && nodeList ? entry.scope.split(' > ').map(segment => parseSegment(segment)) : undefined;
|
|
190
205
|
for (const [i, violation] of violations.entries()) {
|
|
191
206
|
const v = violation;
|
|
192
207
|
if (v.severity !== 'error' || v.ruleId !== ruleId) {
|
|
193
208
|
continue;
|
|
194
209
|
}
|
|
195
|
-
if (
|
|
196
|
-
if (isViolationInScope(nodeList, v,
|
|
210
|
+
if (segments && nodeList) {
|
|
211
|
+
if (isViolationInScope(nodeList, v, segments)) {
|
|
197
212
|
count++;
|
|
198
213
|
indices.push(i);
|
|
199
214
|
}
|
|
@@ -51,6 +51,65 @@ export interface PositionedNode extends ScopedNode {
|
|
|
51
51
|
readonly startLine: number;
|
|
52
52
|
readonly startCol: number;
|
|
53
53
|
}
|
|
54
|
+
/**
|
|
55
|
+
* @experimental
|
|
56
|
+
* Writes a tag name, a class name or an id into a scope, by the CSSOM algorithm "serialize an identifier"
|
|
57
|
+
* (the one `CSS.escape()` is specified by), in which a code point that is not an ident code point of CSS
|
|
58
|
+
* Syntax is escaped, so the output is always one identifier. A NUL is written as U+FFFD, as the algorithm
|
|
59
|
+
* has it, so it does not read back as NUL. A scope is a CSS selector, and CSS lets any code point be part
|
|
60
|
+
* of an identifier by escaping it, "even ones that CSS uses in its syntax". So `Tabs.Root` (a member tag of JSX, MDX or Svelte) is `Tabs\.Root` and not the element
|
|
61
|
+
* `Tabs` with the class `Root`, `py-1.5` is `py-1\.5`, `hover:bg-x` is `hover\:bg-x`, `2xl:flex` is
|
|
62
|
+
* `\32 xl\:flex`, and a class that ends with `:nth-of-type(2)` is no position. What this writes and what
|
|
63
|
+
* {@link unescapeIdentifier} reads are one form, so a scope matches the element it was made for.
|
|
64
|
+
*
|
|
65
|
+
* A scope file written before the escape holds names unescaped, and they still read as the syntax,
|
|
66
|
+
* because every scope that ever matched was written so (`unescapeIdentifier` leaves a character that is
|
|
67
|
+
* not escaped as it is). A scope for a name with a `.` never matched, except in the form
|
|
68
|
+
* `tag[attr="value"]`, whose tag has always been read as a name. Likewise a scope for a name that ends
|
|
69
|
+
* with `:nth-of-type(n)` (`div.a:nth-of-type(2)`) never matched, and is still read as a position. A `\`
|
|
70
|
+
* that such a file holds is read as CSS reads it: `\ba` is the hex escape U+00BA.
|
|
71
|
+
*
|
|
72
|
+
* @see https://drafts.csswg.org/cssom/#serialize-an-identifier
|
|
73
|
+
*/
|
|
74
|
+
export declare function serializeIdentifier(name: string): string;
|
|
75
|
+
/**
|
|
76
|
+
* @experimental
|
|
77
|
+
* Reads an identifier of a scope, the reading side of {@link serializeIdentifier}: the escapes of CSS
|
|
78
|
+
* (`\` and 1 to 6 hex digits with one whitespace, or `\` and any other character), and a character that
|
|
79
|
+
* is not escaped as it is.
|
|
80
|
+
*/
|
|
81
|
+
export declare function unescapeIdentifier(escaped: string): string;
|
|
82
|
+
/**
|
|
83
|
+
* @experimental
|
|
84
|
+
* Splits a compound scope segment (`tag.classA.classB`) at its unescaped `.` and unescapes each part,
|
|
85
|
+
* the reading side of {@link generateUniqueSelector}. The first part is the tag, the rest are classes.
|
|
86
|
+
*/
|
|
87
|
+
export declare function splitScopeIdents(segment: string): string[];
|
|
88
|
+
/**
|
|
89
|
+
* @experimental
|
|
90
|
+
* Splits the form `tag[attr="value"]`. The tag is what comes before the first `[` that is not escaped,
|
|
91
|
+
* and it holds no unescaped `]` or `"`, so `Foo\[x="y"]` is no attribute: its `[` is escaped.
|
|
92
|
+
* The tag is returned as it is written.
|
|
93
|
+
*/
|
|
94
|
+
export declare function splitScopeAttr(segment: string): {
|
|
95
|
+
readonly tag: string;
|
|
96
|
+
readonly attrName: string;
|
|
97
|
+
readonly attrValue: string;
|
|
98
|
+
} | null;
|
|
99
|
+
/**
|
|
100
|
+
* @experimental
|
|
101
|
+
* Splits the position `:nth-of-type(n)` that ends a compound scope segment from the rest of it.
|
|
102
|
+
* The `:` of the suffix is a position only when it is not escaped, which a regex on the end of the
|
|
103
|
+
* segment cannot tell alone: a name that ends with `\` is written `\\`, so `a\\:nth-of-type(2)` is the
|
|
104
|
+
* class `a\` at the position 2, and `a\:nth-of-type(2)` is the class `a:nth-of-type(2)`. The escapes
|
|
105
|
+
* are scanned from the start, as {@link splitScopeIdents} does.
|
|
106
|
+
*
|
|
107
|
+
* @see https://www.w3.org/TR/css-syntax-3/#escaping
|
|
108
|
+
*/
|
|
109
|
+
export declare function splitScopeNth(segment: string): {
|
|
110
|
+
readonly base: string;
|
|
111
|
+
readonly nth: number | null;
|
|
112
|
+
};
|
|
54
113
|
export declare function getAncestorChain(node: ScopedNode): ScopedNode[];
|
|
55
114
|
/**
|
|
56
115
|
* @experimental
|
|
@@ -21,6 +21,190 @@
|
|
|
21
21
|
* @see https://github.com/markuplint/markuplint/issues/3509
|
|
22
22
|
*/
|
|
23
23
|
const BODY_HTML_TAGS = new Set(['body', 'html']);
|
|
24
|
+
/**
|
|
25
|
+
* The position that ends a segment of a scope: `:nth-of-type(n)`.
|
|
26
|
+
* A name never ends with it unescaped, because the `:` and the `()` of a name are written escaped
|
|
27
|
+
* ({@link serializeIdentifier}), so what ends a segment so is the position.
|
|
28
|
+
*/
|
|
29
|
+
const NTH_OF_TYPE = /^:nth-of-type\((\d+)\)$/;
|
|
30
|
+
/**
|
|
31
|
+
* The non-ASCII ident code points of CSS, inclusive: the same as HTML's valid custom element names.
|
|
32
|
+
* U+200C and U+200D (zero width joiners) are the other two, and are checked apart: a joiner in a character
|
|
33
|
+
* class makes the linter read the class as a joined character sequence.
|
|
34
|
+
* @see https://www.w3.org/TR/css-syntax-3/#non-ascii-ident-code-point
|
|
35
|
+
*/
|
|
36
|
+
const NON_ASCII_IDENT_CODE_POINT = /^[\u00B7\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u037D\u037F-\u1FFF\u203F\u2040\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\u{10000}-\u{10FFFF}]$/u;
|
|
37
|
+
/**
|
|
38
|
+
* @experimental
|
|
39
|
+
* Writes a tag name, a class name or an id into a scope, by the CSSOM algorithm "serialize an identifier"
|
|
40
|
+
* (the one `CSS.escape()` is specified by), in which a code point that is not an ident code point of CSS
|
|
41
|
+
* Syntax is escaped, so the output is always one identifier. A NUL is written as U+FFFD, as the algorithm
|
|
42
|
+
* has it, so it does not read back as NUL. A scope is a CSS selector, and CSS lets any code point be part
|
|
43
|
+
* of an identifier by escaping it, "even ones that CSS uses in its syntax". So `Tabs.Root` (a member tag of JSX, MDX or Svelte) is `Tabs\.Root` and not the element
|
|
44
|
+
* `Tabs` with the class `Root`, `py-1.5` is `py-1\.5`, `hover:bg-x` is `hover\:bg-x`, `2xl:flex` is
|
|
45
|
+
* `\32 xl\:flex`, and a class that ends with `:nth-of-type(2)` is no position. What this writes and what
|
|
46
|
+
* {@link unescapeIdentifier} reads are one form, so a scope matches the element it was made for.
|
|
47
|
+
*
|
|
48
|
+
* A scope file written before the escape holds names unescaped, and they still read as the syntax,
|
|
49
|
+
* because every scope that ever matched was written so (`unescapeIdentifier` leaves a character that is
|
|
50
|
+
* not escaped as it is). A scope for a name with a `.` never matched, except in the form
|
|
51
|
+
* `tag[attr="value"]`, whose tag has always been read as a name. Likewise a scope for a name that ends
|
|
52
|
+
* with `:nth-of-type(n)` (`div.a:nth-of-type(2)`) never matched, and is still read as a position. A `\`
|
|
53
|
+
* that such a file holds is read as CSS reads it: `\ba` is the hex escape U+00BA.
|
|
54
|
+
*
|
|
55
|
+
* @see https://drafts.csswg.org/cssom/#serialize-an-identifier
|
|
56
|
+
*/
|
|
57
|
+
export function serializeIdentifier(name) {
|
|
58
|
+
let result = '';
|
|
59
|
+
let index = 0;
|
|
60
|
+
for (const char of name) {
|
|
61
|
+
const isDigit = char >= '0' && char <= '9';
|
|
62
|
+
if (char === '\u0000') {
|
|
63
|
+
result += '\uFFFD';
|
|
64
|
+
}
|
|
65
|
+
else if (
|
|
66
|
+
// U+0001 to U+001F and U+007F
|
|
67
|
+
(char.codePointAt(0) >= 1 && char.codePointAt(0) <= 31) ||
|
|
68
|
+
char.codePointAt(0) === 127 ||
|
|
69
|
+
(index === 0 && isDigit) ||
|
|
70
|
+
(index === 1 && isDigit && name.startsWith('-'))) {
|
|
71
|
+
result += `\\${char.codePointAt(0).toString(16)} `;
|
|
72
|
+
}
|
|
73
|
+
else if (index === 0 && char === '-' && name.length === 1) {
|
|
74
|
+
result += String.raw `\-`;
|
|
75
|
+
}
|
|
76
|
+
else if (char === '-' ||
|
|
77
|
+
char === '_' ||
|
|
78
|
+
isDigit ||
|
|
79
|
+
/^[a-z]$/i.test(char) ||
|
|
80
|
+
NON_ASCII_IDENT_CODE_POINT.test(char) ||
|
|
81
|
+
char === '\u200C' ||
|
|
82
|
+
char === '\u200D') {
|
|
83
|
+
result += char;
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
result += `\\${char}`;
|
|
87
|
+
}
|
|
88
|
+
index++;
|
|
89
|
+
}
|
|
90
|
+
return result;
|
|
91
|
+
}
|
|
92
|
+
/** 1 to 6 hex digits of an escape, and the one whitespace that ends them. Sticky: it matches at `lastIndex`. */
|
|
93
|
+
const HEX_ESCAPE = /[\dA-F]{1,6}(?:\r\n|[\t\n\f\r ])?/iy;
|
|
94
|
+
/**
|
|
95
|
+
* The one scanner of a scope segment, which {@link unescapeIdentifier}, {@link splitScopeIdents},
|
|
96
|
+
* {@link splitScopeAttr} and {@link splitScopeNth} read with: yields each character (an escape is one
|
|
97
|
+
* character) with the index it starts at, and whether a `\` escaped it. The escapes are the ones of CSS.
|
|
98
|
+
*
|
|
99
|
+
* @see https://www.w3.org/TR/css-syntax-3/#consume-escaped-code-point
|
|
100
|
+
*/
|
|
101
|
+
function* scanScope(segment) {
|
|
102
|
+
for (let i = 0; i < segment.length; i++) {
|
|
103
|
+
const char = segment[i];
|
|
104
|
+
const next = segment[i + 1];
|
|
105
|
+
if (char !== '\\' || next === '\n' || next === '\r' || next === '\f') {
|
|
106
|
+
// A `\` before a newline is no escape
|
|
107
|
+
yield { index: i, char, escaped: false };
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (next === undefined) {
|
|
111
|
+
yield { index: i, char: '\uFFFD', escaped: true };
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
HEX_ESCAPE.lastIndex = i + 1;
|
|
115
|
+
const hex = HEX_ESCAPE.exec(segment);
|
|
116
|
+
if (hex) {
|
|
117
|
+
const codePoint = Number.parseInt(hex[0], 16);
|
|
118
|
+
// Zero, a surrogate (U+D800 to U+DFFF) and a value over U+10FFFF are U+FFFD
|
|
119
|
+
const isValid = codePoint !== 0 && !(codePoint >= 55_296 && codePoint <= 57_343) && codePoint <= 1_114_111;
|
|
120
|
+
yield { index: i, char: isValid ? String.fromCodePoint(codePoint) : '\uFFFD', escaped: true };
|
|
121
|
+
i += hex[0].length;
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
const escapedChar = String.fromCodePoint(segment.codePointAt(i + 1));
|
|
125
|
+
yield { index: i, char: escapedChar, escaped: true };
|
|
126
|
+
i += escapedChar.length;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* @experimental
|
|
131
|
+
* Reads an identifier of a scope, the reading side of {@link serializeIdentifier}: the escapes of CSS
|
|
132
|
+
* (`\` and 1 to 6 hex digits with one whitespace, or `\` and any other character), and a character that
|
|
133
|
+
* is not escaped as it is.
|
|
134
|
+
*/
|
|
135
|
+
export function unescapeIdentifier(escaped) {
|
|
136
|
+
let result = '';
|
|
137
|
+
for (const { char } of scanScope(escaped)) {
|
|
138
|
+
result += char;
|
|
139
|
+
}
|
|
140
|
+
return result;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* @experimental
|
|
144
|
+
* Splits a compound scope segment (`tag.classA.classB`) at its unescaped `.` and unescapes each part,
|
|
145
|
+
* the reading side of {@link generateUniqueSelector}. The first part is the tag, the rest are classes.
|
|
146
|
+
*/
|
|
147
|
+
export function splitScopeIdents(segment) {
|
|
148
|
+
const parts = [''];
|
|
149
|
+
for (const { char, escaped } of scanScope(segment)) {
|
|
150
|
+
if (!escaped && char === '.') {
|
|
151
|
+
parts.push('');
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
parts[parts.length - 1] += char;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return parts;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* @experimental
|
|
161
|
+
* Splits the form `tag[attr="value"]`. The tag is what comes before the first `[` that is not escaped,
|
|
162
|
+
* and it holds no unescaped `]` or `"`, so `Foo\[x="y"]` is no attribute: its `[` is escaped.
|
|
163
|
+
* The tag is returned as it is written.
|
|
164
|
+
*/
|
|
165
|
+
export function splitScopeAttr(segment) {
|
|
166
|
+
for (const { index, char, escaped } of scanScope(segment)) {
|
|
167
|
+
if (escaped) {
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
if (char === ']' || char === '"') {
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
if (char === '[') {
|
|
174
|
+
const match = /^\[([a-z-]+)="([^"]+)"\]$/.exec(segment.slice(index));
|
|
175
|
+
return index > 0 && match
|
|
176
|
+
? { tag: segment.slice(0, index), attrName: match[1], attrValue: match[2] }
|
|
177
|
+
: null;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* @experimental
|
|
184
|
+
* Splits the position `:nth-of-type(n)` that ends a compound scope segment from the rest of it.
|
|
185
|
+
* The `:` of the suffix is a position only when it is not escaped, which a regex on the end of the
|
|
186
|
+
* segment cannot tell alone: a name that ends with `\` is written `\\`, so `a\\:nth-of-type(2)` is the
|
|
187
|
+
* class `a\` at the position 2, and `a\:nth-of-type(2)` is the class `a:nth-of-type(2)`. The escapes
|
|
188
|
+
* are scanned from the start, as {@link splitScopeIdents} does.
|
|
189
|
+
*
|
|
190
|
+
* @see https://www.w3.org/TR/css-syntax-3/#escaping
|
|
191
|
+
*/
|
|
192
|
+
export function splitScopeNth(segment) {
|
|
193
|
+
// Only an unescaped `:` is looked at: an escaped one is read with its `\` before it, as one character
|
|
194
|
+
for (const { index, char, escaped } of scanScope(segment)) {
|
|
195
|
+
if (!escaped && char === ':') {
|
|
196
|
+
const match = NTH_OF_TYPE.exec(segment.slice(index));
|
|
197
|
+
if (match) {
|
|
198
|
+
return { base: segment.slice(0, index), nth: Number.parseInt(match[1], 10) };
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return { base: segment, nth: null };
|
|
203
|
+
}
|
|
204
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
205
|
+
function tagWithClasses(node, classes) {
|
|
206
|
+
return [node.localName, ...classes].map(name => serializeIdentifier(name)).join('.');
|
|
207
|
+
}
|
|
24
208
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
25
209
|
export function getAncestorChain(node) {
|
|
26
210
|
const chain = [];
|
|
@@ -97,12 +281,12 @@ export function generateUniqueSelector(node) {
|
|
|
97
281
|
}
|
|
98
282
|
// If this node has an id, just use it
|
|
99
283
|
if (node.id) {
|
|
100
|
-
return `#${node.id}`;
|
|
284
|
+
return `#${serializeIdentifier(node.id)}`;
|
|
101
285
|
}
|
|
102
286
|
// If this node has classes, use tag.classA.classB (all classes)
|
|
103
287
|
const classes = [...node.classList];
|
|
104
288
|
if (classes.length > 0) {
|
|
105
|
-
return
|
|
289
|
+
return tagWithClasses(node, classes);
|
|
106
290
|
}
|
|
107
291
|
// If this node has a distinguishing attribute (role, or type for input),
|
|
108
292
|
// use tag[attr="value"] — more readable than nth-of-type and more stable
|
|
@@ -116,12 +300,12 @@ export function generateUniqueSelector(node) {
|
|
|
116
300
|
let current = node.parentElement;
|
|
117
301
|
while (current && !BODY_HTML_TAGS.has(current.localName)) {
|
|
118
302
|
if (current.id) {
|
|
119
|
-
parts.unshift(`#${current.id}`);
|
|
303
|
+
parts.unshift(`#${serializeIdentifier(current.id)}`);
|
|
120
304
|
break;
|
|
121
305
|
}
|
|
122
306
|
const currentClasses = [...current.classList];
|
|
123
307
|
if (currentClasses.length > 0) {
|
|
124
|
-
parts.unshift(
|
|
308
|
+
parts.unshift(tagWithClasses(current, currentClasses));
|
|
125
309
|
break;
|
|
126
310
|
}
|
|
127
311
|
const currentAttr = getDistinguishingAttrSelector(current);
|
|
@@ -150,8 +334,9 @@ function getDistinguishingAttrSelector(node) {
|
|
|
150
334
|
const attrsToCheck = [...universalAttrs, ...elementAttrs];
|
|
151
335
|
for (const attrName of attrsToCheck) {
|
|
152
336
|
for (const attr of node.attributes) {
|
|
153
|
-
|
|
154
|
-
|
|
337
|
+
// A value with `"` or ` > ` cannot be held by `tag[attr="value"]`: a scope ends the value at `"` and splits at ` > `.
|
|
338
|
+
if (attr.name === attrName && attr.value && !attr.value.includes('"') && !attr.value.includes(' > ')) {
|
|
339
|
+
return `${serializeIdentifier(node.localName)}[${attrName}="${attr.value}"]`;
|
|
155
340
|
}
|
|
156
341
|
}
|
|
157
342
|
}
|
|
@@ -160,8 +345,9 @@ function getDistinguishingAttrSelector(node) {
|
|
|
160
345
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
161
346
|
function buildSegmentWithNth(node) {
|
|
162
347
|
const parent = node.parentElement;
|
|
348
|
+
const name = serializeIdentifier(node.localName);
|
|
163
349
|
if (!parent) {
|
|
164
|
-
return
|
|
350
|
+
return name;
|
|
165
351
|
}
|
|
166
352
|
// Count same-tag siblings and find this node's position
|
|
167
353
|
let sameTagCount = 0;
|
|
@@ -175,9 +361,9 @@ function buildSegmentWithNth(node) {
|
|
|
175
361
|
}
|
|
176
362
|
}
|
|
177
363
|
if (sameTagCount > 1) {
|
|
178
|
-
return `${
|
|
364
|
+
return `${name}:nth-of-type(${position})`;
|
|
179
365
|
}
|
|
180
|
-
return
|
|
366
|
+
return name;
|
|
181
367
|
}
|
|
182
368
|
/**
|
|
183
369
|
* @experimental
|
|
@@ -94,6 +94,6 @@ _nodeList) {
|
|
|
94
94
|
// fall back to file-level filtering (treats all violations as in-scope).
|
|
95
95
|
// This is the safe direction — more violations may be downgraded than intended,
|
|
96
96
|
// but no new violations are hidden.
|
|
97
|
-
// TODO(#3536): Export
|
|
97
|
+
// TODO(#3536): Export the scope matching of apply-suppressions.ts (`parseSegment` and `isViolationInScope`) and reuse here.
|
|
98
98
|
return group;
|
|
99
99
|
}
|
|
@@ -10,6 +10,9 @@ export type SuppressionEntry = {
|
|
|
10
10
|
* CSS selector identifying the LCA (Lowest Common Ancestor) subtree
|
|
11
11
|
* that contains all violations for this rule. Uses id/class/attr for
|
|
12
12
|
* precise matching. When absent, suppression applies to the entire file.
|
|
13
|
+
*
|
|
14
|
+
* A `.`, `#`, `[` or `\` in a tag name or a class name is escaped with a `\` (`Tabs\.Root`),
|
|
15
|
+
* as CSS does. A scope written before the escape has none, and is read as it was written.
|
|
13
16
|
*/
|
|
14
17
|
readonly scope?: string;
|
|
15
18
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "markuplint",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.4.0",
|
|
4
4
|
"description": "An HTML linter for all markup developers",
|
|
5
5
|
"author": "Yusuke Hirao",
|
|
6
6
|
"license": "MIT",
|
|
@@ -15,11 +15,10 @@
|
|
|
15
15
|
"type": "module",
|
|
16
16
|
"exports": {
|
|
17
17
|
".": {
|
|
18
|
-
"
|
|
19
|
-
"
|
|
18
|
+
"types": "./lib/index.d.ts",
|
|
19
|
+
"default": "./lib/index.js"
|
|
20
20
|
},
|
|
21
21
|
"./suppressions": {
|
|
22
|
-
"import": "./lib/suppressions/index.js",
|
|
23
22
|
"types": "./lib/suppressions/index.d.ts",
|
|
24
23
|
"default": "./lib/suppressions/index.js"
|
|
25
24
|
},
|
|
@@ -35,17 +34,17 @@
|
|
|
35
34
|
"clean": "tsc --build --clean tsconfig.build.json"
|
|
36
35
|
},
|
|
37
36
|
"dependencies": {
|
|
38
|
-
"@markuplint/cli-utils": "5.
|
|
39
|
-
"@markuplint/file-resolver": "5.
|
|
40
|
-
"@markuplint/html-parser": "5.
|
|
41
|
-
"@markuplint/html-spec": "5.
|
|
42
|
-
"@markuplint/i18n": "5.
|
|
43
|
-
"@markuplint/ml-ast": "5.
|
|
44
|
-
"@markuplint/ml-config": "5.
|
|
45
|
-
"@markuplint/ml-core": "5.
|
|
46
|
-
"@markuplint/ml-spec": "5.
|
|
47
|
-
"@markuplint/rules": "5.
|
|
48
|
-
"@markuplint/shared": "5.
|
|
37
|
+
"@markuplint/cli-utils": "5.3.0",
|
|
38
|
+
"@markuplint/file-resolver": "5.4.0",
|
|
39
|
+
"@markuplint/html-parser": "5.4.0",
|
|
40
|
+
"@markuplint/html-spec": "5.4.0",
|
|
41
|
+
"@markuplint/i18n": "5.3.0",
|
|
42
|
+
"@markuplint/ml-ast": "5.3.0",
|
|
43
|
+
"@markuplint/ml-config": "5.4.0",
|
|
44
|
+
"@markuplint/ml-core": "5.4.0",
|
|
45
|
+
"@markuplint/ml-spec": "5.4.0",
|
|
46
|
+
"@markuplint/rules": "5.4.0",
|
|
47
|
+
"@markuplint/shared": "5.4.0",
|
|
49
48
|
"@types/debug": "^4.1.13",
|
|
50
49
|
"chokidar": "^5.0.0",
|
|
51
50
|
"debug": "^4.4.3",
|
|
@@ -55,5 +54,5 @@
|
|
|
55
54
|
"strip-ansi": "^7.2.0",
|
|
56
55
|
"type-fest": "^5.10.0"
|
|
57
56
|
},
|
|
58
|
-
"gitHead": "
|
|
57
|
+
"gitHead": "647f6fb5d5743f060678599c97e9c84b58970747"
|
|
59
58
|
}
|