@markuplint/rules 3.0.0-dev.54 → 3.0.0-dev.95

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.
@@ -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
@@ -22,6 +22,12 @@ function attrCheck(t, name, value, isCustomRule, spec) {
22
22
  // Ignore checking because ARIA attributes are check on another rule
23
23
  return false;
24
24
  }
25
+ // @see https://www.w3.org/TR/adapt/
26
+ // It is an experimental
27
+ if (/^adapt-.+$/.test(name)) {
28
+ // Ignore checking because "adapt-*" attribute is any type
29
+ return false;
30
+ }
25
31
  }
26
32
  // Existence
27
33
  if (!spec) {
@@ -41,6 +47,12 @@ function attrCheck(t, name, value, isCustomRule, spec) {
41
47
  t('Did you mean "{0*}"?', spec.name),
42
48
  };
43
49
  }
50
+ if (spec.noUse) {
51
+ return {
52
+ invalidType: 'disallowed-attr',
53
+ message: t('{0} is {1:c}', t('the "{0*}" {1}', name, 'attribute'), 'disallowed'),
54
+ };
55
+ }
44
56
  const types = Array.isArray(spec.type) ? spec.type : [spec.type];
45
57
  const invalidList = types.map(type => {
46
58
  if (!type) {
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
@@ -24,7 +24,9 @@ const no_hard_code_id_1 = tslib_1.__importDefault(require("./no-hard-code-id"));
24
24
  const no_refer_to_non_existent_id_1 = tslib_1.__importDefault(require("./no-refer-to-non-existent-id"));
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
+ const placeholder_label_option_1 = tslib_1.__importDefault(require("./placeholder-label-option"));
27
28
  const require_accessible_name_1 = tslib_1.__importDefault(require("./require-accessible-name"));
29
+ const require_datetime_1 = tslib_1.__importDefault(require("./require-datetime"));
28
30
  const required_attr_1 = tslib_1.__importDefault(require("./required-attr"));
29
31
  const required_element_1 = tslib_1.__importDefault(require("./required-element"));
30
32
  const required_h1_1 = tslib_1.__importDefault(require("./required-h1"));
@@ -54,7 +56,9 @@ exports.default = {
54
56
  'no-refer-to-non-existent-id': no_refer_to_non_existent_id_1.default,
55
57
  'no-use-event-handler-attr': no_use_event_handler_attr_1.default,
56
58
  'permitted-contents': permitted_contents_1.default,
59
+ 'placeholder-label-option': placeholder_label_option_1.default,
57
60
  'require-accessible-name': require_accessible_name_1.default,
61
+ 'require-datetime': require_datetime_1.default,
58
62
  'required-attr': required_attr_1.default,
59
63
  'required-element': required_element_1.default,
60
64
  '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,5 +1,6 @@
1
1
  import type { ARIAVersion } from '@markuplint/ml-spec';
2
2
  declare const _default: import("@markuplint/ml-core").RuleSeed<import("@markuplint/ml-core").RuleConfigValue, {
3
3
  ariaVersion: ARIAVersion;
4
+ fragmentRefersNameAttr: boolean;
4
5
  }>;
5
6
  export default _default;
@@ -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
- await document.walkOn('Attr', attr => {
12
- if (attr.name.toLowerCase() !== 'id') {
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
+ }
@@ -0,0 +1,2 @@
1
+ declare const _default: import("@markuplint/ml-core").RuleSeed<boolean, null>;
2
+ export default _default;
@@ -0,0 +1,83 @@
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
+ if (hasPlaceholderLabelOption(select)) {
8
+ return;
9
+ }
10
+ if (!needPlaceholderLabelOption(select)) {
11
+ return;
12
+ }
13
+ report({
14
+ scope: select,
15
+ message: t('need {0}', t('the {0}', 'placeholder label option')),
16
+ });
17
+ });
18
+ },
19
+ });
20
+ /**
21
+ * > If a select element has a required attribute specified,
22
+ * > does not have a multiple attribute specified,
23
+ * > and has a display size of 1,
24
+ * > then the select element must have a placeholder label option.
25
+ *
26
+ * @param select
27
+ * @returns
28
+ */
29
+ function needPlaceholderLabelOption(select) {
30
+ const hasRequired = select.hasAttribute('required');
31
+ if (!hasRequired) {
32
+ return false;
33
+ }
34
+ const hasMultiple = select.hasAttribute('multiple');
35
+ if (hasMultiple) {
36
+ return false;
37
+ }
38
+ const size = select.getAttribute('size') || '1';
39
+ if (size !== '1') {
40
+ return false;
41
+ }
42
+ return true;
43
+ }
44
+ /**
45
+ * > If a select element has a required attribute specified,
46
+ * > does not have a multiple attribute specified,
47
+ * > and has a display size of 1;
48
+ * > and if the value of the first option element
49
+ * > in the select element's list of options (if any) is the empty string,
50
+ * > and that option element's parent node is the select element (and not an optgroup element),
51
+ * > then that option is the select element's **placeholder label option**.
52
+ *
53
+ * @param select
54
+ * @returns
55
+ */
56
+ function hasPlaceholderLabelOption(select) {
57
+ var _a;
58
+ // > has a required attribute specified
59
+ if (!select.hasAttribute('required')) {
60
+ return false;
61
+ }
62
+ // > does not have a multiple attribute specified
63
+ if (select.hasAttribute('multiple')) {
64
+ return false;
65
+ }
66
+ // > has a display size of 1
67
+ const size = select.getAttribute('size') || '1';
68
+ if (size !== '1') {
69
+ return false;
70
+ }
71
+ // > in the select element's list of options (if any) is the empty string
72
+ const firstOption = select.querySelector('option');
73
+ if (!firstOption) {
74
+ // if any
75
+ return true;
76
+ }
77
+ // > that option element's parent node is the select element (and not an optgroup element)
78
+ if (((_a = firstOption.parentElement) === null || _a === void 0 ? void 0 : _a.localName) === 'optgroup') {
79
+ return false;
80
+ }
81
+ const value = firstOption.getAttribute('value');
82
+ return value === '' || value === null;
83
+ }
@@ -0,0 +1,6 @@
1
+ import type { Lang } from './types';
2
+ type Options = {
3
+ langs?: Lang[];
4
+ };
5
+ declare const _default: import("@markuplint/ml-core").RuleSeed<boolean, Options>;
6
+ export default _default;
@@ -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,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -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.0.0-dev.54+329808d5",
3
+ "version": "3.0.0-dev.95+599674b1",
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.0.0-dev.54+329808d5",
24
- "@markuplint/ml-core": "3.0.0-dev.54+329808d5",
25
- "@markuplint/ml-spec": "3.0.0-dev.54+329808d5",
26
- "@markuplint/types": "3.0.0-dev.54+329808d5",
23
+ "@markuplint/html-spec": "3.0.0-dev.95+599674b1",
24
+ "@markuplint/ml-core": "3.0.0-dev.95+599674b1",
25
+ "@markuplint/ml-spec": "3.0.0-dev.95+599674b1",
26
+ "@markuplint/types": "3.0.0-dev.95+599674b1",
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": "329808d50ff8875e24031f692a0504d00b72c4f8"
36
+ "gitHead": "599674b17e532b5700e63c741217aba826e30feb"
36
37
  }
package/schema.json CHANGED
@@ -75,9 +75,15 @@
75
75
  "permitted-contents": {
76
76
  "$ref": "https://raw.githubusercontent.com/markuplint/markuplint/main/packages/%40markuplint/rules/src/permitted-contents/schema.json"
77
77
  },
78
+ "placeholder-label-option": {
79
+ "$ref": "https://raw.githubusercontent.com/markuplint/markuplint/main/packages/%40markuplint/rules/src/placeholder-label-option/schema.json"
80
+ },
78
81
  "require-accessible-name": {
79
82
  "$ref": "https://raw.githubusercontent.com/markuplint/markuplint/main/packages/%40markuplint/rules/src/require-accessible-name/schema.json"
80
83
  },
84
+ "require-datetime": {
85
+ "$ref": "https://raw.githubusercontent.com/markuplint/markuplint/main/packages/%40markuplint/rules/src/require-datetime/schema.json"
86
+ },
81
87
  "required-attr": {
82
88
  "$ref": "https://raw.githubusercontent.com/markuplint/markuplint/main/packages/%40markuplint/rules/src/required-attr/schema.json"
83
89
  },