@markuplint/types 4.8.2 → 5.0.0-alpha.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.
@@ -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
- * Supports "on"/"off" keywords, optional named groups (`section-*`),
6
- * address parts (shipping/billing), contacting tokens, and autofill field names.
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
- const URL_CONTACTABLE_FIELD = 'https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#autofilling-form-controls:-the-autocomplete-attribute:attr-fe-autocomplete-tel';
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
- * Supports "on"/"off" keywords, optional named groups (`section-*`),
96
- * address parts (shipping/billing), contacting tokens, and autofill field names.
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
- const headAndTail1 = identTokens.headAndTail();
126
- let { head, tail } = headAndTail1;
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
- // > When wearing the autofill anchor mantle, the autocomplete attribute,
132
- // > if specified, must have a value that is
133
- // > an ordered set of space-separated tokens consisting of
134
- // > just autofill detail tokens
135
- // > (i.e. the "on" and "off" keywords are not allowed).
136
- if (head.matches(['on', 'off'], true)) {
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
- // > Optionally, a token whose first eight characters are
153
- // > an ASCII case-insensitive match for the string "section-",
154
- // > meaning that the field belongs to the named group.
155
- if (head.matches(namedGroup, true)) {
156
- hasNamedGroup = true;
157
- const sectionToken = tail.search(namedGroup);
158
- if (sectionToken) {
159
- acLog('[Unmatched ("%s")] Deprecated in autofill named group: "%s"', value, sectionToken.value);
160
- return sectionToken.unmatched({
161
- partName: 'autofill named group',
162
- reason: 'duplicated',
163
- ref: URL_NAMED_GROUP,
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
- // > Optionally, a token that is an ASCII case-insensitive match for
175
- // > one of the following strings:
176
- // > - "shipping", meaning the field is part of the shipping address or contact information
177
- // > - "billing", meaning the field is part of the billing address or contact information
178
- if (head.matches(partOfAddress, true)) {
179
- hasPartOfAddress = true;
180
- const partToken = tail.search(partOfAddress);
181
- if (partToken) {
182
- acLog('[Unmatched ("%s")] Duplicated values: "%s"', value, partToken.value);
183
- return partToken.unmatched({
184
- reason: 'duplicated',
185
- expects: [
186
- {
187
- type: 'format',
188
- value: 'autocomplete',
189
- },
190
- ],
191
- ref: URL_PART_OF_ADDRESS,
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
- const headAndTail3 = tail.headAndTail();
195
- head = headAndTail3.head;
196
- tail = headAndTail3.tail;
197
- if (!head) {
198
- // Missing autofill field name but it is valid
199
- return matched();
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
- if (head.matches(contactingTokens, true)) {
203
- // eslint-disable-next-line no-useless-assignment
204
- hasContactingToken = true;
205
- const contactableFiledToken = tail[0];
206
- if (!contactableFiledToken) {
207
- // Missing autofill field name but it is valid
208
- return matched();
209
- }
210
- if (!contactableFiledToken.matches(contactableFieldNames, true)) {
211
- const candidate = getCandidate(contactableFiledToken.value, contactableFieldNames);
212
- acLog('[Unmatched ("%s")] Unexpected token: "%s"', value, contactableFiledToken.value);
213
- return contactableFiledToken.unmatched({
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
- const candidate = getCandidate(tail[1].value, webauthnFieldNames);
228
- if (candidate) {
229
- acLog('[Unmatched ("%s")] Unnecessarily token: "%s", Do you mean "%s"? ', value, tail[1].value, candidate);
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
- acLog('[Unmatched ("%s")] Unnecessarily token: "%s"', value, tail[1].value);
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
- if (head.matches([...autofillFieldNames, ...contactableFieldNames], true)) {
248
- if (tail[0]) {
249
- if (tail[0].matches(webauthnFieldNames)) {
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
- const candidate = getCandidate(tail[0].value, webauthnFieldNames);
253
- if (candidate) {
254
- acLog('[Unmatched ("%s")] Unnecessarily token: "%s", Do you mean "%s"? ', value, tail[0].value, candidate);
255
- }
256
- else {
257
- acLog('[Unmatched ("%s")] Unnecessarily token: "%s"', value, tail[0].value);
258
- }
259
- return tail[0].unmatched({
260
- reason: 'extra-token',
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: 'format',
264
- value: 'autocomplete',
291
+ type: 'common',
292
+ value: 'autofill named group',
265
293
  },
266
294
  ],
267
- ref: URL_AUTOFILL_FIELD,
295
+ ref: URL_PART_OF_ADDRESS,
268
296
  });
269
297
  }
270
- return matched();
271
298
  }
272
- if (head.matches(webauthnFieldNames)) {
273
- return matched();
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
- const expects = [
276
- {
277
- type: 'common',
278
- value: 'autofill field name',
279
- },
280
- ];
281
- let candidate;
282
- if (!hasNamedGroup) {
283
- expects.unshift({
284
- type: 'common',
285
- value: 'autofill named group',
286
- });
287
- // Potentially typo a named group
288
- const [prefix, namedGroupStr] = head.value.split('-');
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
- else if (!hasPartOfAddress) {
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()) / 86400000 + 1) / 7);
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
- const ALLOWED_LINK_TYPE_MICROFORMATS = [
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) => [
@@ -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;