html-validate 6.4.0 → 6.6.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/dist/es/core.js CHANGED
@@ -1476,8 +1476,8 @@ class DOMTokenList extends Array {
1476
1476
  constructor(value, location) {
1477
1477
  if (value && typeof value === "string") {
1478
1478
  /* replace all whitespace with a single space for easier parsing */
1479
- const condensed = value.replace(/[\t\r\n ]+/g, " ");
1480
- const { tokens, locations } = parse(condensed, location);
1479
+ const normalized = value.replace(/[\t\r\n]/g, " ");
1480
+ const { tokens, locations } = parse(normalized, location);
1481
1481
  super(...tokens);
1482
1482
  this.locations = locations;
1483
1483
  }
@@ -1880,10 +1880,13 @@ class HtmlElement extends DOMNode {
1880
1880
  }
1881
1881
  /**
1882
1882
  * @internal
1883
+ *
1884
+ * @param namespace - If given it is appended to the tagName.
1883
1885
  */
1884
- static fromTokens(startToken, endToken, parent, metaTable) {
1885
- const tagName = startToken.data[2];
1886
- if (!tagName) {
1886
+ static fromTokens(startToken, endToken, parent, metaTable, namespace = "") {
1887
+ const name = startToken.data[2];
1888
+ const tagName = namespace ? `${namespace}:${name}` : name;
1889
+ if (!name) {
1887
1890
  throw new Error("tagName cannot be empty");
1888
1891
  }
1889
1892
  const meta = metaTable ? metaTable.getMetaFor(tagName) : null;
@@ -2945,7 +2948,7 @@ var TRANSFORMER_API;
2945
2948
  /** @public */
2946
2949
  const name = "html-validate";
2947
2950
  /** @public */
2948
- const version = "6.4.0";
2951
+ const version = "6.6.1";
2949
2952
  /** @public */
2950
2953
  const homepage = "https://html-validate.org";
2951
2954
  /** @public */
@@ -2984,6 +2987,42 @@ function parseSeverity(value) {
2984
2987
  }
2985
2988
  }
2986
2989
 
2990
+ function escape(value) {
2991
+ return value.replace(/'/g, "\\'");
2992
+ }
2993
+ function format(value, quote = false) {
2994
+ if (value === null) {
2995
+ return "null";
2996
+ }
2997
+ if (typeof value === "number") {
2998
+ return value.toString();
2999
+ }
3000
+ if (typeof value === "string") {
3001
+ return quote ? `'${escape(value)}'` : value;
3002
+ }
3003
+ if (Array.isArray(value)) {
3004
+ const content = value.map((it) => format(it, true)).join(", ");
3005
+ return `[ ${content} ]`;
3006
+ }
3007
+ if (typeof value === "object") {
3008
+ const content = Object.entries(value)
3009
+ .map(([key, nested]) => `${key}: ${format(nested, true)}`)
3010
+ .join(", ");
3011
+ return `{ ${content} }`;
3012
+ }
3013
+ return String(value);
3014
+ }
3015
+ /**
3016
+ * Replaces placeholder `{{ ... }}` with values from given object.
3017
+ *
3018
+ * @internal
3019
+ */
3020
+ function interpolate(text, data) {
3021
+ return text.replace(/{{\s*([^\s]+)\s*}}/g, (match, key) => {
3022
+ return typeof data[key] !== "undefined" ? format(data[key]) : match;
3023
+ });
3024
+ }
3025
+
2987
3026
  const remapEvents = {
2988
3027
  "tag:open": "tag:start",
2989
3028
  "tag:close": "tag:end",
@@ -3118,7 +3157,8 @@ class Rule {
3118
3157
  report(node, message, location, context) {
3119
3158
  if (this.isEnabled() && (!node || node.ruleEnabled(this.name))) {
3120
3159
  const where = this.findLocation({ node, location, event: this.event });
3121
- this.reporter.add(this, message, this.severity, node, where, context);
3160
+ const interpolated = interpolate(message, context !== null && context !== void 0 ? context : {});
3161
+ this.reporter.add(this, interpolated, this.severity, node, where, context);
3122
3162
  }
3123
3163
  }
3124
3164
  findLocation(src) {
@@ -3643,22 +3683,21 @@ var TokenType;
3643
3683
  (function (TokenType) {
3644
3684
  TokenType[TokenType["UNICODE_BOM"] = 1] = "UNICODE_BOM";
3645
3685
  TokenType[TokenType["WHITESPACE"] = 2] = "WHITESPACE";
3646
- TokenType[TokenType["NEWLINE"] = 3] = "NEWLINE";
3647
- TokenType[TokenType["DOCTYPE_OPEN"] = 4] = "DOCTYPE_OPEN";
3648
- TokenType[TokenType["DOCTYPE_VALUE"] = 5] = "DOCTYPE_VALUE";
3649
- TokenType[TokenType["DOCTYPE_CLOSE"] = 6] = "DOCTYPE_CLOSE";
3650
- TokenType[TokenType["TAG_OPEN"] = 7] = "TAG_OPEN";
3651
- TokenType[TokenType["TAG_CLOSE"] = 8] = "TAG_CLOSE";
3652
- TokenType[TokenType["ATTR_NAME"] = 9] = "ATTR_NAME";
3653
- TokenType[TokenType["ATTR_VALUE"] = 10] = "ATTR_VALUE";
3654
- TokenType[TokenType["TEXT"] = 11] = "TEXT";
3655
- TokenType[TokenType["TEMPLATING"] = 12] = "TEMPLATING";
3656
- TokenType[TokenType["SCRIPT"] = 13] = "SCRIPT";
3657
- TokenType[TokenType["STYLE"] = 14] = "STYLE";
3658
- TokenType[TokenType["COMMENT"] = 15] = "COMMENT";
3659
- TokenType[TokenType["CONDITIONAL"] = 16] = "CONDITIONAL";
3660
- TokenType[TokenType["DIRECTIVE"] = 17] = "DIRECTIVE";
3661
- TokenType[TokenType["EOF"] = 18] = "EOF";
3686
+ TokenType[TokenType["DOCTYPE_OPEN"] = 3] = "DOCTYPE_OPEN";
3687
+ TokenType[TokenType["DOCTYPE_VALUE"] = 4] = "DOCTYPE_VALUE";
3688
+ TokenType[TokenType["DOCTYPE_CLOSE"] = 5] = "DOCTYPE_CLOSE";
3689
+ TokenType[TokenType["TAG_OPEN"] = 6] = "TAG_OPEN";
3690
+ TokenType[TokenType["TAG_CLOSE"] = 7] = "TAG_CLOSE";
3691
+ TokenType[TokenType["ATTR_NAME"] = 8] = "ATTR_NAME";
3692
+ TokenType[TokenType["ATTR_VALUE"] = 9] = "ATTR_VALUE";
3693
+ TokenType[TokenType["TEXT"] = 10] = "TEXT";
3694
+ TokenType[TokenType["TEMPLATING"] = 11] = "TEMPLATING";
3695
+ TokenType[TokenType["SCRIPT"] = 12] = "SCRIPT";
3696
+ TokenType[TokenType["STYLE"] = 13] = "STYLE";
3697
+ TokenType[TokenType["COMMENT"] = 14] = "COMMENT";
3698
+ TokenType[TokenType["CONDITIONAL"] = 15] = "CONDITIONAL";
3699
+ TokenType[TokenType["DIRECTIVE"] = 16] = "DIRECTIVE";
3700
+ TokenType[TokenType["EOF"] = 17] = "EOF";
3662
3701
  })(TokenType || (TokenType = {}));
3663
3702
 
3664
3703
  /* eslint-disable no-useless-escape */
@@ -3683,7 +3722,7 @@ const MATCH_SCRIPT_DATA = /^[^]*?(?=<\/script)/;
3683
3722
  const MATCH_SCRIPT_END = /^<(\/)(script)/;
3684
3723
  const MATCH_STYLE_DATA = /^[^]*?(?=<\/style)/;
3685
3724
  const MATCH_STYLE_END = /^<(\/)(style)/;
3686
- const MATCH_DIRECTIVE = /^<!--\s*\[html-validate-(.*?)]\s*-->/;
3725
+ const MATCH_DIRECTIVE = /^<!--\s*(\[)html-validate-([a-z0-9-]+)\s*(.*?)(]?)\s*-->/;
3687
3726
  const MATCH_COMMENT = /^<!--([^]*?)-->/;
3688
3727
  const MATCH_CONDITIONAL = /^<!\[([^\]]*?)\]>/;
3689
3728
  class InvalidTokenError extends Error {
@@ -3738,15 +3777,15 @@ class Lexer {
3738
3777
  previousState = context.state;
3739
3778
  previousLength = context.string.length;
3740
3779
  }
3741
- yield this.token(context, TokenType.EOF);
3780
+ yield this.token(context, TokenType.EOF, []);
3742
3781
  }
3743
3782
  token(context, type, data) {
3744
- const size = data ? data[0].length : 0;
3783
+ const size = data.length > 0 ? data[0].length : 0;
3745
3784
  const location = context.getLocation(size);
3746
3785
  return {
3747
3786
  type,
3748
3787
  location,
3749
- data: data ? Array.from(data) : null,
3788
+ data: Array.from(data),
3750
3789
  };
3751
3790
  }
3752
3791
  /* istanbul ignore next: used to provide a better error when an unhandled state happens */
@@ -3771,17 +3810,18 @@ class Lexer {
3771
3810
  }
3772
3811
  }
3773
3812
  *match(context, tests, error) {
3774
- let match = null;
3775
3813
  const n = tests.length;
3776
3814
  for (let i = 0; i < n; i++) {
3777
3815
  const [regex, nextState, tokenType] = tests[i];
3778
- if (regex === false || (match = context.string.match(regex))) {
3816
+ const match = regex ? context.string.match(regex) : [""];
3817
+ if (match) {
3779
3818
  let token = null;
3780
3819
  if (tokenType !== false) {
3781
- yield (token = this.token(context, tokenType, match));
3820
+ token = this.token(context, tokenType, match);
3821
+ yield token;
3782
3822
  }
3783
3823
  const state = this.evalNextState(nextState, token);
3784
- context.consume(match || 0, state);
3824
+ context.consume(match, state);
3785
3825
  this.enter(context, state, match);
3786
3826
  return;
3787
3827
  }
@@ -3828,18 +3868,19 @@ class Lexer {
3828
3868
  *tokenizeTag(context) {
3829
3869
  /* eslint-disable-next-line consistent-return -- exhaustive switch handled by typescript */
3830
3870
  function nextState(token) {
3871
+ const tagCloseToken = token;
3831
3872
  switch (context.contentModel) {
3832
3873
  case ContentModel.TEXT:
3833
3874
  return State.TEXT;
3834
3875
  case ContentModel.SCRIPT:
3835
- if (token && token.data[0][0] !== "/") {
3876
+ if (tagCloseToken && tagCloseToken.data[0][0] !== "/") {
3836
3877
  return State.SCRIPT;
3837
3878
  }
3838
3879
  else {
3839
3880
  return State.TEXT; /* <script/> (not legal but handle it anyway so the lexer doesn't choke on it) */
3840
3881
  }
3841
3882
  case ContentModel.STYLE:
3842
- if (token && token.data[0][0] !== "/") {
3883
+ if (tagCloseToken && tagCloseToken.data[0][0] !== "/") {
3843
3884
  return State.STYLE;
3844
3885
  }
3845
3886
  else {
@@ -9567,7 +9608,7 @@ const config$3 = {
9567
9608
  "no-redundant-for": "error",
9568
9609
  "no-redundant-role": "error",
9569
9610
  "prefer-native-element": "error",
9570
- "svg-focusable": "error",
9611
+ "svg-focusable": "off",
9571
9612
  "text-content": "error",
9572
9613
  "wcag/h30": "error",
9573
9614
  "wcag/h32": "error",
@@ -9638,7 +9679,7 @@ const config$1 = {
9638
9679
  "prefer-tbody": "error",
9639
9680
  "script-element": "error",
9640
9681
  "script-type": "error",
9641
- "svg-focusable": "error",
9682
+ "svg-focusable": "off",
9642
9683
  "text-content": "error",
9643
9684
  "unrecognized-char-ref": "error",
9644
9685
  void: "off",
@@ -10413,6 +10454,12 @@ class ParserError extends Error {
10413
10454
  }
10414
10455
  }
10415
10456
 
10457
+ function isAttrValueToken(token) {
10458
+ return Boolean(token && token.type === TokenType.ATTR_VALUE);
10459
+ }
10460
+ function svgShouldRetainTag(foreignTagName, tagName) {
10461
+ return foreignTagName === "svg" && ["title", "desc"].includes(tagName);
10462
+ }
10416
10463
  /**
10417
10464
  * Parse HTML document into a DOM tree.
10418
10465
  *
@@ -10425,6 +10472,7 @@ class Parser {
10425
10472
  * @param config - Configuration
10426
10473
  */
10427
10474
  constructor(config) {
10475
+ this.currentNamespace = "";
10428
10476
  this.event = new EventHandler();
10429
10477
  this.dom = null;
10430
10478
  this.metaTable = config.getMetaTable();
@@ -10435,7 +10483,6 @@ class Parser {
10435
10483
  * @param source - HTML markup.
10436
10484
  * @returns DOM tree representing the HTML markup.
10437
10485
  */
10438
- // eslint-disable-next-line complexity
10439
10486
  parseHtml(source) {
10440
10487
  var _a, _b, _c, _d;
10441
10488
  if (typeof source === "string") {
@@ -10466,40 +10513,7 @@ class Parser {
10466
10513
  let it = this.next(tokenStream);
10467
10514
  while (!it.done) {
10468
10515
  const token = it.value;
10469
- switch (token.type) {
10470
- case TokenType.UNICODE_BOM:
10471
- /* ignore */
10472
- break;
10473
- case TokenType.TAG_OPEN:
10474
- this.consumeTag(source, token, tokenStream);
10475
- break;
10476
- case TokenType.WHITESPACE:
10477
- this.trigger("whitespace", {
10478
- text: token.data[0],
10479
- location: token.location,
10480
- });
10481
- this.appendText(token.data[0], token.location);
10482
- break;
10483
- case TokenType.DIRECTIVE:
10484
- this.consumeDirective(token);
10485
- break;
10486
- case TokenType.CONDITIONAL:
10487
- this.consumeConditional(token);
10488
- break;
10489
- case TokenType.COMMENT:
10490
- this.consumeComment(token);
10491
- break;
10492
- case TokenType.DOCTYPE_OPEN:
10493
- this.consumeDoctype(token, tokenStream);
10494
- break;
10495
- case TokenType.TEXT:
10496
- case TokenType.TEMPLATING:
10497
- this.appendText(token.data, token.location);
10498
- break;
10499
- case TokenType.EOF:
10500
- this.closeTree(source, token.location);
10501
- break;
10502
- }
10516
+ this.consume(source, token, tokenStream);
10503
10517
  it = this.next(tokenStream);
10504
10518
  }
10505
10519
  /* resolve any dynamic meta element properties */
@@ -10546,13 +10560,50 @@ class Parser {
10546
10560
  return Boolean(active.parent && active.parent.is(tagName) && meta.includes(active.tagName));
10547
10561
  }
10548
10562
  }
10563
+ /* eslint-disable-next-line complexity */
10564
+ consume(source, token, tokenStream) {
10565
+ switch (token.type) {
10566
+ case TokenType.UNICODE_BOM:
10567
+ /* ignore */
10568
+ break;
10569
+ case TokenType.TAG_OPEN:
10570
+ this.consumeTag(source, token, tokenStream);
10571
+ break;
10572
+ case TokenType.WHITESPACE:
10573
+ this.trigger("whitespace", {
10574
+ text: token.data[0],
10575
+ location: token.location,
10576
+ });
10577
+ this.appendText(token.data[0], token.location);
10578
+ break;
10579
+ case TokenType.DIRECTIVE:
10580
+ this.consumeDirective(token);
10581
+ break;
10582
+ case TokenType.CONDITIONAL:
10583
+ this.consumeConditional(token);
10584
+ break;
10585
+ case TokenType.COMMENT:
10586
+ this.consumeComment(token);
10587
+ break;
10588
+ case TokenType.DOCTYPE_OPEN:
10589
+ this.consumeDoctype(token, tokenStream);
10590
+ break;
10591
+ case TokenType.TEXT:
10592
+ case TokenType.TEMPLATING:
10593
+ this.appendText(token.data[0], token.location);
10594
+ break;
10595
+ case TokenType.EOF:
10596
+ this.closeTree(source, token.location);
10597
+ break;
10598
+ }
10599
+ }
10549
10600
  /* eslint-disable-next-line complexity, sonarjs/cognitive-complexity */
10550
10601
  consumeTag(source, startToken, tokenStream) {
10551
10602
  const tokens = Array.from(this.consumeUntil(tokenStream, TokenType.TAG_CLOSE, startToken.location));
10552
10603
  const endToken = tokens.slice(-1)[0];
10553
10604
  const closeOptional = this.closeOptional(startToken);
10554
10605
  const parent = closeOptional ? this.dom.getActive().parent : this.dom.getActive();
10555
- const node = HtmlElement.fromTokens(startToken, endToken, parent, this.metaTable);
10606
+ const node = HtmlElement.fromTokens(startToken, endToken, parent, this.metaTable, this.currentNamespace);
10556
10607
  const isStartTag = !startToken.data[1];
10557
10608
  const isClosing = !isStartTag || node.closed !== NodeClosed.Open;
10558
10609
  const isForeign = node.meta && node.meta.foreign;
@@ -10657,6 +10708,15 @@ class Parser {
10657
10708
  const tokens = Array.from(this.consumeUntil(tokenStream, TokenType.TAG_OPEN, errorLocation));
10658
10709
  const [last] = tokens.slice(-1);
10659
10710
  const [, tagClosed, tagName] = last.data;
10711
+ /* special case: svg <title> and <desc> should be intact as it affects accessibility */
10712
+ if (!tagClosed && svgShouldRetainTag(foreignTagName, tagName)) {
10713
+ const oldNamespace = this.currentNamespace;
10714
+ this.currentNamespace = "svg";
10715
+ this.consumeTag(source, last, tokenStream);
10716
+ this.consumeUntilMatchingTag(source, tokenStream, tagName);
10717
+ this.currentNamespace = oldNamespace;
10718
+ continue;
10719
+ }
10660
10720
  /* keep going unless the new tag matches the foreign root element */
10661
10721
  if (tagName !== foreignTagName) {
10662
10722
  continue;
@@ -10689,15 +10749,15 @@ class Parser {
10689
10749
  const keyLocation = this.getAttributeKeyLocation(token);
10690
10750
  const valueLocation = this.getAttributeValueLocation(next);
10691
10751
  const location = this.getAttributeLocation(token, next);
10692
- const haveValue = next && next.type === TokenType.ATTR_VALUE;
10752
+ const haveValue = isAttrValueToken(next);
10693
10753
  const attrData = {
10694
10754
  key: token.data[1],
10695
10755
  value: null,
10696
10756
  quote: null,
10697
10757
  };
10698
- if (next && haveValue) {
10758
+ if (haveValue) {
10699
10759
  const [, , value, quote] = next.data;
10700
- attrData.value = value !== null && value !== void 0 ? value : null;
10760
+ attrData.value = value;
10701
10761
  attrData.quote = quote !== null && quote !== void 0 ? quote : null;
10702
10762
  }
10703
10763
  /* get callback to process attributes, default is to just return attribute
@@ -10776,12 +10836,16 @@ class Parser {
10776
10836
  };
10777
10837
  }
10778
10838
  consumeDirective(token) {
10779
- const directive = token.data[1];
10780
- const match = directive.match(/^([a-zA-Z0-9-]+)\s*(.*?)(?:\s*:\s*(.*))?$/);
10839
+ const [text, , action, directive, end] = token.data;
10840
+ if (end === "") {
10841
+ throw new Error(`Missing end bracket "]" on directive "${text}"`);
10842
+ }
10843
+ const match = directive.match(/^(.*?)(?:\s*(?:--|:)\s*(.*))?$/);
10844
+ /* istanbul ignore next: should not be possible, would be emitted as comment token */
10781
10845
  if (!match) {
10782
- throw new Error(`Failed to parse directive "${directive}"`);
10846
+ throw new Error(`Failed to parse directive "${text}"`);
10783
10847
  }
10784
- const [, action, data, comment] = match;
10848
+ const [, data, comment] = match;
10785
10849
  this.trigger("directive", {
10786
10850
  action,
10787
10851
  data,
@@ -10824,7 +10888,8 @@ class Parser {
10824
10888
  */
10825
10889
  consumeDoctype(startToken, tokenStream) {
10826
10890
  const tokens = Array.from(this.consumeUntil(tokenStream, TokenType.DOCTYPE_CLOSE, startToken.location));
10827
- const doctype = tokens[0]; /* first token is the doctype, second is the closing ">" */
10891
+ /* first token is the doctype, second is the closing ">" */
10892
+ const doctype = tokens[0];
10828
10893
  const value = doctype.data[0];
10829
10894
  this.dom.doctype = value;
10830
10895
  this.trigger("doctype", {
@@ -10850,6 +10915,35 @@ class Parser {
10850
10915
  }
10851
10916
  throw new ParserError(errorLocation, `stream ended before ${TokenType[search]} token was found`);
10852
10917
  }
10918
+ /**
10919
+ * Consumes tokens until a matching close-tag is found. Tags are appended to
10920
+ * the document.
10921
+ *
10922
+ * @internal
10923
+ */
10924
+ consumeUntilMatchingTag(source, tokenStream, searchTag) {
10925
+ let numOpen = 1;
10926
+ let it = this.next(tokenStream);
10927
+ while (!it.done) {
10928
+ const token = it.value;
10929
+ this.consume(source, token, tokenStream);
10930
+ if (token.type === TokenType.TAG_OPEN) {
10931
+ const [, close, tagName] = token.data;
10932
+ if (tagName === searchTag) {
10933
+ if (close) {
10934
+ numOpen--;
10935
+ }
10936
+ else {
10937
+ numOpen++;
10938
+ }
10939
+ if (numOpen === 0) {
10940
+ return;
10941
+ }
10942
+ }
10943
+ }
10944
+ it = this.next(tokenStream);
10945
+ }
10946
+ }
10853
10947
  next(tokenStream) {
10854
10948
  const it = tokenStream.next();
10855
10949
  if (!it.done) {
@@ -10857,7 +10951,7 @@ class Parser {
10857
10951
  this.trigger("token", {
10858
10952
  location: token.location,
10859
10953
  type: token.type,
10860
- data: token.data ? Array.from(token.data) : undefined,
10954
+ data: Array.from(token.data),
10861
10955
  });
10862
10956
  }
10863
10957
  return it;
@@ -11117,11 +11211,12 @@ class Engine {
11117
11211
  return lines;
11118
11212
  }
11119
11213
  dumpTokens(source) {
11214
+ var _a;
11120
11215
  const lexer = new Lexer();
11121
11216
  const lines = [];
11122
11217
  for (const src of source) {
11123
11218
  for (const token of lexer.tokenize(src)) {
11124
- const data = token.data ? token.data[0] : null;
11219
+ const data = (_a = token.data[0]) !== null && _a !== void 0 ? _a : "";
11125
11220
  lines.push({
11126
11221
  token: TokenType[token.type],
11127
11222
  data,