@markuplint/rules 3.0.0 → 3.1.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/lib/attr-check.d.ts +1 -1
- package/lib/attr-check.js +6 -0
- package/lib/debug.js +1 -1
- package/lib/helpers.d.ts +1 -1
- package/lib/index.js +2 -0
- package/lib/invalid-attr/index.js +7 -0
- package/lib/no-empty-palpable-content/index.js +8 -0
- package/lib/no-refer-to-non-existent-id/index.d.ts +1 -0
- package/lib/no-refer-to-non-existent-id/index.js +67 -2
- package/lib/permitted-contents/choice.js +34 -1
- package/lib/permitted-contents/count-pattern.js +62 -62
- package/lib/permitted-contents/matches-selector.js +4 -4
- package/lib/permitted-contents/order.js +20 -12
- package/lib/permitted-contents/types.d.ts +10 -4
- package/lib/permitted-contents/utils.d.ts +19 -1
- package/lib/permitted-contents/utils.js +105 -1
- package/lib/placeholder-label-option/index.d.ts +2 -0
- package/lib/placeholder-label-option/index.js +47 -0
- package/lib/require-datetime/index.d.ts +6 -0
- package/lib/require-datetime/index.js +34 -0
- package/lib/require-datetime/types.d.ts +10 -0
- package/lib/require-datetime/types.js +2 -0
- package/lib/require-datetime/utils.d.ts +11 -0
- package/lib/require-datetime/utils.js +134 -0
- package/package.json +6 -5
- package/schema.json +3 -0
package/lib/attr-check.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Translator } from '@markuplint/i18n';
|
|
2
2
|
import type { Attribute as AttrSpec, AttributeType } from '@markuplint/ml-spec';
|
|
3
3
|
type Invalid = {
|
|
4
|
-
invalidType: 'non-existent' | 'invalid-value';
|
|
4
|
+
invalidType: 'non-existent' | 'invalid-value' | 'disallowed-attr';
|
|
5
5
|
message: string;
|
|
6
6
|
loc?: Loc;
|
|
7
7
|
};
|
package/lib/attr-check.js
CHANGED
|
@@ -41,6 +41,12 @@ function attrCheck(t, name, value, isCustomRule, spec) {
|
|
|
41
41
|
t('Did you mean "{0*}"?', spec.name),
|
|
42
42
|
};
|
|
43
43
|
}
|
|
44
|
+
if (spec.noUse) {
|
|
45
|
+
return {
|
|
46
|
+
invalidType: 'disallowed-attr',
|
|
47
|
+
message: t('{0} is {1:c}', t('the "{0*}" {1}', name, 'attribute'), 'disallowed'),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
44
50
|
const types = Array.isArray(spec.type) ? spec.type : [spec.type];
|
|
45
51
|
const invalidList = types.map(type => {
|
|
46
52
|
if (!type) {
|
package/lib/debug.js
CHANGED
|
@@ -3,4 +3,4 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.log = void 0;
|
|
4
4
|
const tslib_1 = require("tslib");
|
|
5
5
|
const debug_1 = tslib_1.__importDefault(require("debug"));
|
|
6
|
-
exports.log = (0, debug_1.default)('
|
|
6
|
+
exports.log = (0, debug_1.default)('ml-rules');
|
package/lib/helpers.d.ts
CHANGED
|
@@ -21,7 +21,7 @@ export declare function match(needle: string, pattern: string): boolean;
|
|
|
21
21
|
*/
|
|
22
22
|
export declare const rePCENChar: string;
|
|
23
23
|
export declare function isValidAttr(t: Translator, name: string, value: string, isDynamicValue: boolean, node: Element<any, any>, attrSpecs: Attribute[], log?: Log): false | {
|
|
24
|
-
invalidType: "non-existent" | "invalid-value";
|
|
24
|
+
invalidType: "non-existent" | "invalid-value" | "disallowed-attr";
|
|
25
25
|
message: string;
|
|
26
26
|
loc?: {
|
|
27
27
|
raw: string;
|
package/lib/index.js
CHANGED
|
@@ -25,6 +25,7 @@ const no_refer_to_non_existent_id_1 = tslib_1.__importDefault(require("./no-refe
|
|
|
25
25
|
const no_use_event_handler_attr_1 = tslib_1.__importDefault(require("./no-use-event-handler-attr"));
|
|
26
26
|
const permitted_contents_1 = tslib_1.__importDefault(require("./permitted-contents"));
|
|
27
27
|
const require_accessible_name_1 = tslib_1.__importDefault(require("./require-accessible-name"));
|
|
28
|
+
const require_datetime_1 = tslib_1.__importDefault(require("./require-datetime"));
|
|
28
29
|
const required_attr_1 = tslib_1.__importDefault(require("./required-attr"));
|
|
29
30
|
const required_element_1 = tslib_1.__importDefault(require("./required-element"));
|
|
30
31
|
const required_h1_1 = tslib_1.__importDefault(require("./required-h1"));
|
|
@@ -55,6 +56,7 @@ exports.default = {
|
|
|
55
56
|
'no-use-event-handler-attr': no_use_event_handler_attr_1.default,
|
|
56
57
|
'permitted-contents': permitted_contents_1.default,
|
|
57
58
|
'require-accessible-name': require_accessible_name_1.default,
|
|
59
|
+
'require-datetime': require_datetime_1.default,
|
|
58
60
|
'required-attr': required_attr_1.default,
|
|
59
61
|
'required-element': required_element_1.default,
|
|
60
62
|
'required-h1': required_h1_1.default,
|
|
@@ -77,6 +77,13 @@ exports.default = (0, ml_core_1.createRule)({
|
|
|
77
77
|
}
|
|
78
78
|
if (invalid) {
|
|
79
79
|
switch (invalid.invalidType) {
|
|
80
|
+
case 'disallowed-attr': {
|
|
81
|
+
report({
|
|
82
|
+
scope: attr,
|
|
83
|
+
message: invalid.message,
|
|
84
|
+
});
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
80
87
|
case 'invalid-value': {
|
|
81
88
|
if (attr.isDynamicValue) {
|
|
82
89
|
break;
|
|
@@ -10,6 +10,14 @@ exports.default = (0, ml_core_1.createRule)({
|
|
|
10
10
|
},
|
|
11
11
|
async verify({ document, report, t }) {
|
|
12
12
|
await document.walkOn('Element', el => {
|
|
13
|
+
/**
|
|
14
|
+
* Exception
|
|
15
|
+
*
|
|
16
|
+
* - The `textarea` element is possibly empty because a user inputs it.
|
|
17
|
+
*/
|
|
18
|
+
if (el.localName === 'textarea') {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
13
21
|
if (!(0, ml_spec_1.isPalpableElement)(el, el.ownerMLDocument.specs, {
|
|
14
22
|
extendsSvg: false,
|
|
15
23
|
extendsExposableElements: el.rule.options.extendsExposableElements,
|
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
const ml_core_1 = require("@markuplint/ml-core");
|
|
4
|
+
const HYPERLINK_SELECTOR = 'a[href], area[href]';
|
|
4
5
|
exports.default = (0, ml_core_1.createRule)({
|
|
5
6
|
defaultOptions: {
|
|
6
7
|
ariaVersion: '1.2',
|
|
8
|
+
fragmentRefersNameAttr: false,
|
|
7
9
|
},
|
|
8
10
|
async verify({ document, report, t }) {
|
|
9
11
|
const idList = new Set();
|
|
12
|
+
const nameList = new Set();
|
|
10
13
|
let hasDynamicId = false;
|
|
11
|
-
|
|
12
|
-
|
|
14
|
+
let hasDynamicName = false;
|
|
15
|
+
document.querySelectorAll('[id]').forEach(el => {
|
|
16
|
+
const attr = el.getAttributeNode('id');
|
|
17
|
+
if (!attr) {
|
|
13
18
|
return;
|
|
14
19
|
}
|
|
15
20
|
if (attr.isDynamicValue) {
|
|
@@ -22,6 +27,18 @@ exports.default = (0, ml_core_1.createRule)({
|
|
|
22
27
|
if (hasDynamicId) {
|
|
23
28
|
return;
|
|
24
29
|
}
|
|
30
|
+
document.querySelectorAll('[name]').forEach(el => {
|
|
31
|
+
const attr = el.getAttributeNode('name');
|
|
32
|
+
if (!attr) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
if (attr.isDynamicValue) {
|
|
36
|
+
hasDynamicName = true;
|
|
37
|
+
}
|
|
38
|
+
if (attr.valueType !== 'code') {
|
|
39
|
+
nameList.add(attr.value);
|
|
40
|
+
}
|
|
41
|
+
});
|
|
25
42
|
await document.walkOn('Attr', attr => {
|
|
26
43
|
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;
|
|
27
44
|
const attrSpec = (0, ml_core_1.getAttrSpecs)(attr.ownerElement, document.specs);
|
|
@@ -100,5 +117,53 @@ exports.default = (0, ml_core_1.createRule)({
|
|
|
100
117
|
}
|
|
101
118
|
}
|
|
102
119
|
});
|
|
120
|
+
/**
|
|
121
|
+
* @see https://html.spec.whatwg.org/multipage/browsing-the-web.html#scrolling-to-a-fragment
|
|
122
|
+
*/
|
|
123
|
+
await document.walkOn('Element', el => {
|
|
124
|
+
var _a, _b, _c, _d;
|
|
125
|
+
if (el.rule.options.fragmentRefersNameAttr && hasDynamicName) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
if (!el.matches(HYPERLINK_SELECTOR)) {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
const href = el.getAttributeNode('href');
|
|
132
|
+
if (!href) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
const rawFragment = (_a = href.value.match(/^#(.+)/)) === null || _a === void 0 ? void 0 : _a[1];
|
|
136
|
+
if (rawFragment == null) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
const decodedFragment = decode(rawFragment);
|
|
140
|
+
// > 2. If fragment is the empty string, then return the special value top of the document.
|
|
141
|
+
// >
|
|
142
|
+
// > 9. If decodedFragment is an ASCII case-insensitive match for the string top, then return the top of the document.
|
|
143
|
+
if (decodedFragment === '' || /^top$/i.test(decodedFragment)) {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
if (!idList.has(decodedFragment) &&
|
|
147
|
+
(el.rule.options.fragmentRefersNameAttr ? !nameList.has(decodedFragment) : true)) {
|
|
148
|
+
report({
|
|
149
|
+
scope: href,
|
|
150
|
+
line: (_b = href.valueNode) === null || _b === void 0 ? void 0 : _b.startLine,
|
|
151
|
+
col: (_c = href.valueNode) === null || _c === void 0 ? void 0 : _c.startCol,
|
|
152
|
+
raw: (_d = href.valueNode) === null || _d === void 0 ? void 0 : _d.raw,
|
|
153
|
+
message: t('Missing {0}', t('"{0*}" ID', decodedFragment)),
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
});
|
|
103
157
|
},
|
|
104
158
|
});
|
|
159
|
+
function decode(fragment) {
|
|
160
|
+
try {
|
|
161
|
+
return decodeURI(fragment);
|
|
162
|
+
}
|
|
163
|
+
catch (e) {
|
|
164
|
+
if (e instanceof URIError) {
|
|
165
|
+
return fragment;
|
|
166
|
+
}
|
|
167
|
+
throw e;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.choice = void 0;
|
|
4
|
+
const debug_1 = require("./debug");
|
|
4
5
|
const order_1 = require("./order");
|
|
5
6
|
const utils_1 = require("./utils");
|
|
7
|
+
const indexes = new WeakMap();
|
|
6
8
|
function choice(pattern, elements, specs, options, depth) {
|
|
9
|
+
const choiceLog = debug_1.cmLog.extend(`choice#${depth}`);
|
|
7
10
|
const collection = new utils_1.Collection(elements);
|
|
8
11
|
const unmatchedResults = [];
|
|
12
|
+
let i = 0;
|
|
9
13
|
for (const some of pattern.choice) {
|
|
14
|
+
choiceLog('Patterns[%s]: %s', i, (0, utils_1.modelLog)(some, ''));
|
|
10
15
|
const result = (0, order_1.order)(some, collection.unmatched, specs, options, depth + 1);
|
|
11
16
|
if (result.type === 'UNEXPECTED_EXTRA_NODE' || result.type === 'MATCHED' || result.type === 'MATCHED_ZERO') {
|
|
17
|
+
choiceLog('Results[%s]: %s', i, choiceLogString(pattern.choice, i));
|
|
12
18
|
collection.addMatched(result.matched);
|
|
13
19
|
return {
|
|
14
20
|
type: result.type,
|
|
@@ -20,11 +26,27 @@ function choice(pattern, elements, specs, options, depth) {
|
|
|
20
26
|
};
|
|
21
27
|
}
|
|
22
28
|
unmatchedResults.push(result);
|
|
29
|
+
indexes.set(result, i);
|
|
30
|
+
i++;
|
|
23
31
|
}
|
|
24
|
-
const barelyMatchedResult = unmatchedResults.sort((a, b) =>
|
|
32
|
+
const barelyMatchedResult = unmatchedResults.sort((a, b) => {
|
|
33
|
+
var _c, _d, _e, _f;
|
|
34
|
+
const computed1 = b.matched.length - a.matched.length;
|
|
35
|
+
if (computed1 !== 0) {
|
|
36
|
+
return computed1;
|
|
37
|
+
}
|
|
38
|
+
const _a = (_d = (_c = a.hint.missing) === null || _c === void 0 ? void 0 : _c.barelyMatchedElements) !== null && _d !== void 0 ? _d : 0;
|
|
39
|
+
const _b = (_f = (_e = b.hint.missing) === null || _e === void 0 ? void 0 : _e.barelyMatchedElements) !== null && _f !== void 0 ? _f : 0;
|
|
40
|
+
const computed2 = _b - _a;
|
|
41
|
+
return computed2;
|
|
42
|
+
})[0];
|
|
25
43
|
if (!barelyMatchedResult) {
|
|
26
44
|
throw new Error('Unreachable code');
|
|
27
45
|
}
|
|
46
|
+
const index = indexes.get(barelyMatchedResult);
|
|
47
|
+
if (index != null) {
|
|
48
|
+
choiceLog('Results[%s]: %s', index, choiceLogString(pattern.choice, index, true));
|
|
49
|
+
}
|
|
28
50
|
return {
|
|
29
51
|
type: barelyMatchedResult.type,
|
|
30
52
|
matched: collection.matched,
|
|
@@ -35,3 +57,14 @@ function choice(pattern, elements, specs, options, depth) {
|
|
|
35
57
|
};
|
|
36
58
|
}
|
|
37
59
|
exports.choice = choice;
|
|
60
|
+
function choiceLogString(choice, index, barely = false) {
|
|
61
|
+
const colorFn = barely ? debug_1.bgBlue : debug_1.bgGreen;
|
|
62
|
+
return choice
|
|
63
|
+
.map((pattern, i) => {
|
|
64
|
+
if (i === index) {
|
|
65
|
+
return colorFn((0, utils_1.modelLog)(pattern, ''));
|
|
66
|
+
}
|
|
67
|
+
return (0, utils_1.modelLog)(pattern, '');
|
|
68
|
+
})
|
|
69
|
+
.join(', ');
|
|
70
|
+
}
|
|
@@ -15,94 +15,72 @@ const utils_1 = require("./utils");
|
|
|
15
15
|
* @returns
|
|
16
16
|
*/
|
|
17
17
|
function countPattern(pattern, elements, specs, options, depth) {
|
|
18
|
-
var _a, _b, _c, _d, _e;
|
|
19
18
|
const ptLog = debug_1.cmLog.extend(`countPattern#${depth}`);
|
|
20
19
|
const collection = new utils_1.Collection(elements);
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
let model;
|
|
24
|
-
let type;
|
|
25
|
-
let _type;
|
|
26
|
-
if ((0, utils_1.isRequire)(pattern)) {
|
|
27
|
-
min = (_a = pattern.min) !== null && _a !== void 0 ? _a : 1;
|
|
28
|
-
max = Math.max((_b = pattern.max) !== null && _b !== void 0 ? _b : 1, min);
|
|
29
|
-
model = pattern.require;
|
|
30
|
-
type = 'MISSING_NODE_REQUIRED';
|
|
31
|
-
_type = 'require';
|
|
32
|
-
}
|
|
33
|
-
else if ((0, utils_1.isOptional)(pattern)) {
|
|
34
|
-
min = 0;
|
|
35
|
-
max = Math.max((_c = pattern.max) !== null && _c !== void 0 ? _c : 1, 1);
|
|
36
|
-
model = pattern.optional;
|
|
37
|
-
_type = 'optional';
|
|
38
|
-
}
|
|
39
|
-
else if ((0, utils_1.isOneOrMore)(pattern)) {
|
|
40
|
-
min = 1;
|
|
41
|
-
max = Math.max((_d = pattern.max) !== null && _d !== void 0 ? _d : Infinity, 1);
|
|
42
|
-
model = pattern.oneOrMore;
|
|
43
|
-
type = 'MISSING_NODE_ONE_OR_MORE';
|
|
44
|
-
_type = 'one or more';
|
|
45
|
-
}
|
|
46
|
-
else if ((0, utils_1.isZeroOrMore)(pattern)) {
|
|
47
|
-
min = 0;
|
|
48
|
-
max = Math.max((_e = pattern.max) !== null && _e !== void 0 ? _e : Infinity, 1);
|
|
49
|
-
model = pattern.zeroOrMore;
|
|
50
|
-
_type = 'zero or more';
|
|
51
|
-
}
|
|
52
|
-
else {
|
|
53
|
-
throw new Error('Unreachable code');
|
|
54
|
-
}
|
|
55
|
-
ptLog('%s: %o', _type, model);
|
|
20
|
+
const { model, min, max, repeat, missingType } = (0, utils_1.normalizeModel)(pattern);
|
|
21
|
+
ptLog('Model:\n RegEx: %s\n Schema: %o', (0, utils_1.modelLog)(model, repeat), pattern);
|
|
56
22
|
let prevResult = null;
|
|
23
|
+
let barelyResult = null;
|
|
24
|
+
let loopCount = 0;
|
|
57
25
|
// eslint-disable-next-line no-constant-condition
|
|
58
26
|
while (true) {
|
|
59
|
-
|
|
27
|
+
loopCount++;
|
|
28
|
+
ptLog('Check#%s: %s', loopCount, collection);
|
|
60
29
|
const result = (0, recursive_branch_1.recursiveBranch)(model, collection.unmatched, specs, options, depth);
|
|
61
30
|
const added = collection.addMatched(result.matched);
|
|
31
|
+
const { matchedCount } = collection;
|
|
62
32
|
if (result.type === 'UNMATCHED_SELECTOR_BUT_MAY_EMPTY') {
|
|
63
|
-
ptLog('MATCHED_ZERO: %s',
|
|
64
|
-
return {
|
|
33
|
+
ptLog('MATCHED_ZERO:\n model: %s\n max: %s\n collection: %s\n matched element: %s', (0, utils_1.modelLog)(model, repeat), max, collection, matchedCount);
|
|
34
|
+
return compereResult({
|
|
65
35
|
type: 'MATCHED_ZERO',
|
|
66
36
|
matched: collection.matched,
|
|
67
37
|
unmatched: collection.unmatched,
|
|
68
38
|
zeroMatch: true,
|
|
69
39
|
query: result.query,
|
|
70
40
|
hint: result.hint,
|
|
71
|
-
};
|
|
41
|
+
}, barelyResult);
|
|
72
42
|
}
|
|
73
43
|
if (max < collection.matchedCount) {
|
|
74
44
|
collection.max(max);
|
|
75
|
-
ptLog('UNEXPECTED_EXTRA_NODE
|
|
76
|
-
return {
|
|
45
|
+
ptLog('UNEXPECTED_EXTRA_NODE:\n model: %s\n max: %s\n collection: %s\n matched element: %s', (0, utils_1.modelLog)(model, repeat), max, collection, matchedCount);
|
|
46
|
+
return compereResult({
|
|
77
47
|
type: 'UNEXPECTED_EXTRA_NODE',
|
|
78
48
|
matched: collection.matched,
|
|
79
49
|
unmatched: collection.unmatched,
|
|
80
50
|
zeroMatch: result.zeroMatch,
|
|
81
51
|
query: result.query,
|
|
82
|
-
hint: {
|
|
83
|
-
|
|
84
|
-
max,
|
|
85
|
-
},
|
|
86
|
-
};
|
|
52
|
+
hint: (0, utils_1.mergeHints)(result.hint, { max }),
|
|
53
|
+
}, barelyResult);
|
|
87
54
|
}
|
|
88
55
|
if (prevResult) {
|
|
89
56
|
if (result.type === 'MISSING_NODE_ONE_OR_MORE' ||
|
|
90
57
|
result.type === 'MISSING_NODE_REQUIRED' ||
|
|
91
58
|
result.type === 'TRANSPARENT_MODEL_DISALLOWS') {
|
|
92
|
-
ptLog('%s(continued): %s', result.type, collection);
|
|
93
|
-
return {
|
|
59
|
+
ptLog('%s(continued): %s; Needs', result.type, collection, result.query);
|
|
60
|
+
return compereResult({
|
|
94
61
|
type: result.type,
|
|
95
62
|
matched: collection.matched,
|
|
96
63
|
unmatched: collection.unmatched,
|
|
97
64
|
zeroMatch: result.zeroMatch,
|
|
98
65
|
query: result.query,
|
|
99
66
|
hint: result.hint,
|
|
100
|
-
};
|
|
67
|
+
}, barelyResult);
|
|
101
68
|
}
|
|
102
69
|
ptLog('%s(continued): %s', prevResult.type, collection);
|
|
103
|
-
return prevResult;
|
|
70
|
+
return compereResult(prevResult, barelyResult);
|
|
104
71
|
}
|
|
105
72
|
if (added && collection.unmatched.length > 0) {
|
|
73
|
+
if (result.type !== 'MISSING_NODE' && result.type !== 'UNMATCHED_SELECTORS') {
|
|
74
|
+
barelyResult = {
|
|
75
|
+
type: result.type,
|
|
76
|
+
matched: collection.matched,
|
|
77
|
+
unmatched: collection.unmatched,
|
|
78
|
+
zeroMatch: result.zeroMatch,
|
|
79
|
+
query: result.query,
|
|
80
|
+
hint: result.hint,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
ptLog('continue⤴️');
|
|
106
84
|
continue;
|
|
107
85
|
}
|
|
108
86
|
if (collection.matchedCount + (result.zeroMatch ? 1 : 0) < min) {
|
|
@@ -110,16 +88,21 @@ function countPattern(pattern, elements, specs, options, depth) {
|
|
|
110
88
|
result.type === 'MISSING_NODE_ONE_OR_MORE' ||
|
|
111
89
|
result.type === 'TRANSPARENT_MODEL_DISALLOWS'
|
|
112
90
|
? result.type
|
|
113
|
-
:
|
|
114
|
-
ptLog('%s(in %s)
|
|
115
|
-
return {
|
|
91
|
+
: missingType !== null && missingType !== void 0 ? missingType : 'MISSING_NODE_REQUIRED';
|
|
92
|
+
ptLog('%s(in %s); Needs %s', resultType, missingType, result.query);
|
|
93
|
+
return compereResult({
|
|
116
94
|
type: resultType,
|
|
117
95
|
matched: collection.matched,
|
|
118
96
|
unmatched: collection.unmatched,
|
|
119
97
|
zeroMatch: result.zeroMatch,
|
|
120
98
|
query: result.query,
|
|
121
|
-
hint: result.hint,
|
|
122
|
-
|
|
99
|
+
hint: (0, utils_1.mergeHints)(result.hint, {
|
|
100
|
+
missing: {
|
|
101
|
+
barelyMatchedElements: collection.matched.length,
|
|
102
|
+
need: result.query,
|
|
103
|
+
},
|
|
104
|
+
}),
|
|
105
|
+
}, barelyResult);
|
|
123
106
|
}
|
|
124
107
|
const resultType = collection.matched.length === 0 ? 'MATCHED_ZERO' : 'MATCHED';
|
|
125
108
|
const zeroMatch = result.zeroMatch || min === 0 || resultType === 'MATCHED_ZERO';
|
|
@@ -129,27 +112,44 @@ function countPattern(pattern, elements, specs, options, depth) {
|
|
|
129
112
|
unmatched: collection.unmatched,
|
|
130
113
|
zeroMatch,
|
|
131
114
|
query: result.query,
|
|
132
|
-
hint: result.hint,
|
|
115
|
+
hint: (0, utils_1.mergeHints)(result.hint, {
|
|
116
|
+
missing: {
|
|
117
|
+
barelyMatchedElements: collection.matched.length,
|
|
118
|
+
need: result.query,
|
|
119
|
+
},
|
|
120
|
+
}),
|
|
133
121
|
};
|
|
134
122
|
if (!prevResult && collection.unmatched.length) {
|
|
135
|
-
ptLog('continue checking');
|
|
136
123
|
prevResult = matchedResult;
|
|
124
|
+
ptLog('continue⤴️ (add prev)');
|
|
137
125
|
continue;
|
|
138
126
|
}
|
|
139
|
-
ptLog('%s: %s', resultType, collection);
|
|
127
|
+
ptLog('%s:\n model: %s\n max: %s\n collection: %s\n matched element: %s', resultType, (0, utils_1.modelLog)(model, repeat), max, collection, matchedCount);
|
|
140
128
|
if (result.type === 'MISSING_NODE_REQUIRED' ||
|
|
141
129
|
result.type === 'MISSING_NODE_ONE_OR_MORE' ||
|
|
142
130
|
result.type === 'TRANSPARENT_MODEL_DISALLOWS') {
|
|
143
|
-
return {
|
|
131
|
+
return compereResult({
|
|
144
132
|
type: result.type,
|
|
145
133
|
matched: collection.matched,
|
|
146
134
|
unmatched: collection.unmatched,
|
|
147
135
|
zeroMatch: result.zeroMatch,
|
|
148
136
|
query: result.query,
|
|
149
137
|
hint: result.hint,
|
|
150
|
-
};
|
|
138
|
+
}, barelyResult);
|
|
151
139
|
}
|
|
152
|
-
return matchedResult;
|
|
140
|
+
return compereResult(matchedResult, barelyResult);
|
|
153
141
|
}
|
|
154
142
|
}
|
|
155
143
|
exports.countPattern = countPattern;
|
|
144
|
+
const cLog = debug_1.cmLog.extend('countCompereResult');
|
|
145
|
+
function compereResult(a, b) {
|
|
146
|
+
cLog('current: %s %O\nbarely: %s %O', a.type, a.hint, b === null || b === void 0 ? void 0 : b.type, b === null || b === void 0 ? void 0 : b.hint);
|
|
147
|
+
if (b == null) {
|
|
148
|
+
return a;
|
|
149
|
+
}
|
|
150
|
+
if (a.type === 'MATCHED' || a.type === 'MATCHED_ZERO' || a.type === 'UNEXPECTED_EXTRA_NODE') {
|
|
151
|
+
return a;
|
|
152
|
+
}
|
|
153
|
+
const result = [a, b].sort((a, b) => { var _a, _b, _c, _d; return ((_b = (_a = b.hint.missing) === null || _a === void 0 ? void 0 : _a.barelyMatchedElements) !== null && _b !== void 0 ? _b : 0) - ((_d = (_c = a.hint.missing) === null || _c === void 0 ? void 0 : _c.barelyMatchedElements) !== null && _d !== void 0 ? _d : 0); })[0];
|
|
154
|
+
return result;
|
|
155
|
+
}
|
|
@@ -103,9 +103,9 @@ function matchesSelector(query, node, specs, options, depth) {
|
|
|
103
103
|
unmatched: [node],
|
|
104
104
|
zeroMatch: true,
|
|
105
105
|
query,
|
|
106
|
-
hint: {
|
|
106
|
+
hint: (0, utils_1.cleanObject)({
|
|
107
107
|
not: result.not,
|
|
108
|
-
},
|
|
108
|
+
}),
|
|
109
109
|
};
|
|
110
110
|
}
|
|
111
111
|
return {
|
|
@@ -114,9 +114,9 @@ function matchesSelector(query, node, specs, options, depth) {
|
|
|
114
114
|
unmatched: [node],
|
|
115
115
|
zeroMatch: false,
|
|
116
116
|
query,
|
|
117
|
-
hint: {
|
|
117
|
+
hint: (0, utils_1.cleanObject)({
|
|
118
118
|
not: result.not,
|
|
119
|
-
},
|
|
119
|
+
}),
|
|
120
120
|
};
|
|
121
121
|
}
|
|
122
122
|
return {
|
|
@@ -16,11 +16,12 @@ const utils_1 = require("./utils");
|
|
|
16
16
|
* @returns
|
|
17
17
|
*/
|
|
18
18
|
function order(contents, nodes, specs, options, depth) {
|
|
19
|
-
var _a, _b, _c, _d;
|
|
19
|
+
var _a, _b, _c, _d, _e;
|
|
20
20
|
const orderLog = debug_1.cmLog.extend(`order#${depth}`);
|
|
21
21
|
const btLog = debug_1.cmLog.extend(`backtrack#${depth}`);
|
|
22
22
|
const patterns = (0, helpers_1.deepCopy)(contents);
|
|
23
23
|
const collection = new utils_1.Collection(nodes);
|
|
24
|
+
orderLog('Model:\n RegEx: %s\n Schema: %o', (0, utils_1.modelLog)(patterns, ''), patterns);
|
|
24
25
|
orderLog('Starts: %s', collection);
|
|
25
26
|
let result = undefined;
|
|
26
27
|
let backtrackMode = false;
|
|
@@ -29,28 +30,34 @@ function order(contents, nodes, specs, options, depth) {
|
|
|
29
30
|
while (patterns.length) {
|
|
30
31
|
result = (0, complex_branch_1.complexBranch)(patterns[0], collection.unmatched, specs, options, depth);
|
|
31
32
|
collection.addMatched(result.matched);
|
|
32
|
-
orderLog('stack: %s', collection);
|
|
33
33
|
if (result.type !== 'UNEXPECTED_EXTRA_NODE' && result.type !== 'MATCHED' && result.type !== 'MATCHED_ZERO') {
|
|
34
34
|
unmatchedResults.push(result);
|
|
35
35
|
if (backtrackMode) {
|
|
36
36
|
collection.back();
|
|
37
|
-
btLog('
|
|
37
|
+
btLog('🔙◀BACK');
|
|
38
38
|
backtrackMode = false;
|
|
39
39
|
afterBacktrack = true;
|
|
40
40
|
continue;
|
|
41
41
|
}
|
|
42
|
-
orderLog('conformed (%s): %s', result.type, collection.toString(true));
|
|
43
42
|
const barelyMatchedResult = unmatchedResults.sort((a, b) => b.matched.length - a.matched.length)[0];
|
|
44
43
|
if (!barelyMatchedResult) {
|
|
45
44
|
throw new Error('Unreachable code');
|
|
46
45
|
}
|
|
46
|
+
orderLog('Result (%s): %s%s', result.type, collection.toString(true), ((_a = barelyMatchedResult.hint.missing) === null || _a === void 0 ? void 0 : _a.barelyMatchedElements)
|
|
47
|
+
? `; But ${barelyMatchedResult.hint.missing.barelyMatchedElements} elements hit out of pattern`
|
|
48
|
+
: '');
|
|
47
49
|
return {
|
|
48
50
|
type: barelyMatchedResult.type,
|
|
49
51
|
matched: collection.matched,
|
|
50
52
|
unmatched: collection.unmatched,
|
|
51
53
|
zeroMatch: barelyMatchedResult.zeroMatch,
|
|
52
54
|
query: barelyMatchedResult.query,
|
|
53
|
-
hint: barelyMatchedResult.hint,
|
|
55
|
+
hint: (0, utils_1.mergeHints)(barelyMatchedResult.hint, {
|
|
56
|
+
missing: {
|
|
57
|
+
barelyMatchedElements: collection.matched.length,
|
|
58
|
+
need: barelyMatchedResult.query,
|
|
59
|
+
},
|
|
60
|
+
}),
|
|
54
61
|
};
|
|
55
62
|
}
|
|
56
63
|
if (afterBacktrack) {
|
|
@@ -66,24 +73,25 @@ function order(contents, nodes, specs, options, depth) {
|
|
|
66
73
|
patterns.shift();
|
|
67
74
|
}
|
|
68
75
|
if (collection.unmatched.length) {
|
|
69
|
-
orderLog('
|
|
76
|
+
orderLog('Result (UNEXPECTED_EXTRA_NODE): %s', collection.toString(true));
|
|
70
77
|
return {
|
|
71
78
|
type: 'UNEXPECTED_EXTRA_NODE',
|
|
72
79
|
matched: collection.matched,
|
|
73
80
|
unmatched: collection.unmatched,
|
|
74
81
|
zeroMatch: false,
|
|
75
|
-
query: (
|
|
76
|
-
hint: (
|
|
82
|
+
query: (_b = result === null || result === void 0 ? void 0 : result.query) !== null && _b !== void 0 ? _b : 'N/A',
|
|
83
|
+
hint: (_c = result === null || result === void 0 ? void 0 : result.hint) !== null && _c !== void 0 ? _c : {},
|
|
77
84
|
};
|
|
78
85
|
}
|
|
79
|
-
|
|
86
|
+
const resultType = collection.matched.length ? 'MATCHED' : 'MATCHED_ZERO';
|
|
87
|
+
orderLog('Result (%s): %s', resultType, collection.toString(true));
|
|
80
88
|
return {
|
|
81
|
-
type:
|
|
89
|
+
type: resultType,
|
|
82
90
|
matched: collection.matched,
|
|
83
91
|
unmatched: collection.unmatched,
|
|
84
92
|
zeroMatch: false,
|
|
85
|
-
query: (
|
|
86
|
-
hint: (
|
|
93
|
+
query: (_d = result === null || result === void 0 ? void 0 : result.query) !== null && _d !== void 0 ? _d : 'N/A',
|
|
94
|
+
hint: (_e = result === null || result === void 0 ? void 0 : result.hint) !== null && _e !== void 0 ? _e : {},
|
|
87
95
|
};
|
|
88
96
|
}
|
|
89
97
|
exports.order = order;
|
|
@@ -24,15 +24,21 @@ export type Result<T extends string = MatchedReason> = {
|
|
|
24
24
|
unmatched: ChildNode[];
|
|
25
25
|
zeroMatch: boolean;
|
|
26
26
|
query: string;
|
|
27
|
-
hint:
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
27
|
+
hint: Hints;
|
|
28
|
+
};
|
|
29
|
+
export type Hints = {
|
|
30
|
+
max?: number;
|
|
31
|
+
not?: ChildNode;
|
|
32
|
+
transparent?: Element;
|
|
33
|
+
missing?: {
|
|
34
|
+
barelyMatchedElements?: number;
|
|
35
|
+
need?: string;
|
|
31
36
|
};
|
|
32
37
|
};
|
|
33
38
|
export type MatchedReason = 'MATCHED' | 'MATCHED_ZERO';
|
|
34
39
|
export type UnmatchedReason = 'NOTHING' | 'UNEXPECTED_EXTRA_NODE' | 'TRANSPARENT_MODEL_DISALLOWS' | MissingNodeReason;
|
|
35
40
|
export type MissingNodeReason = 'MISSING_NODE_REQUIRED' | 'MISSING_NODE_ONE_OR_MORE';
|
|
41
|
+
export type RepeatSign = '' | '?' | '+' | '*' | `{${number},${number}}`;
|
|
36
42
|
export type TransparentModel = {
|
|
37
43
|
el: Element;
|
|
38
44
|
additionalCondition: string;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ChildNode, Element, Specs } from './types';
|
|
1
|
+
import type { ChildNode, Element, Hints, MissingNodeReason, RepeatSign, Specs } from './types';
|
|
2
2
|
import type { PermittedContentPattern, PermittedContentChoice, PermittedContentOneOrMore, PermittedContentOptional, PermittedContentRequire, PermittedContentTransparent, PermittedContentZeroOrMore, Model } from '@markuplint/ml-spec';
|
|
3
3
|
export declare function getChildNodesWithoutWhitespaces(el: Element): ChildNode[];
|
|
4
4
|
export declare function isModel(model: Model | PermittedContentPattern[]): model is Model;
|
|
@@ -15,6 +15,23 @@ export declare function isOneOrMore(content: PermittedContentPattern): content i
|
|
|
15
15
|
export declare function isZeroOrMore(content: PermittedContentPattern): content is PermittedContentZeroOrMore;
|
|
16
16
|
export declare function isChoice(content: PermittedContentPattern): content is PermittedContentChoice;
|
|
17
17
|
export declare function isTransparent(content: PermittedContentPattern): content is PermittedContentTransparent;
|
|
18
|
+
export declare function normalizeModel(pattern: PermittedContentRequire | PermittedContentOptional | PermittedContentOneOrMore | PermittedContentZeroOrMore): {
|
|
19
|
+
model: PermittedContentPattern[] | Model;
|
|
20
|
+
min: number;
|
|
21
|
+
max: number;
|
|
22
|
+
repeat: RepeatSign;
|
|
23
|
+
missingType: MissingNodeReason | undefined;
|
|
24
|
+
};
|
|
25
|
+
export declare function mergeHints(a: Hints, b: Hints): Partial<{
|
|
26
|
+
missing: Partial<{
|
|
27
|
+
barelyMatchedElements?: number | undefined;
|
|
28
|
+
need?: string | undefined;
|
|
29
|
+
}> | undefined;
|
|
30
|
+
max?: number | undefined;
|
|
31
|
+
not?: import("packages/@markuplint/ml-core/lib").DocumentType<import("./types").TagRule[], import("./types").Options> | import("packages/@markuplint/ml-core/lib/ml-dom/node/character-data").MLCharacterData<import("./types").TagRule[], import("./types").Options, import("packages/@markuplint/ml-ast/lib").MLASTAbstractNode> | import("packages/@markuplint/ml-core/lib").Element<import("./types").TagRule[], import("./types").Options> | import("packages/@markuplint/ml-core/lib").Block<import("./types").TagRule[], import("./types").Options> | undefined;
|
|
32
|
+
transparent?: Element | undefined;
|
|
33
|
+
}>;
|
|
34
|
+
export declare function cleanObject<T extends Object>(object: T): Partial<T>;
|
|
18
35
|
export declare class Collection {
|
|
19
36
|
#private;
|
|
20
37
|
constructor(origin: readonly ChildNode[]);
|
|
@@ -30,3 +47,4 @@ export declare class Collection {
|
|
|
30
47
|
}
|
|
31
48
|
export declare class UnsupportedError extends Error {
|
|
32
49
|
}
|
|
50
|
+
export declare function modelLog(model: Model | PermittedContentPattern[], repeat: RepeatSign): string;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
var _Collection_locked, _Collection_matched, _Collection_nodes, _Collection_origin;
|
|
3
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
-
exports.UnsupportedError = exports.Collection = exports.isTransparent = exports.isChoice = exports.isZeroOrMore = exports.isOneOrMore = exports.isOptional = exports.isRequire = exports.matches = exports.isModel = exports.getChildNodesWithoutWhitespaces = void 0;
|
|
4
|
+
exports.modelLog = exports.UnsupportedError = exports.Collection = exports.cleanObject = exports.mergeHints = exports.normalizeModel = exports.isTransparent = exports.isChoice = exports.isZeroOrMore = exports.isOneOrMore = exports.isOptional = exports.isRequire = exports.matches = exports.isModel = exports.getChildNodesWithoutWhitespaces = void 0;
|
|
5
5
|
const tslib_1 = require("tslib");
|
|
6
6
|
const selector_1 = require("@markuplint/selector");
|
|
7
7
|
const debug_1 = require("./debug");
|
|
@@ -95,6 +95,81 @@ function isTransparent(content) {
|
|
|
95
95
|
return 'transparent' in content;
|
|
96
96
|
}
|
|
97
97
|
exports.isTransparent = isTransparent;
|
|
98
|
+
function normalizeModel(pattern) {
|
|
99
|
+
var _a, _b, _c, _d, _e;
|
|
100
|
+
let model;
|
|
101
|
+
let min;
|
|
102
|
+
let max;
|
|
103
|
+
let repeat;
|
|
104
|
+
let missingType;
|
|
105
|
+
if (isRequire(pattern)) {
|
|
106
|
+
model = pattern.require;
|
|
107
|
+
min = (_a = pattern.min) !== null && _a !== void 0 ? _a : 1;
|
|
108
|
+
max = Math.max((_b = pattern.max) !== null && _b !== void 0 ? _b : 1, min);
|
|
109
|
+
missingType = 'MISSING_NODE_REQUIRED';
|
|
110
|
+
}
|
|
111
|
+
else if (isOptional(pattern)) {
|
|
112
|
+
model = pattern.optional;
|
|
113
|
+
min = 0;
|
|
114
|
+
max = Math.max((_c = pattern.max) !== null && _c !== void 0 ? _c : 1, 1);
|
|
115
|
+
}
|
|
116
|
+
else if (isOneOrMore(pattern)) {
|
|
117
|
+
model = pattern.oneOrMore;
|
|
118
|
+
min = 1;
|
|
119
|
+
max = Math.max((_d = pattern.max) !== null && _d !== void 0 ? _d : Infinity, 1);
|
|
120
|
+
missingType = 'MISSING_NODE_ONE_OR_MORE';
|
|
121
|
+
}
|
|
122
|
+
else if (isZeroOrMore(pattern)) {
|
|
123
|
+
model = pattern.zeroOrMore;
|
|
124
|
+
min = 0;
|
|
125
|
+
max = Math.max((_e = pattern.max) !== null && _e !== void 0 ? _e : Infinity, 1);
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
throw new Error('Unreachable code');
|
|
129
|
+
}
|
|
130
|
+
if (min === 0 && max === 1) {
|
|
131
|
+
repeat = '?';
|
|
132
|
+
}
|
|
133
|
+
else if (min === 0 && !Number.isFinite(max)) {
|
|
134
|
+
repeat = '*';
|
|
135
|
+
}
|
|
136
|
+
else if (min === 1 && max === 1) {
|
|
137
|
+
repeat = '';
|
|
138
|
+
}
|
|
139
|
+
else if (min === 1 && !Number.isFinite(max)) {
|
|
140
|
+
repeat = '+';
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
repeat = `{${min},${max}}`;
|
|
144
|
+
}
|
|
145
|
+
return {
|
|
146
|
+
model,
|
|
147
|
+
min,
|
|
148
|
+
max,
|
|
149
|
+
repeat,
|
|
150
|
+
missingType,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
exports.normalizeModel = normalizeModel;
|
|
154
|
+
function mergeHints(a, b) {
|
|
155
|
+
const missing = [a.missing, b.missing].sort((a, b) => { var _a, _b; return ((_a = b === null || b === void 0 ? void 0 : b.barelyMatchedElements) !== null && _a !== void 0 ? _a : 0) - ((_b = a === null || a === void 0 ? void 0 : a.barelyMatchedElements) !== null && _b !== void 0 ? _b : 0); })[0];
|
|
156
|
+
return cleanObject({
|
|
157
|
+
...a,
|
|
158
|
+
...b,
|
|
159
|
+
missing: missing && cleanObject(missing),
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
exports.mergeHints = mergeHints;
|
|
163
|
+
function cleanObject(object) {
|
|
164
|
+
const newObject = {};
|
|
165
|
+
Object.entries(object).forEach(([key, value]) => {
|
|
166
|
+
if (value !== undefined) {
|
|
167
|
+
newObject[key] = value;
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
return newObject;
|
|
171
|
+
}
|
|
172
|
+
exports.cleanObject = cleanObject;
|
|
98
173
|
class Collection {
|
|
99
174
|
constructor(origin) {
|
|
100
175
|
_Collection_locked.set(this, new Set());
|
|
@@ -182,3 +257,32 @@ _Collection_locked = new WeakMap(), _Collection_matched = new WeakMap(), _Collec
|
|
|
182
257
|
class UnsupportedError extends Error {
|
|
183
258
|
}
|
|
184
259
|
exports.UnsupportedError = UnsupportedError;
|
|
260
|
+
function modelLog(model, repeat) {
|
|
261
|
+
if (!isModel(model)) {
|
|
262
|
+
return orderLog(model, repeat);
|
|
263
|
+
}
|
|
264
|
+
if (typeof model === 'string') {
|
|
265
|
+
return `<${model}>${repeat}`;
|
|
266
|
+
}
|
|
267
|
+
return `(<${model.join('>|<')}>)${repeat}`;
|
|
268
|
+
}
|
|
269
|
+
exports.modelLog = modelLog;
|
|
270
|
+
function orderLog(order, repeat) {
|
|
271
|
+
return order.length === 1
|
|
272
|
+
? markRepeat(patternLog(order[0]), repeat)
|
|
273
|
+
: markRepeat(order.map(pattern => patternLog(pattern)).join(''), repeat);
|
|
274
|
+
}
|
|
275
|
+
function patternLog(pattern) {
|
|
276
|
+
if (isTransparent(pattern)) {
|
|
277
|
+
// 適当
|
|
278
|
+
return `:transparent(${modelLog(pattern.transparent, '')})`;
|
|
279
|
+
}
|
|
280
|
+
if (isChoice(pattern)) {
|
|
281
|
+
return `(${pattern.choice.map(candidate => orderLog(candidate, '')).join('|')})`;
|
|
282
|
+
}
|
|
283
|
+
const { model, repeat } = normalizeModel(pattern);
|
|
284
|
+
return modelLog(model, repeat);
|
|
285
|
+
}
|
|
286
|
+
function markRepeat(pattern, repeat) {
|
|
287
|
+
return repeat ? `(${pattern})${repeat}` : pattern;
|
|
288
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const ml_core_1 = require("@markuplint/ml-core");
|
|
4
|
+
exports.default = (0, ml_core_1.createRule)({
|
|
5
|
+
verify({ document, report, t }) {
|
|
6
|
+
document.querySelectorAll('select').forEach(select => {
|
|
7
|
+
const required = select.getAttributeNode('required');
|
|
8
|
+
if (!required) {
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
if (!hasPlaceholderLabelOption(select)) {
|
|
12
|
+
report({
|
|
13
|
+
scope: required,
|
|
14
|
+
message: t('{0} if it has {1}', t('need {0}', t('the {0}', 'placeholder label option')), t('the "{0*}" {1}', 'required', 'attribute')),
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
function hasPlaceholderLabelOption(select) {
|
|
21
|
+
var _a;
|
|
22
|
+
// has a required attribute specified
|
|
23
|
+
if (!select.hasAttribute('required')) {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
// does not have a multiple attribute specified
|
|
27
|
+
if (select.hasAttribute('multiple')) {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
// has a display size of 1
|
|
31
|
+
const size = select.getAttribute('size') || '1';
|
|
32
|
+
if (size !== '1') {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
// in the select element's list of options (if any) is the empty string
|
|
36
|
+
const firstOption = select.querySelector('option');
|
|
37
|
+
if (!firstOption) {
|
|
38
|
+
// if any
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
// that option element's parent node is the select element (and not an optgroup element)
|
|
42
|
+
if (((_a = firstOption.parentElement) === null || _a === void 0 ? void 0 : _a.localName) === 'optgroup') {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
const value = firstOption.getAttribute('value');
|
|
46
|
+
return value === '' || value === null;
|
|
47
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const ml_core_1 = require("@markuplint/ml-core");
|
|
4
|
+
const types_1 = require("@markuplint/types");
|
|
5
|
+
const utils_1 = require("./utils");
|
|
6
|
+
exports.default = (0, ml_core_1.createRule)({
|
|
7
|
+
defaultOptions: {
|
|
8
|
+
langs: undefined,
|
|
9
|
+
},
|
|
10
|
+
verify({ document, report, t }) {
|
|
11
|
+
document.querySelectorAll('time:not([datetime])').forEach(time => {
|
|
12
|
+
if (time.hasMutableChildren()) {
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
const content = time.textContent.trim();
|
|
16
|
+
const result = (0, types_1.check)(content, 'DateTime');
|
|
17
|
+
if (result.matched) {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
const candidate = (0, utils_1.getCandidateDatetimeString)(content, time.rule.options.langs);
|
|
21
|
+
if (candidate) {
|
|
22
|
+
report({
|
|
23
|
+
scope: time,
|
|
24
|
+
message: t('need {0*}', `datetime="${candidate}"`),
|
|
25
|
+
});
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
report({
|
|
29
|
+
scope: time,
|
|
30
|
+
message: t('need {0}', t('the "{0*}" {1}', 'datetime', 'attribute')),
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
},
|
|
34
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type DateTimeKey = 'year' | 'month' | 'day' | 'hour' | 'minute' | 'second' | 'ms';
|
|
2
|
+
export type DateTimeData = Partial<Record<DateTimeKey, number>>;
|
|
3
|
+
export type DateTime = {
|
|
4
|
+
datetime: DateTimeData;
|
|
5
|
+
zone?: number;
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* @see https://github.com/wanasit/chrono#locales
|
|
9
|
+
*/
|
|
10
|
+
export type Lang = 'en' | 'ja' | 'fr' | 'nl' | 'ru' | 'de' | 'pt' | 'zh';
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { DateTime, Lang } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Datetime-ish text to a datetime data
|
|
4
|
+
*
|
|
5
|
+
* @param content
|
|
6
|
+
* @param langs
|
|
7
|
+
* @param base Reference date for a test
|
|
8
|
+
* @returns
|
|
9
|
+
*/
|
|
10
|
+
export declare function parseADatetime(content: string, langs: Lang[], base?: Date): DateTime | null;
|
|
11
|
+
export declare function getCandidateDatetimeString(content: string, langs?: Lang[]): string | null;
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getCandidateDatetimeString = exports.parseADatetime = void 0;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const chrono = tslib_1.__importStar(require("chrono-node"));
|
|
6
|
+
const defaultLangs = ['en', 'ja', 'fr', 'nl', 'ru', 'de', 'pt', 'zh'];
|
|
7
|
+
/**
|
|
8
|
+
* Datetime-ish text to a datetime data
|
|
9
|
+
*
|
|
10
|
+
* @param content
|
|
11
|
+
* @param langs
|
|
12
|
+
* @param base Reference date for a test
|
|
13
|
+
* @returns
|
|
14
|
+
*/
|
|
15
|
+
function parseADatetime(content, langs, base) {
|
|
16
|
+
const date = parseTryMultipleLangs(content, langs, base);
|
|
17
|
+
if (!date) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
const data = {};
|
|
21
|
+
if (date.isCertain('year')) {
|
|
22
|
+
data.year = date.get('year');
|
|
23
|
+
}
|
|
24
|
+
if (date.isCertain('month')) {
|
|
25
|
+
data.month = date.get('month');
|
|
26
|
+
}
|
|
27
|
+
if (date.isCertain('day')) {
|
|
28
|
+
data.day = date.get('day');
|
|
29
|
+
}
|
|
30
|
+
if (date.isCertain('hour')) {
|
|
31
|
+
data.hour = date.get('hour');
|
|
32
|
+
}
|
|
33
|
+
if (date.isCertain('hour')) {
|
|
34
|
+
data.minute = date.get('minute') || 0;
|
|
35
|
+
}
|
|
36
|
+
if (date.isCertain('second')) {
|
|
37
|
+
data.second = date.get('second');
|
|
38
|
+
}
|
|
39
|
+
if (date.isCertain('millisecond')) {
|
|
40
|
+
data.ms = date.get('millisecond');
|
|
41
|
+
}
|
|
42
|
+
const datetime = {
|
|
43
|
+
datetime: data,
|
|
44
|
+
};
|
|
45
|
+
if (date.isCertain('timezoneOffset')) {
|
|
46
|
+
datetime.zone = date.get('timezoneOffset');
|
|
47
|
+
}
|
|
48
|
+
return datetime;
|
|
49
|
+
}
|
|
50
|
+
exports.parseADatetime = parseADatetime;
|
|
51
|
+
function getCandidateDatetimeString(content, langs = defaultLangs) {
|
|
52
|
+
const date = parseADatetime(content, langs);
|
|
53
|
+
if (!date) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
let datetimeStr = toDatetimeString(date.datetime);
|
|
57
|
+
if (!datetimeStr) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
if (date.zone) {
|
|
61
|
+
const plusMinus = date.zone < 0 ? '-' : '+';
|
|
62
|
+
const hour = Math.floor(Math.abs(date.zone) / 60);
|
|
63
|
+
const minute = Math.abs(date.zone) % 60;
|
|
64
|
+
datetimeStr += `${plusMinus}${f(hour, 2)}${f(minute, 2)}`;
|
|
65
|
+
}
|
|
66
|
+
return datetimeStr;
|
|
67
|
+
}
|
|
68
|
+
exports.getCandidateDatetimeString = getCandidateDatetimeString;
|
|
69
|
+
function toDatetimeString(date) {
|
|
70
|
+
if (only(date, ['year', 'month'])) {
|
|
71
|
+
return `${f(date.year, 4)}-${f(date.month, 2)}`;
|
|
72
|
+
}
|
|
73
|
+
if (only(date, ['year', 'month', 'day'])) {
|
|
74
|
+
return `${f(date.year, 4)}-${f(date.month, 2)}-${f(date.day, 2)}`;
|
|
75
|
+
}
|
|
76
|
+
if (only(date, ['month', 'day'])) {
|
|
77
|
+
return `${f(date.month, 2)}-${f(date.day, 2)}`;
|
|
78
|
+
}
|
|
79
|
+
if (only(date, ['hour', 'minute'])) {
|
|
80
|
+
return `${f(date.hour, 2)}:${f(date.minute, 2)}`;
|
|
81
|
+
}
|
|
82
|
+
if (only(date, ['hour', 'minute', 'second'])) {
|
|
83
|
+
return `${f(date.hour, 2)}:${f(date.minute, 2)}:${f(date.second, 2)}`;
|
|
84
|
+
}
|
|
85
|
+
if (only(date, ['hour', 'minute', 'second', 'ms'])) {
|
|
86
|
+
return `${f(date.hour, 2)}:${f(date.minute, 2)}:${f(date.second, 2)}.${date.ms}`;
|
|
87
|
+
}
|
|
88
|
+
if (only(date, ['year', 'month', 'day', 'hour', 'minute'])) {
|
|
89
|
+
return `${f(date.year, 4)}-${f(date.month, 2)}-${f(date.day, 2)}T${f(date.hour, 2)}:${f(date.minute, 2)}`;
|
|
90
|
+
}
|
|
91
|
+
if (only(date, ['year', 'month', 'day', 'hour', 'minute', 'second'])) {
|
|
92
|
+
return `${f(date.year, 4)}-${f(date.month, 2)}-${f(date.day, 2)}T${f(date.hour, 2)}:${f(date.minute, 2)}:${f(date.second, 2)}`;
|
|
93
|
+
}
|
|
94
|
+
if (only(date, ['year', 'month', 'day', 'hour', 'minute', 'second', 'ms'])) {
|
|
95
|
+
return `${f(date.year, 4)}-${f(date.month, 2)}-${f(date.day, 2)}T${f(date.hour, 2)}:${f(date.minute, 2)}:${f(date.second, 2)}.${date.ms}`;
|
|
96
|
+
}
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
function parseTryMultipleLangs(content, langs, base) {
|
|
100
|
+
for (const lang of langs) {
|
|
101
|
+
// eslint-disable-next-line import/namespace
|
|
102
|
+
const results = chrono[lang].casual.parse(content, base);
|
|
103
|
+
// Is not multiple datetime contents
|
|
104
|
+
if (results.length < 1) {
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
const result = results[0];
|
|
108
|
+
// Is not a range or period
|
|
109
|
+
if (result.end) {
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
return result.start;
|
|
113
|
+
}
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
function only(date, keys) {
|
|
117
|
+
const list = Object.keys(date);
|
|
118
|
+
for (const exists of list) {
|
|
119
|
+
if (!keys.includes(exists)) {
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Formatter
|
|
127
|
+
*
|
|
128
|
+
* @param n
|
|
129
|
+
* @param pad zero padding
|
|
130
|
+
* @returns
|
|
131
|
+
*/
|
|
132
|
+
function f(n, pad) {
|
|
133
|
+
return n.toString(10).padStart(pad, '0');
|
|
134
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@markuplint/rules",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.1.0",
|
|
4
4
|
"description": "Built-in rules of markuplint",
|
|
5
5
|
"repository": "git@github.com:markuplint/markuplint.git",
|
|
6
6
|
"author": "Yusuke Hirao <yusukehirao@me.com>",
|
|
@@ -20,11 +20,12 @@
|
|
|
20
20
|
"./lib/permitted-contents/debug.js": "./lib/permitted-contents/debug.browser.js"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@markuplint/html-spec": "3.
|
|
24
|
-
"@markuplint/ml-core": "3.
|
|
25
|
-
"@markuplint/ml-spec": "3.
|
|
23
|
+
"@markuplint/html-spec": "3.1.0",
|
|
24
|
+
"@markuplint/ml-core": "3.1.0",
|
|
25
|
+
"@markuplint/ml-spec": "3.1.0",
|
|
26
26
|
"@markuplint/types": "3.0.0",
|
|
27
27
|
"@ungap/structured-clone": "^1.0.1",
|
|
28
|
+
"chrono-node": "^2.5.0",
|
|
28
29
|
"debug": "^4.3.4",
|
|
29
30
|
"html-entities": "^2.3.3",
|
|
30
31
|
"tslib": "^2.4.1"
|
|
@@ -32,5 +33,5 @@
|
|
|
32
33
|
"devDependencies": {
|
|
33
34
|
"@types/debug": "^4.1.7"
|
|
34
35
|
},
|
|
35
|
-
"gitHead": "
|
|
36
|
+
"gitHead": "0e75df6944f453bc19d2a6e05ad264788a8dc149"
|
|
36
37
|
}
|
package/schema.json
CHANGED
|
@@ -78,6 +78,9 @@
|
|
|
78
78
|
"require-accessible-name": {
|
|
79
79
|
"$ref": "https://raw.githubusercontent.com/markuplint/markuplint/main/packages/%40markuplint/rules/src/require-accessible-name/schema.json"
|
|
80
80
|
},
|
|
81
|
+
"require-datetime": {
|
|
82
|
+
"$ref": "https://raw.githubusercontent.com/markuplint/markuplint/main/packages/%40markuplint/rules/src/require-datetime/schema.json"
|
|
83
|
+
},
|
|
81
84
|
"required-attr": {
|
|
82
85
|
"$ref": "https://raw.githubusercontent.com/markuplint/markuplint/main/packages/%40markuplint/rules/src/required-attr/schema.json"
|
|
83
86
|
},
|