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/cjs/core.js CHANGED
@@ -1487,8 +1487,8 @@ class DOMTokenList extends Array {
1487
1487
  constructor(value, location) {
1488
1488
  if (value && typeof value === "string") {
1489
1489
  /* replace all whitespace with a single space for easier parsing */
1490
- const condensed = value.replace(/[\t\r\n ]+/g, " ");
1491
- const { tokens, locations } = parse(condensed, location);
1490
+ const normalized = value.replace(/[\t\r\n]/g, " ");
1491
+ const { tokens, locations } = parse(normalized, location);
1492
1492
  super(...tokens);
1493
1493
  this.locations = locations;
1494
1494
  }
@@ -1891,10 +1891,13 @@ class HtmlElement extends DOMNode {
1891
1891
  }
1892
1892
  /**
1893
1893
  * @internal
1894
+ *
1895
+ * @param namespace - If given it is appended to the tagName.
1894
1896
  */
1895
- static fromTokens(startToken, endToken, parent, metaTable) {
1896
- const tagName = startToken.data[2];
1897
- if (!tagName) {
1897
+ static fromTokens(startToken, endToken, parent, metaTable, namespace = "") {
1898
+ const name = startToken.data[2];
1899
+ const tagName = namespace ? `${namespace}:${name}` : name;
1900
+ if (!name) {
1898
1901
  throw new Error("tagName cannot be empty");
1899
1902
  }
1900
1903
  const meta = metaTable ? metaTable.getMetaFor(tagName) : null;
@@ -2956,7 +2959,7 @@ var TRANSFORMER_API;
2956
2959
  /** @public */
2957
2960
  const name = "html-validate";
2958
2961
  /** @public */
2959
- const version = "6.4.0";
2962
+ const version = "6.6.1";
2960
2963
  /** @public */
2961
2964
  const homepage = "https://html-validate.org";
2962
2965
  /** @public */
@@ -2995,6 +2998,42 @@ function parseSeverity(value) {
2995
2998
  }
2996
2999
  }
2997
3000
 
3001
+ function escape(value) {
3002
+ return value.replace(/'/g, "\\'");
3003
+ }
3004
+ function format(value, quote = false) {
3005
+ if (value === null) {
3006
+ return "null";
3007
+ }
3008
+ if (typeof value === "number") {
3009
+ return value.toString();
3010
+ }
3011
+ if (typeof value === "string") {
3012
+ return quote ? `'${escape(value)}'` : value;
3013
+ }
3014
+ if (Array.isArray(value)) {
3015
+ const content = value.map((it) => format(it, true)).join(", ");
3016
+ return `[ ${content} ]`;
3017
+ }
3018
+ if (typeof value === "object") {
3019
+ const content = Object.entries(value)
3020
+ .map(([key, nested]) => `${key}: ${format(nested, true)}`)
3021
+ .join(", ");
3022
+ return `{ ${content} }`;
3023
+ }
3024
+ return String(value);
3025
+ }
3026
+ /**
3027
+ * Replaces placeholder `{{ ... }}` with values from given object.
3028
+ *
3029
+ * @internal
3030
+ */
3031
+ function interpolate(text, data) {
3032
+ return text.replace(/{{\s*([^\s]+)\s*}}/g, (match, key) => {
3033
+ return typeof data[key] !== "undefined" ? format(data[key]) : match;
3034
+ });
3035
+ }
3036
+
2998
3037
  const remapEvents = {
2999
3038
  "tag:open": "tag:start",
3000
3039
  "tag:close": "tag:end",
@@ -3129,7 +3168,8 @@ class Rule {
3129
3168
  report(node, message, location, context) {
3130
3169
  if (this.isEnabled() && (!node || node.ruleEnabled(this.name))) {
3131
3170
  const where = this.findLocation({ node, location, event: this.event });
3132
- this.reporter.add(this, message, this.severity, node, where, context);
3171
+ const interpolated = interpolate(message, context !== null && context !== void 0 ? context : {});
3172
+ this.reporter.add(this, interpolated, this.severity, node, where, context);
3133
3173
  }
3134
3174
  }
3135
3175
  findLocation(src) {
@@ -3654,22 +3694,21 @@ exports.TokenType = void 0;
3654
3694
  (function (TokenType) {
3655
3695
  TokenType[TokenType["UNICODE_BOM"] = 1] = "UNICODE_BOM";
3656
3696
  TokenType[TokenType["WHITESPACE"] = 2] = "WHITESPACE";
3657
- TokenType[TokenType["NEWLINE"] = 3] = "NEWLINE";
3658
- TokenType[TokenType["DOCTYPE_OPEN"] = 4] = "DOCTYPE_OPEN";
3659
- TokenType[TokenType["DOCTYPE_VALUE"] = 5] = "DOCTYPE_VALUE";
3660
- TokenType[TokenType["DOCTYPE_CLOSE"] = 6] = "DOCTYPE_CLOSE";
3661
- TokenType[TokenType["TAG_OPEN"] = 7] = "TAG_OPEN";
3662
- TokenType[TokenType["TAG_CLOSE"] = 8] = "TAG_CLOSE";
3663
- TokenType[TokenType["ATTR_NAME"] = 9] = "ATTR_NAME";
3664
- TokenType[TokenType["ATTR_VALUE"] = 10] = "ATTR_VALUE";
3665
- TokenType[TokenType["TEXT"] = 11] = "TEXT";
3666
- TokenType[TokenType["TEMPLATING"] = 12] = "TEMPLATING";
3667
- TokenType[TokenType["SCRIPT"] = 13] = "SCRIPT";
3668
- TokenType[TokenType["STYLE"] = 14] = "STYLE";
3669
- TokenType[TokenType["COMMENT"] = 15] = "COMMENT";
3670
- TokenType[TokenType["CONDITIONAL"] = 16] = "CONDITIONAL";
3671
- TokenType[TokenType["DIRECTIVE"] = 17] = "DIRECTIVE";
3672
- TokenType[TokenType["EOF"] = 18] = "EOF";
3697
+ TokenType[TokenType["DOCTYPE_OPEN"] = 3] = "DOCTYPE_OPEN";
3698
+ TokenType[TokenType["DOCTYPE_VALUE"] = 4] = "DOCTYPE_VALUE";
3699
+ TokenType[TokenType["DOCTYPE_CLOSE"] = 5] = "DOCTYPE_CLOSE";
3700
+ TokenType[TokenType["TAG_OPEN"] = 6] = "TAG_OPEN";
3701
+ TokenType[TokenType["TAG_CLOSE"] = 7] = "TAG_CLOSE";
3702
+ TokenType[TokenType["ATTR_NAME"] = 8] = "ATTR_NAME";
3703
+ TokenType[TokenType["ATTR_VALUE"] = 9] = "ATTR_VALUE";
3704
+ TokenType[TokenType["TEXT"] = 10] = "TEXT";
3705
+ TokenType[TokenType["TEMPLATING"] = 11] = "TEMPLATING";
3706
+ TokenType[TokenType["SCRIPT"] = 12] = "SCRIPT";
3707
+ TokenType[TokenType["STYLE"] = 13] = "STYLE";
3708
+ TokenType[TokenType["COMMENT"] = 14] = "COMMENT";
3709
+ TokenType[TokenType["CONDITIONAL"] = 15] = "CONDITIONAL";
3710
+ TokenType[TokenType["DIRECTIVE"] = 16] = "DIRECTIVE";
3711
+ TokenType[TokenType["EOF"] = 17] = "EOF";
3673
3712
  })(exports.TokenType || (exports.TokenType = {}));
3674
3713
 
3675
3714
  /* eslint-disable no-useless-escape */
@@ -3694,7 +3733,7 @@ const MATCH_SCRIPT_DATA = /^[^]*?(?=<\/script)/;
3694
3733
  const MATCH_SCRIPT_END = /^<(\/)(script)/;
3695
3734
  const MATCH_STYLE_DATA = /^[^]*?(?=<\/style)/;
3696
3735
  const MATCH_STYLE_END = /^<(\/)(style)/;
3697
- const MATCH_DIRECTIVE = /^<!--\s*\[html-validate-(.*?)]\s*-->/;
3736
+ const MATCH_DIRECTIVE = /^<!--\s*(\[)html-validate-([a-z0-9-]+)\s*(.*?)(]?)\s*-->/;
3698
3737
  const MATCH_COMMENT = /^<!--([^]*?)-->/;
3699
3738
  const MATCH_CONDITIONAL = /^<!\[([^\]]*?)\]>/;
3700
3739
  class InvalidTokenError extends Error {
@@ -3749,15 +3788,15 @@ class Lexer {
3749
3788
  previousState = context.state;
3750
3789
  previousLength = context.string.length;
3751
3790
  }
3752
- yield this.token(context, exports.TokenType.EOF);
3791
+ yield this.token(context, exports.TokenType.EOF, []);
3753
3792
  }
3754
3793
  token(context, type, data) {
3755
- const size = data ? data[0].length : 0;
3794
+ const size = data.length > 0 ? data[0].length : 0;
3756
3795
  const location = context.getLocation(size);
3757
3796
  return {
3758
3797
  type,
3759
3798
  location,
3760
- data: data ? Array.from(data) : null,
3799
+ data: Array.from(data),
3761
3800
  };
3762
3801
  }
3763
3802
  /* istanbul ignore next: used to provide a better error when an unhandled state happens */
@@ -3782,17 +3821,18 @@ class Lexer {
3782
3821
  }
3783
3822
  }
3784
3823
  *match(context, tests, error) {
3785
- let match = null;
3786
3824
  const n = tests.length;
3787
3825
  for (let i = 0; i < n; i++) {
3788
3826
  const [regex, nextState, tokenType] = tests[i];
3789
- if (regex === false || (match = context.string.match(regex))) {
3827
+ const match = regex ? context.string.match(regex) : [""];
3828
+ if (match) {
3790
3829
  let token = null;
3791
3830
  if (tokenType !== false) {
3792
- yield (token = this.token(context, tokenType, match));
3831
+ token = this.token(context, tokenType, match);
3832
+ yield token;
3793
3833
  }
3794
3834
  const state = this.evalNextState(nextState, token);
3795
- context.consume(match || 0, state);
3835
+ context.consume(match, state);
3796
3836
  this.enter(context, state, match);
3797
3837
  return;
3798
3838
  }
@@ -3839,18 +3879,19 @@ class Lexer {
3839
3879
  *tokenizeTag(context) {
3840
3880
  /* eslint-disable-next-line consistent-return -- exhaustive switch handled by typescript */
3841
3881
  function nextState(token) {
3882
+ const tagCloseToken = token;
3842
3883
  switch (context.contentModel) {
3843
3884
  case ContentModel.TEXT:
3844
3885
  return State.TEXT;
3845
3886
  case ContentModel.SCRIPT:
3846
- if (token && token.data[0][0] !== "/") {
3887
+ if (tagCloseToken && tagCloseToken.data[0][0] !== "/") {
3847
3888
  return State.SCRIPT;
3848
3889
  }
3849
3890
  else {
3850
3891
  return State.TEXT; /* <script/> (not legal but handle it anyway so the lexer doesn't choke on it) */
3851
3892
  }
3852
3893
  case ContentModel.STYLE:
3853
- if (token && token.data[0][0] !== "/") {
3894
+ if (tagCloseToken && tagCloseToken.data[0][0] !== "/") {
3854
3895
  return State.STYLE;
3855
3896
  }
3856
3897
  else {
@@ -9578,7 +9619,7 @@ const config$3 = {
9578
9619
  "no-redundant-for": "error",
9579
9620
  "no-redundant-role": "error",
9580
9621
  "prefer-native-element": "error",
9581
- "svg-focusable": "error",
9622
+ "svg-focusable": "off",
9582
9623
  "text-content": "error",
9583
9624
  "wcag/h30": "error",
9584
9625
  "wcag/h32": "error",
@@ -9649,7 +9690,7 @@ const config$1 = {
9649
9690
  "prefer-tbody": "error",
9650
9691
  "script-element": "error",
9651
9692
  "script-type": "error",
9652
- "svg-focusable": "error",
9693
+ "svg-focusable": "off",
9653
9694
  "text-content": "error",
9654
9695
  "unrecognized-char-ref": "error",
9655
9696
  void: "off",
@@ -10424,6 +10465,12 @@ class ParserError extends Error {
10424
10465
  }
10425
10466
  }
10426
10467
 
10468
+ function isAttrValueToken(token) {
10469
+ return Boolean(token && token.type === exports.TokenType.ATTR_VALUE);
10470
+ }
10471
+ function svgShouldRetainTag(foreignTagName, tagName) {
10472
+ return foreignTagName === "svg" && ["title", "desc"].includes(tagName);
10473
+ }
10427
10474
  /**
10428
10475
  * Parse HTML document into a DOM tree.
10429
10476
  *
@@ -10436,6 +10483,7 @@ class Parser {
10436
10483
  * @param config - Configuration
10437
10484
  */
10438
10485
  constructor(config) {
10486
+ this.currentNamespace = "";
10439
10487
  this.event = new EventHandler();
10440
10488
  this.dom = null;
10441
10489
  this.metaTable = config.getMetaTable();
@@ -10446,7 +10494,6 @@ class Parser {
10446
10494
  * @param source - HTML markup.
10447
10495
  * @returns DOM tree representing the HTML markup.
10448
10496
  */
10449
- // eslint-disable-next-line complexity
10450
10497
  parseHtml(source) {
10451
10498
  var _a, _b, _c, _d;
10452
10499
  if (typeof source === "string") {
@@ -10477,40 +10524,7 @@ class Parser {
10477
10524
  let it = this.next(tokenStream);
10478
10525
  while (!it.done) {
10479
10526
  const token = it.value;
10480
- switch (token.type) {
10481
- case exports.TokenType.UNICODE_BOM:
10482
- /* ignore */
10483
- break;
10484
- case exports.TokenType.TAG_OPEN:
10485
- this.consumeTag(source, token, tokenStream);
10486
- break;
10487
- case exports.TokenType.WHITESPACE:
10488
- this.trigger("whitespace", {
10489
- text: token.data[0],
10490
- location: token.location,
10491
- });
10492
- this.appendText(token.data[0], token.location);
10493
- break;
10494
- case exports.TokenType.DIRECTIVE:
10495
- this.consumeDirective(token);
10496
- break;
10497
- case exports.TokenType.CONDITIONAL:
10498
- this.consumeConditional(token);
10499
- break;
10500
- case exports.TokenType.COMMENT:
10501
- this.consumeComment(token);
10502
- break;
10503
- case exports.TokenType.DOCTYPE_OPEN:
10504
- this.consumeDoctype(token, tokenStream);
10505
- break;
10506
- case exports.TokenType.TEXT:
10507
- case exports.TokenType.TEMPLATING:
10508
- this.appendText(token.data, token.location);
10509
- break;
10510
- case exports.TokenType.EOF:
10511
- this.closeTree(source, token.location);
10512
- break;
10513
- }
10527
+ this.consume(source, token, tokenStream);
10514
10528
  it = this.next(tokenStream);
10515
10529
  }
10516
10530
  /* resolve any dynamic meta element properties */
@@ -10557,13 +10571,50 @@ class Parser {
10557
10571
  return Boolean(active.parent && active.parent.is(tagName) && meta.includes(active.tagName));
10558
10572
  }
10559
10573
  }
10574
+ /* eslint-disable-next-line complexity */
10575
+ consume(source, token, tokenStream) {
10576
+ switch (token.type) {
10577
+ case exports.TokenType.UNICODE_BOM:
10578
+ /* ignore */
10579
+ break;
10580
+ case exports.TokenType.TAG_OPEN:
10581
+ this.consumeTag(source, token, tokenStream);
10582
+ break;
10583
+ case exports.TokenType.WHITESPACE:
10584
+ this.trigger("whitespace", {
10585
+ text: token.data[0],
10586
+ location: token.location,
10587
+ });
10588
+ this.appendText(token.data[0], token.location);
10589
+ break;
10590
+ case exports.TokenType.DIRECTIVE:
10591
+ this.consumeDirective(token);
10592
+ break;
10593
+ case exports.TokenType.CONDITIONAL:
10594
+ this.consumeConditional(token);
10595
+ break;
10596
+ case exports.TokenType.COMMENT:
10597
+ this.consumeComment(token);
10598
+ break;
10599
+ case exports.TokenType.DOCTYPE_OPEN:
10600
+ this.consumeDoctype(token, tokenStream);
10601
+ break;
10602
+ case exports.TokenType.TEXT:
10603
+ case exports.TokenType.TEMPLATING:
10604
+ this.appendText(token.data[0], token.location);
10605
+ break;
10606
+ case exports.TokenType.EOF:
10607
+ this.closeTree(source, token.location);
10608
+ break;
10609
+ }
10610
+ }
10560
10611
  /* eslint-disable-next-line complexity, sonarjs/cognitive-complexity */
10561
10612
  consumeTag(source, startToken, tokenStream) {
10562
10613
  const tokens = Array.from(this.consumeUntil(tokenStream, exports.TokenType.TAG_CLOSE, startToken.location));
10563
10614
  const endToken = tokens.slice(-1)[0];
10564
10615
  const closeOptional = this.closeOptional(startToken);
10565
10616
  const parent = closeOptional ? this.dom.getActive().parent : this.dom.getActive();
10566
- const node = HtmlElement.fromTokens(startToken, endToken, parent, this.metaTable);
10617
+ const node = HtmlElement.fromTokens(startToken, endToken, parent, this.metaTable, this.currentNamespace);
10567
10618
  const isStartTag = !startToken.data[1];
10568
10619
  const isClosing = !isStartTag || node.closed !== exports.NodeClosed.Open;
10569
10620
  const isForeign = node.meta && node.meta.foreign;
@@ -10668,6 +10719,15 @@ class Parser {
10668
10719
  const tokens = Array.from(this.consumeUntil(tokenStream, exports.TokenType.TAG_OPEN, errorLocation));
10669
10720
  const [last] = tokens.slice(-1);
10670
10721
  const [, tagClosed, tagName] = last.data;
10722
+ /* special case: svg <title> and <desc> should be intact as it affects accessibility */
10723
+ if (!tagClosed && svgShouldRetainTag(foreignTagName, tagName)) {
10724
+ const oldNamespace = this.currentNamespace;
10725
+ this.currentNamespace = "svg";
10726
+ this.consumeTag(source, last, tokenStream);
10727
+ this.consumeUntilMatchingTag(source, tokenStream, tagName);
10728
+ this.currentNamespace = oldNamespace;
10729
+ continue;
10730
+ }
10671
10731
  /* keep going unless the new tag matches the foreign root element */
10672
10732
  if (tagName !== foreignTagName) {
10673
10733
  continue;
@@ -10700,15 +10760,15 @@ class Parser {
10700
10760
  const keyLocation = this.getAttributeKeyLocation(token);
10701
10761
  const valueLocation = this.getAttributeValueLocation(next);
10702
10762
  const location = this.getAttributeLocation(token, next);
10703
- const haveValue = next && next.type === exports.TokenType.ATTR_VALUE;
10763
+ const haveValue = isAttrValueToken(next);
10704
10764
  const attrData = {
10705
10765
  key: token.data[1],
10706
10766
  value: null,
10707
10767
  quote: null,
10708
10768
  };
10709
- if (next && haveValue) {
10769
+ if (haveValue) {
10710
10770
  const [, , value, quote] = next.data;
10711
- attrData.value = value !== null && value !== void 0 ? value : null;
10771
+ attrData.value = value;
10712
10772
  attrData.quote = quote !== null && quote !== void 0 ? quote : null;
10713
10773
  }
10714
10774
  /* get callback to process attributes, default is to just return attribute
@@ -10787,12 +10847,16 @@ class Parser {
10787
10847
  };
10788
10848
  }
10789
10849
  consumeDirective(token) {
10790
- const directive = token.data[1];
10791
- const match = directive.match(/^([a-zA-Z0-9-]+)\s*(.*?)(?:\s*:\s*(.*))?$/);
10850
+ const [text, , action, directive, end] = token.data;
10851
+ if (end === "") {
10852
+ throw new Error(`Missing end bracket "]" on directive "${text}"`);
10853
+ }
10854
+ const match = directive.match(/^(.*?)(?:\s*(?:--|:)\s*(.*))?$/);
10855
+ /* istanbul ignore next: should not be possible, would be emitted as comment token */
10792
10856
  if (!match) {
10793
- throw new Error(`Failed to parse directive "${directive}"`);
10857
+ throw new Error(`Failed to parse directive "${text}"`);
10794
10858
  }
10795
- const [, action, data, comment] = match;
10859
+ const [, data, comment] = match;
10796
10860
  this.trigger("directive", {
10797
10861
  action,
10798
10862
  data,
@@ -10835,7 +10899,8 @@ class Parser {
10835
10899
  */
10836
10900
  consumeDoctype(startToken, tokenStream) {
10837
10901
  const tokens = Array.from(this.consumeUntil(tokenStream, exports.TokenType.DOCTYPE_CLOSE, startToken.location));
10838
- const doctype = tokens[0]; /* first token is the doctype, second is the closing ">" */
10902
+ /* first token is the doctype, second is the closing ">" */
10903
+ const doctype = tokens[0];
10839
10904
  const value = doctype.data[0];
10840
10905
  this.dom.doctype = value;
10841
10906
  this.trigger("doctype", {
@@ -10861,6 +10926,35 @@ class Parser {
10861
10926
  }
10862
10927
  throw new ParserError(errorLocation, `stream ended before ${exports.TokenType[search]} token was found`);
10863
10928
  }
10929
+ /**
10930
+ * Consumes tokens until a matching close-tag is found. Tags are appended to
10931
+ * the document.
10932
+ *
10933
+ * @internal
10934
+ */
10935
+ consumeUntilMatchingTag(source, tokenStream, searchTag) {
10936
+ let numOpen = 1;
10937
+ let it = this.next(tokenStream);
10938
+ while (!it.done) {
10939
+ const token = it.value;
10940
+ this.consume(source, token, tokenStream);
10941
+ if (token.type === exports.TokenType.TAG_OPEN) {
10942
+ const [, close, tagName] = token.data;
10943
+ if (tagName === searchTag) {
10944
+ if (close) {
10945
+ numOpen--;
10946
+ }
10947
+ else {
10948
+ numOpen++;
10949
+ }
10950
+ if (numOpen === 0) {
10951
+ return;
10952
+ }
10953
+ }
10954
+ }
10955
+ it = this.next(tokenStream);
10956
+ }
10957
+ }
10864
10958
  next(tokenStream) {
10865
10959
  const it = tokenStream.next();
10866
10960
  if (!it.done) {
@@ -10868,7 +10962,7 @@ class Parser {
10868
10962
  this.trigger("token", {
10869
10963
  location: token.location,
10870
10964
  type: token.type,
10871
- data: token.data ? Array.from(token.data) : undefined,
10965
+ data: Array.from(token.data),
10872
10966
  });
10873
10967
  }
10874
10968
  return it;
@@ -11128,11 +11222,12 @@ class Engine {
11128
11222
  return lines;
11129
11223
  }
11130
11224
  dumpTokens(source) {
11225
+ var _a;
11131
11226
  const lexer = new Lexer();
11132
11227
  const lines = [];
11133
11228
  for (const src of source) {
11134
11229
  for (const token of lexer.tokenize(src)) {
11135
- const data = token.data ? token.data[0] : null;
11230
+ const data = (_a = token.data[0]) !== null && _a !== void 0 ? _a : "";
11136
11231
  lines.push({
11137
11232
  token: exports.TokenType[token.type],
11138
11233
  data,