@markuplint/types 4.8.2 → 5.0.0-alpha.1
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/ARCHITECTURE.ja.md +4 -0
- package/ARCHITECTURE.md +4 -0
- package/CHANGELOG.md +20 -0
- package/docs/validators.ja.md +18 -8
- package/docs/validators.md +18 -8
- package/lib/check-base.d.ts +10 -1
- package/lib/check-base.js +16 -0
- package/lib/defs.js +18 -0
- package/lib/index.d.ts +4 -1
- package/lib/index.js +3 -1
- package/lib/pattern.d.ts +12 -0
- package/lib/pattern.js +33 -0
- package/lib/rfc/is-bcp-47.js +2 -2
- package/lib/token/token-collection.d.ts +1 -1
- package/lib/token/token-collection.js +7 -0
- package/lib/token/token.js +23 -19
- package/lib/types.d.ts +1 -1
- package/lib/types.schema.d.ts +4 -1
- package/lib/whatwg/check-autocomplete.d.ts +7 -2
- package/lib/whatwg/check-autocomplete.js +211 -162
- package/lib/whatwg/check-datetime/datetime-tokens.js +1 -1
- package/lib/whatwg/check-link-type.d.ts +103 -0
- package/lib/whatwg/check-link-type.js +12 -6
- package/lib/whatwg/check-mime-type.js +2 -2
- package/lib/whatwg/is-abs-url.js +1 -4
- package/package.json +8 -6
- package/types.schema.json +10 -1
|
@@ -2,8 +2,13 @@ import type { CustomSyntaxChecker } from '../types.js';
|
|
|
2
2
|
/**
|
|
3
3
|
* Validates the `autocomplete` attribute value according to the WHATWG specification.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* Uses backward parsing (right-to-left) to match the spec algorithm:
|
|
6
|
+
* 1. Determine field name from the last token
|
|
7
|
+
* 2. Handle `webauthn` credential token and category re-determination
|
|
8
|
+
* 3. Validate optional contacting token (home/work/mobile/fax/pager)
|
|
9
|
+
* 4. Validate optional shipping/billing token
|
|
10
|
+
* 5. Validate optional section-* named group
|
|
11
|
+
* 6. Check maximum token count per category
|
|
7
12
|
*
|
|
8
13
|
* @see https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#attr-fe-autocomplete
|
|
9
14
|
*/
|
|
@@ -82,25 +82,45 @@ const contactableFieldNames = [
|
|
|
82
82
|
/**
|
|
83
83
|
* @see https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#attr-fe-autocomplete-webauthn
|
|
84
84
|
*/
|
|
85
|
-
const webauthnFieldNames = ['webauthn'];
|
|
85
|
+
const webauthnFieldNames = new Set(['webauthn']);
|
|
86
86
|
const URL_AUTOCOMPLETE = 'https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#attr-fe-autocomplete';
|
|
87
87
|
const URL_ON_OFF = 'https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#autofilling-form-controls:-the-autocomplete-attribute:attr-fe-autocomplete-on-2';
|
|
88
88
|
const URL_NAMED_GROUP = 'https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#attr-fe-autocomplete-section';
|
|
89
89
|
const URL_PART_OF_ADDRESS = 'https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#attr-fe-autocomplete-shipping';
|
|
90
90
|
const URL_AUTOFILL_FIELD = 'https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#autofill-field';
|
|
91
|
-
|
|
91
|
+
/**
|
|
92
|
+
* Determines the field category and maximum allowed token count
|
|
93
|
+
* based on the last meaningful token (the field name).
|
|
94
|
+
*
|
|
95
|
+
* @see https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#autofill-field
|
|
96
|
+
*/
|
|
97
|
+
function determineFieldCategory(value) {
|
|
98
|
+
const lower = value.toLowerCase();
|
|
99
|
+
if (autofillFieldNames.includes(lower)) {
|
|
100
|
+
return { category: 'Normal' };
|
|
101
|
+
}
|
|
102
|
+
if (contactableFieldNames.includes(lower)) {
|
|
103
|
+
return { category: 'Contact' };
|
|
104
|
+
}
|
|
105
|
+
if (webauthnFieldNames.has(lower)) {
|
|
106
|
+
return { category: 'Credential' };
|
|
107
|
+
}
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
92
110
|
/**
|
|
93
111
|
* Validates the `autocomplete` attribute value according to the WHATWG specification.
|
|
94
112
|
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
113
|
+
* Uses backward parsing (right-to-left) to match the spec algorithm:
|
|
114
|
+
* 1. Determine field name from the last token
|
|
115
|
+
* 2. Handle `webauthn` credential token and category re-determination
|
|
116
|
+
* 3. Validate optional contacting token (home/work/mobile/fax/pager)
|
|
117
|
+
* 4. Validate optional shipping/billing token
|
|
118
|
+
* 5. Validate optional section-* named group
|
|
119
|
+
* 6. Check maximum token count per category
|
|
97
120
|
*
|
|
98
121
|
* @see https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#attr-fe-autocomplete
|
|
99
122
|
*/
|
|
100
123
|
export const checkAutoComplete = () => value => {
|
|
101
|
-
let hasNamedGroup = false;
|
|
102
|
-
let hasPartOfAddress = false;
|
|
103
|
-
let hasContactingToken = false;
|
|
104
124
|
const tokens = new TokenCollection(value, {
|
|
105
125
|
disallowToSurroundBySpaces: false,
|
|
106
126
|
allowEmpty: false,
|
|
@@ -122,21 +142,16 @@ export const checkAutoComplete = () => value => {
|
|
|
122
142
|
return listingChecked;
|
|
123
143
|
}
|
|
124
144
|
const identTokens = tokens.getIdentTokens();
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
if (!head) {
|
|
128
|
-
// Never
|
|
145
|
+
if (identTokens.length === 0) {
|
|
146
|
+
// Never — TokenCollection.check would catch empty
|
|
129
147
|
throw new Error('TokenCollection is empty');
|
|
130
148
|
}
|
|
131
|
-
|
|
132
|
-
//
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
if (tail[0]) {
|
|
138
|
-
acLog('[Unmatched ("%s")] Unexpected pair with "on" or "off": "%s"', value, tail.value);
|
|
139
|
-
return tail[0].unmatched({
|
|
149
|
+
const firstToken = identTokens[0];
|
|
150
|
+
// Check for "on" / "off"
|
|
151
|
+
if (firstToken.matches(['on', 'off'], true)) {
|
|
152
|
+
if (identTokens[1]) {
|
|
153
|
+
acLog('[Unmatched ("%s")] Unexpected pair with "on" or "off": "%s"', value, identTokens[1].value);
|
|
154
|
+
return identTokens[1].unmatched({
|
|
140
155
|
reason: 'extra-token',
|
|
141
156
|
expects: [
|
|
142
157
|
{
|
|
@@ -149,173 +164,207 @@ export const checkAutoComplete = () => value => {
|
|
|
149
164
|
}
|
|
150
165
|
return matched();
|
|
151
166
|
}
|
|
152
|
-
//
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
}
|
|
166
|
-
const headAndTail2 = tail.headAndTail();
|
|
167
|
-
head = headAndTail2.head;
|
|
168
|
-
tail = headAndTail2.tail;
|
|
169
|
-
if (!head) {
|
|
170
|
-
// Missing autofill field name but it is valid
|
|
171
|
-
return matched();
|
|
172
|
-
}
|
|
167
|
+
// Check for "on" / "off" appearing as the last token in a multi-token context
|
|
168
|
+
const lastIdentToken = identTokens.at(-1);
|
|
169
|
+
if (lastIdentToken.matches(['on', 'off'], true) && identTokens.length > 1) {
|
|
170
|
+
acLog('[Unmatched ("%s")] Extra token "on"/"off" at end: "%s"', value, lastIdentToken.value);
|
|
171
|
+
return lastIdentToken.unmatched({
|
|
172
|
+
reason: 'extra-token',
|
|
173
|
+
expects: [
|
|
174
|
+
{
|
|
175
|
+
type: 'format',
|
|
176
|
+
value: 'autocomplete',
|
|
177
|
+
},
|
|
178
|
+
],
|
|
179
|
+
ref: URL_AUTOFILL_FIELD,
|
|
180
|
+
});
|
|
173
181
|
}
|
|
174
|
-
//
|
|
175
|
-
|
|
176
|
-
//
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
182
|
+
// --- Backward parsing ---
|
|
183
|
+
let index = identTokens.length - 1;
|
|
184
|
+
// Step 1: Determine field category from last token
|
|
185
|
+
const lastToken = identTokens[index];
|
|
186
|
+
const fieldResult = determineFieldCategory(lastToken.value);
|
|
187
|
+
if (!fieldResult) {
|
|
188
|
+
// Last token is not a valid field name
|
|
189
|
+
const allFieldNames = [...autofillFieldNames, ...contactableFieldNames];
|
|
190
|
+
const expects = [
|
|
191
|
+
{
|
|
192
|
+
type: 'common',
|
|
193
|
+
value: 'autofill field name',
|
|
194
|
+
},
|
|
195
|
+
];
|
|
196
|
+
// If single token, also suggest named group
|
|
197
|
+
if (identTokens.length === 1) {
|
|
198
|
+
expects.unshift({
|
|
199
|
+
type: 'common',
|
|
200
|
+
value: 'autofill named group',
|
|
192
201
|
});
|
|
193
202
|
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
203
|
+
let candidate = getCandidate(lastToken.value, allFieldNames);
|
|
204
|
+
// If single token, also check for section- typo
|
|
205
|
+
if (!candidate && identTokens.length === 1) {
|
|
206
|
+
const [prefix, namedGroupStr] = lastToken.value.split('-');
|
|
207
|
+
const candidatePrefix = getCandidate(prefix, 'section');
|
|
208
|
+
if (candidatePrefix) {
|
|
209
|
+
candidate = `${candidatePrefix}-${namedGroupStr ?? ''}`;
|
|
210
|
+
}
|
|
200
211
|
}
|
|
212
|
+
acLog('[Unmatched ("%s")] Unexpected token: "%s"', value, lastToken.value);
|
|
213
|
+
return lastToken.unmatched({
|
|
214
|
+
reason: 'unexpected-token',
|
|
215
|
+
expects,
|
|
216
|
+
candidate,
|
|
217
|
+
ref: URL_AUTOFILL_FIELD,
|
|
218
|
+
});
|
|
201
219
|
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
if
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
reason: 'unexpected-token',
|
|
215
|
-
expects: contactableFieldNames.map(token => ({
|
|
216
|
-
type: 'const',
|
|
217
|
-
value: token,
|
|
218
|
-
})),
|
|
219
|
-
candidate,
|
|
220
|
-
ref: URL_CONTACTABLE_FIELD,
|
|
221
|
-
});
|
|
222
|
-
}
|
|
223
|
-
if (tail[1]) {
|
|
224
|
-
if (tail[1].matches(webauthnFieldNames)) {
|
|
225
|
-
return matched();
|
|
220
|
+
let { category } = fieldResult;
|
|
221
|
+
index--;
|
|
222
|
+
// Step 2: Handle webauthn (Credential category re-determination)
|
|
223
|
+
if (category === 'Credential') {
|
|
224
|
+
// webauthn token consumed; if there are more tokens, re-determine category
|
|
225
|
+
if (index >= 0) {
|
|
226
|
+
const preWebauthnToken = identTokens[index];
|
|
227
|
+
const reResult = determineFieldCategory(preWebauthnToken.value);
|
|
228
|
+
if (reResult && reResult.category !== 'Credential') {
|
|
229
|
+
// Re-determine: the token before webauthn is the actual field name
|
|
230
|
+
category = reResult.category;
|
|
231
|
+
index--;
|
|
226
232
|
}
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
acLog('[Unmatched ("%s")]
|
|
233
|
+
else if (reResult && reResult.category === 'Credential') {
|
|
234
|
+
// webauthn webauthn — duplicate caught by TokenCollection.check unique
|
|
235
|
+
acLog('[Unmatched ("%s")] Duplicate webauthn', value);
|
|
236
|
+
return preWebauthnToken.unmatched({
|
|
237
|
+
reason: 'extra-token',
|
|
238
|
+
expects: [{ type: 'format', value: 'autocomplete' }],
|
|
239
|
+
ref: URL_AUTOFILL_FIELD,
|
|
240
|
+
});
|
|
230
241
|
}
|
|
231
242
|
else {
|
|
232
|
-
|
|
243
|
+
// Token before webauthn is not a valid field name
|
|
244
|
+
const allFieldNames = [...autofillFieldNames, ...contactableFieldNames];
|
|
245
|
+
const candidate = getCandidate(preWebauthnToken.value, allFieldNames);
|
|
246
|
+
acLog('[Unmatched ("%s")] Unexpected token before webauthn: "%s"', value, preWebauthnToken.value);
|
|
247
|
+
return preWebauthnToken.unmatched({
|
|
248
|
+
reason: 'unexpected-token',
|
|
249
|
+
expects: [{ type: 'common', value: 'autofill field name' }],
|
|
250
|
+
candidate,
|
|
251
|
+
ref: URL_AUTOFILL_FIELD,
|
|
252
|
+
});
|
|
233
253
|
}
|
|
234
|
-
return tail[1].unmatched({
|
|
235
|
-
reason: 'extra-token',
|
|
236
|
-
expects: [
|
|
237
|
-
{
|
|
238
|
-
type: 'format',
|
|
239
|
-
value: 'autocomplete',
|
|
240
|
-
},
|
|
241
|
-
],
|
|
242
|
-
ref: URL_AUTOFILL_FIELD,
|
|
243
|
-
});
|
|
244
254
|
}
|
|
255
|
+
else {
|
|
256
|
+
// Standalone "webauthn" is valid
|
|
257
|
+
return matched();
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
// No more tokens to validate — only the field name was present
|
|
261
|
+
if (index < 0) {
|
|
245
262
|
return matched();
|
|
246
263
|
}
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
264
|
+
// Track which optional prefixes have been consumed
|
|
265
|
+
let hasPartOfAddress = false;
|
|
266
|
+
let hasNamedGroup = false;
|
|
267
|
+
// Step 3: If Contact category, optionally consume contacting token
|
|
268
|
+
if (category === 'Contact') {
|
|
269
|
+
const currentToken = identTokens[index];
|
|
270
|
+
if (currentToken.matches(contactingTokens, true)) {
|
|
271
|
+
index--;
|
|
272
|
+
if (index < 0) {
|
|
250
273
|
return matched();
|
|
251
274
|
}
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
// Step 4: If Normal category, the current token must NOT be a contacting token
|
|
278
|
+
// (contacting tokens are only valid before contactable field names)
|
|
279
|
+
if (category === 'Normal') {
|
|
280
|
+
const currentToken = identTokens[index];
|
|
281
|
+
if (currentToken.matches(contactingTokens, true)) {
|
|
282
|
+
acLog('[Unmatched ("%s")] Contacting token not valid for Normal field', value, currentToken.value);
|
|
283
|
+
return currentToken.unmatched({
|
|
284
|
+
reason: 'unexpected-token',
|
|
261
285
|
expects: [
|
|
286
|
+
...partOfAddress.map(token => ({
|
|
287
|
+
type: 'const',
|
|
288
|
+
value: token,
|
|
289
|
+
})),
|
|
262
290
|
{
|
|
263
|
-
type: '
|
|
264
|
-
value: '
|
|
291
|
+
type: 'common',
|
|
292
|
+
value: 'autofill named group',
|
|
265
293
|
},
|
|
266
294
|
],
|
|
267
|
-
ref:
|
|
295
|
+
ref: URL_PART_OF_ADDRESS,
|
|
268
296
|
});
|
|
269
297
|
}
|
|
270
|
-
return matched();
|
|
271
298
|
}
|
|
272
|
-
|
|
273
|
-
|
|
299
|
+
// Step 5: Optionally consume shipping/billing
|
|
300
|
+
if (index >= 0) {
|
|
301
|
+
const currentToken = identTokens[index];
|
|
302
|
+
if (currentToken.matches(partOfAddress, true)) {
|
|
303
|
+
hasPartOfAddress = true;
|
|
304
|
+
index--;
|
|
305
|
+
if (index < 0) {
|
|
306
|
+
return matched();
|
|
307
|
+
}
|
|
308
|
+
}
|
|
274
309
|
}
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
const
|
|
310
|
+
// Step 6: Optionally consume section-*
|
|
311
|
+
if (index >= 0) {
|
|
312
|
+
const currentToken = identTokens[index];
|
|
313
|
+
if (currentToken.matches(namedGroup, true)) {
|
|
314
|
+
hasNamedGroup = true;
|
|
315
|
+
index--;
|
|
316
|
+
if (index < 0) {
|
|
317
|
+
return matched();
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
// Step 7: If there are remaining tokens, they are extra
|
|
322
|
+
if (index >= 0) {
|
|
323
|
+
const extraToken = identTokens[index];
|
|
324
|
+
// Build expects based on what hasn't been consumed yet
|
|
325
|
+
const extraExpects = [];
|
|
326
|
+
if (!hasPartOfAddress && !hasNamedGroup) {
|
|
327
|
+
// Neither shipping/billing nor section-* consumed — could be either
|
|
328
|
+
extraExpects.push(...partOfAddress.map(token => ({
|
|
329
|
+
type: 'const',
|
|
330
|
+
value: token,
|
|
331
|
+
})), {
|
|
332
|
+
type: 'common',
|
|
333
|
+
value: 'autofill field name',
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
else if (hasNamedGroup) {
|
|
337
|
+
// Both consumed — nothing expected, pure extra
|
|
338
|
+
extraExpects.push({
|
|
339
|
+
type: 'common',
|
|
340
|
+
value: 'autofill named group',
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
else {
|
|
344
|
+
// shipping/billing consumed but section-* not — expect section-*
|
|
345
|
+
extraExpects.push({
|
|
346
|
+
type: 'common',
|
|
347
|
+
value: 'autofill named group',
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
// Check if it's a section-* typo
|
|
351
|
+
let candidate;
|
|
352
|
+
const [prefix, namedGroupStr] = extraToken.value.split('-');
|
|
289
353
|
const candidatePrefix = getCandidate(prefix, 'section');
|
|
290
354
|
if (candidatePrefix) {
|
|
291
355
|
candidate = `${candidatePrefix}-${namedGroupStr ?? ''}`;
|
|
292
356
|
}
|
|
357
|
+
if (!candidate) {
|
|
358
|
+
candidate = getCandidate(extraToken.value, partOfAddress, autofillFieldNames, contactableFieldNames);
|
|
359
|
+
}
|
|
360
|
+
const ref = !hasPartOfAddress && !hasNamedGroup ? URL_AUTOFILL_FIELD : URL_NAMED_GROUP;
|
|
361
|
+
acLog('[Unmatched ("%s")] Extra token: "%s"', value, extraToken.value);
|
|
362
|
+
return extraToken.unmatched({
|
|
363
|
+
reason: 'unexpected-token',
|
|
364
|
+
expects: extraExpects,
|
|
365
|
+
candidate,
|
|
366
|
+
ref,
|
|
367
|
+
});
|
|
293
368
|
}
|
|
294
|
-
|
|
295
|
-
expects.unshift(...[...partOfAddress].reverse().map(token => ({
|
|
296
|
-
type: 'const',
|
|
297
|
-
value: token,
|
|
298
|
-
})));
|
|
299
|
-
candidate = getCandidate(head.value, partOfAddress, autofillFieldNames, contactingTokens, contactableFieldNames);
|
|
300
|
-
}
|
|
301
|
-
else if (!hasContactingToken) {
|
|
302
|
-
expects.push(...contactingTokens.map(token => ({
|
|
303
|
-
type: 'const',
|
|
304
|
-
value: token,
|
|
305
|
-
})));
|
|
306
|
-
candidate = getCandidate(head.value, autofillFieldNames, contactingTokens, contactableFieldNames);
|
|
307
|
-
}
|
|
308
|
-
candidate = candidate ?? getCandidate(head.value, autofillFieldNames);
|
|
309
|
-
if (candidate) {
|
|
310
|
-
acLog('[Unmatched ("%s")] Unexpected token: "%s", Do you mean "%s"? ', value, head.value, candidate);
|
|
311
|
-
}
|
|
312
|
-
else {
|
|
313
|
-
acLog('[Unmatched ("%s")] Unexpected token: "%s"', value, head.value);
|
|
314
|
-
}
|
|
315
|
-
return head.unmatched({
|
|
316
|
-
reason: 'unexpected-token',
|
|
317
|
-
expects,
|
|
318
|
-
candidate,
|
|
319
|
-
ref: URL_AUTOFILL_FIELD,
|
|
320
|
-
});
|
|
369
|
+
return matched();
|
|
321
370
|
};
|
|
@@ -509,7 +509,7 @@ export function getMaxWeekNum(year) {
|
|
|
509
509
|
const day = d.getDay();
|
|
510
510
|
d.setDate(d.getDate() + 4 - (day > 0 ? day : 7));
|
|
511
511
|
const yearStart = new Date(d.getFullYear(), 0, 1);
|
|
512
|
-
const weekNo = Math.ceil(((d.valueOf() - yearStart.valueOf()) /
|
|
512
|
+
const weekNo = Math.ceil(((d.valueOf() - yearStart.valueOf()) / 86_400_000 + 1) / 7);
|
|
513
513
|
if (weekNo !== 1) {
|
|
514
514
|
return weekNo;
|
|
515
515
|
}
|
|
@@ -1,4 +1,107 @@
|
|
|
1
1
|
import type { CustomSyntaxChecker } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* A WHATWG-defined link type keyword with per-element context and body-ok flag.
|
|
4
|
+
*
|
|
5
|
+
* @see https://html.spec.whatwg.org/multipage/links.html#linkTypes
|
|
6
|
+
*/
|
|
7
|
+
export type DefLinkTypeWhatwg = {
|
|
8
|
+
readonly keyword: string;
|
|
9
|
+
readonly link: string;
|
|
10
|
+
readonly a: string;
|
|
11
|
+
readonly form: string;
|
|
12
|
+
readonly bodyOk: string;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* A Microformats-registered link type keyword with per-element context flags.
|
|
16
|
+
*
|
|
17
|
+
* @see https://microformats.org/wiki/existing-rel-values
|
|
18
|
+
*/
|
|
19
|
+
export type DefLinkTypeMicroformats = {
|
|
20
|
+
readonly keyword: string;
|
|
21
|
+
readonly link: boolean;
|
|
22
|
+
readonly a: boolean;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* A dropped, rejected, or non-HTML link type keyword from the Microformats registry.
|
|
26
|
+
*
|
|
27
|
+
* @see https://microformats.org/wiki/existing-rel-values
|
|
28
|
+
*/
|
|
29
|
+
export type DefLinkTypeMicroformatsDropped = {
|
|
30
|
+
readonly keyword: string;
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* @see https://html.spec.whatwg.org/multipage/links.html#linkTypes
|
|
34
|
+
*
|
|
35
|
+
* Scraping:
|
|
36
|
+
* ```js
|
|
37
|
+
* JSON.stringify(document.querySelectorAll('#table-link-relations tbody tr').values().toArray().map((el) => {
|
|
38
|
+
* const keyword = el.querySelector('td').textContent.trim();
|
|
39
|
+
* const link = el.querySelector('td:nth-child(2)').textContent.trim();
|
|
40
|
+
* const a = el.querySelector('td[colspan="3"]:nth-child(2)')?.textContent.trim() ?? el.querySelector('td[colspan="2"]:nth-child(2)')?.textContent.trim() ?? el.querySelector('td:nth-child(3)')?.textContent.trim();
|
|
41
|
+
* const form = el.querySelector('td[colspan="3"]:nth-child(2)')?.textContent.trim() ?? el.querySelector('td[colspan="2"]:nth-child(3)')?.textContent.trim() ?? el.querySelector('td[colspan="2"]:nth-child(2) + td')?.textContent.trim() ?? el.querySelector('td:nth-child(4)')?.textContent.trim();
|
|
42
|
+
* const bodyOk = el.querySelector('td[colspan="3"]:nth-child(2) + td')?.textContent.trim() ?? el.querySelector('td[colspan="2"]:nth-child(3) + td')?.textContent.trim() ?? el.querySelector('td[colspan="2"]:nth-child(2) + td + td')?.textContent.trim() ?? el.querySelector('td:nth-child(5)')?.textContent.trim();
|
|
43
|
+
* return { keyword, link, a, form, bodyOk }
|
|
44
|
+
* }));
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
export declare const DEF_LINK_TYPE_WHATWG: DefLinkTypeWhatwg[];
|
|
48
|
+
/**
|
|
49
|
+
* @see https://microformats.org/wiki/existing-rel-values#non_HTML_rel_values
|
|
50
|
+
*
|
|
51
|
+
* Scraping:
|
|
52
|
+
* ```js
|
|
53
|
+
* JSON.stringify($("h2:has('#non_HTML_rel_values') ~ table").eq(0).find('tr:has(td)').toArray().map((el) => {
|
|
54
|
+
* const $tr = $(el);
|
|
55
|
+
* const keyword = $tr.find('td')[0].textContent.trim();
|
|
56
|
+
* return { keyword, link: true, a: true }
|
|
57
|
+
* }));
|
|
58
|
+
*/
|
|
59
|
+
export declare const DEF_LINK_TYPE_MICROFORMATS_NON_HTML_REL_VALUES: DefLinkTypeMicroformatsDropped[];
|
|
60
|
+
/**
|
|
61
|
+
* @see https://microformats.org/wiki/existing-rel-values#dropped
|
|
62
|
+
*
|
|
63
|
+
* Scraping:
|
|
64
|
+
* ```js
|
|
65
|
+
* JSON.stringify($("h2:has('#dropped') ~ table").eq(0).find('tr:has(td)').toArray().map((el) => {
|
|
66
|
+
* const $tr = $(el);
|
|
67
|
+
* const keyword = $tr.find('td')[0].textContent.split(' ')[0]?.trim();
|
|
68
|
+
* return { keyword }
|
|
69
|
+
* }));
|
|
70
|
+
*/
|
|
71
|
+
export declare const DEF_LINK_TYPE_MICROFORMATS_DROPPED: DefLinkTypeMicroformatsDropped[];
|
|
72
|
+
/**
|
|
73
|
+
* @see https://microformats.org/wiki/existing-rel-values#dropped_without_prejudice
|
|
74
|
+
*
|
|
75
|
+
* Scraping:
|
|
76
|
+
* ```js
|
|
77
|
+
* JSON.stringify($("h2:has('#dropped_without_prejudice') ~ table").eq(0).find('tr:has(td)').toArray().map((el) => {
|
|
78
|
+
* const $tr = $(el);
|
|
79
|
+
* const keyword = $tr.find('td')[0].textContent.split(' ')[0]?.trim();
|
|
80
|
+
* return { keyword }
|
|
81
|
+
* }));
|
|
82
|
+
* ```
|
|
83
|
+
*/
|
|
84
|
+
export declare const DEF_LINK_TYPE_MICROFORMATS_DROPPED_WITHOUT_PREJUDICE: DefLinkTypeMicroformatsDropped[];
|
|
85
|
+
/**
|
|
86
|
+
* @see https://microformats.org/wiki/existing-rel-values#rejected
|
|
87
|
+
*
|
|
88
|
+
* Scraping:
|
|
89
|
+
* ```js
|
|
90
|
+
* JSON.stringify($("h2:has('#rejected') ~ table").eq(0).find('tr:has(td)').toArray().map((el) => {
|
|
91
|
+
* const $tr = $(el);
|
|
92
|
+
* const keyword = $tr.find('td')[0].textContent.split(' ')[0]?.trim();
|
|
93
|
+
* return { keyword }
|
|
94
|
+
* }));
|
|
95
|
+
* ```
|
|
96
|
+
*/
|
|
97
|
+
export declare const DEF_LINK_TYPE_MICROFORMATS_REJECTED: DefLinkTypeMicroformatsDropped[];
|
|
98
|
+
/**
|
|
99
|
+
* Combined list of all allowed Microformats link type keywords,
|
|
100
|
+
* excluding any that overlap with WHATWG standard keywords.
|
|
101
|
+
*
|
|
102
|
+
* @see https://microformats.org/wiki/existing-rel-values
|
|
103
|
+
*/
|
|
104
|
+
export declare const ALLOWED_LINK_TYPE_MICROFORMATS: DefLinkTypeMicroformats[];
|
|
2
105
|
/**
|
|
3
106
|
* Validates a link type attribute value against WHATWG and Microformats registries.
|
|
4
107
|
*
|
|
@@ -15,7 +15,7 @@ import { TokenCollection } from '../token/token-collection.js';
|
|
|
15
15
|
* }));
|
|
16
16
|
* ```
|
|
17
17
|
*/
|
|
18
|
-
const DEF_LINK_TYPE_WHATWG = [
|
|
18
|
+
export const DEF_LINK_TYPE_WHATWG = [
|
|
19
19
|
{ keyword: 'alternate', link: 'Hyperlink', a: 'Hyperlink', form: 'not allowed', bodyOk: '·' },
|
|
20
20
|
{ keyword: 'canonical', link: 'Hyperlink', a: 'not allowed', form: 'not allowed', bodyOk: '·' },
|
|
21
21
|
{ keyword: 'author', link: 'Hyperlink', a: 'Hyperlink', form: 'not allowed', bodyOk: '·' },
|
|
@@ -353,7 +353,7 @@ const DEF_LINK_TYPE_MICROFORMATS_DUBLIN_CORE = [
|
|
|
353
353
|
* return { keyword, link: true, a: true }
|
|
354
354
|
* }));
|
|
355
355
|
*/
|
|
356
|
-
const DEF_LINK_TYPE_MICROFORMATS_NON_HTML_REL_VALUES = [
|
|
356
|
+
export const DEF_LINK_TYPE_MICROFORMATS_NON_HTML_REL_VALUES = [
|
|
357
357
|
{ keyword: 'self' },
|
|
358
358
|
{ keyword: 'http://gdata.youtube.com/schemas/2007#in-reply-to' },
|
|
359
359
|
{ keyword: 'collection' },
|
|
@@ -394,7 +394,7 @@ const DEF_LINK_TYPE_MICROFORMATS_NON_HTML_REL_VALUES = [
|
|
|
394
394
|
* return { keyword }
|
|
395
395
|
* }));
|
|
396
396
|
*/
|
|
397
|
-
const DEF_LINK_TYPE_MICROFORMATS_DROPPED = [
|
|
397
|
+
export const DEF_LINK_TYPE_MICROFORMATS_DROPPED = [
|
|
398
398
|
{ keyword: 'banner' },
|
|
399
399
|
{ keyword: 'begin' },
|
|
400
400
|
{ keyword: 'biblioentry' }, // cspell:disable-line
|
|
@@ -430,7 +430,7 @@ const DEF_LINK_TYPE_MICROFORMATS_DROPPED = [
|
|
|
430
430
|
* }));
|
|
431
431
|
* ```
|
|
432
432
|
*/
|
|
433
|
-
const DEF_LINK_TYPE_MICROFORMATS_DROPPED_WITHOUT_PREJUDICE = [
|
|
433
|
+
export const DEF_LINK_TYPE_MICROFORMATS_DROPPED_WITHOUT_PREJUDICE = [
|
|
434
434
|
{ keyword: 'first' },
|
|
435
435
|
{ keyword: 'index' },
|
|
436
436
|
{ keyword: 'last' },
|
|
@@ -448,11 +448,17 @@ const DEF_LINK_TYPE_MICROFORMATS_DROPPED_WITHOUT_PREJUDICE = [
|
|
|
448
448
|
* }));
|
|
449
449
|
* ```
|
|
450
450
|
*/
|
|
451
|
-
const DEF_LINK_TYPE_MICROFORMATS_REJECTED = [
|
|
451
|
+
export const DEF_LINK_TYPE_MICROFORMATS_REJECTED = [
|
|
452
452
|
{ keyword: 'logo' },
|
|
453
453
|
{ keyword: 'pavatar' }, // cspell:disable-line
|
|
454
454
|
];
|
|
455
|
-
|
|
455
|
+
/**
|
|
456
|
+
* Combined list of all allowed Microformats link type keywords,
|
|
457
|
+
* excluding any that overlap with WHATWG standard keywords.
|
|
458
|
+
*
|
|
459
|
+
* @see https://microformats.org/wiki/existing-rel-values
|
|
460
|
+
*/
|
|
461
|
+
export const ALLOWED_LINK_TYPE_MICROFORMATS = [
|
|
456
462
|
...DEF_LINK_TYPE_MICROFORMATS_FORMATS,
|
|
457
463
|
...DEF_LINK_TYPE_MICROFORMATS_PROPOSALS,
|
|
458
464
|
...DEF_LINK_TYPE_MICROFORMATS_HTML5_LINK_TYPE_EXTENSIONS,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
// @ts-ignore
|
|
2
|
-
import MIMEType from 'whatwg-mimetype';
|
|
1
|
+
// @ts-ignore -- whatwg-mimetype v5 has no type definitions
|
|
2
|
+
import { MIMEType } from 'whatwg-mimetype';
|
|
3
3
|
import { matched, unmatched } from '../match-result.js';
|
|
4
4
|
import { Token } from '../token/index.js';
|
|
5
5
|
const expects = (withoutParameters) => [
|
package/lib/whatwg/is-abs-url.js
CHANGED
|
@@ -19,10 +19,7 @@ export const isAbsURL = () => {
|
|
|
19
19
|
new URL(value);
|
|
20
20
|
}
|
|
21
21
|
catch (error) {
|
|
22
|
-
if (error
|
|
23
|
-
typeof error === 'object' &&
|
|
24
|
-
'code' in error && // @ts-ignore
|
|
25
|
-
error.code === 'ERR_INVALID_URL') {
|
|
22
|
+
if (error instanceof TypeError) {
|
|
26
23
|
return false;
|
|
27
24
|
}
|
|
28
25
|
throw error;
|