tutuca 0.9.38 → 0.9.40

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.
@@ -2468,8 +2468,11 @@ var DEFAULT_SCOPE_BOUNDARIES = new Set([
2468
2468
  "th",
2469
2469
  "marquee",
2470
2470
  "object",
2471
+ "select",
2471
2472
  "template"
2472
2473
  ]);
2474
+ var MATHML_TEXT_INTEGRATION_POINT_NAMES = new Set(["mi", "mo", "mn", "ms", "mtext"]);
2475
+ var SVG_HTML_INTEGRATION_POINT_NAMES = new Set(["foreignobject", "desc", "title"]);
2473
2476
  var SCOPE_LIST_ITEM = new Set([...DEFAULT_SCOPE_BOUNDARIES, "ol", "ul"]);
2474
2477
  var SCOPE_BUTTON = new Set([...DEFAULT_SCOPE_BOUNDARIES, "button"]);
2475
2478
  var SCOPE_DEFAULT = DEFAULT_SCOPE_BOUNDARIES;
@@ -2574,6 +2577,12 @@ var STANDARD_SVG_CAMEL_ATTRS = new Set([
2574
2577
  "zoomAndPan"
2575
2578
  ]);
2576
2579
  var MATHML_CAMEL_ATTRS = new Set(["definitionURL"]);
2580
+ var SVG_ATTR_LOWERCASE_TO_CAMEL = new Map;
2581
+ for (const camel of STANDARD_SVG_CAMEL_ATTRS)
2582
+ SVG_ATTR_LOWERCASE_TO_CAMEL.set(camel.toLowerCase(), camel);
2583
+ var MATHML_ATTR_LOWERCASE_TO_CAMEL = new Map;
2584
+ for (const camel of MATHML_CAMEL_ATTRS)
2585
+ MATHML_ATTR_LOWERCASE_TO_CAMEL.set(camel.toLowerCase(), camel);
2577
2586
  var FOREIGN_BREAKOUT_TAGS = new Set([
2578
2587
  "b",
2579
2588
  "big",
@@ -2668,7 +2677,6 @@ var SELECT_BREAKOUT_TAGS = new Set(["input", "keygen", "textarea", "select"]);
2668
2677
  var MODES = Object.freeze({
2669
2678
  inBody: "inBody",
2670
2679
  inTable: "inTable",
2671
- inTableText: "inTableText",
2672
2680
  inCaption: "inCaption",
2673
2681
  inColumnGroup: "inColumnGroup",
2674
2682
  inTableBody: "inTableBody",
@@ -2676,8 +2684,7 @@ var MODES = Object.freeze({
2676
2684
  inCell: "inCell",
2677
2685
  inSelect: "inSelect",
2678
2686
  inSelectInTable: "inSelectInTable",
2679
- inTemplate: "inTemplate",
2680
- text: "text"
2687
+ inTemplate: "inTemplate"
2681
2688
  });
2682
2689
  var NS = Object.freeze({
2683
2690
  html: "html",
@@ -2705,6 +2712,8 @@ var FRAGMENT_CONTEXT_MODES = Object.freeze({
2705
2712
  // tools/core/htmllinter.js
2706
2713
  var HTML_TAG_NAME_HAS_UPPERCASE = "HTML_TAG_NAME_HAS_UPPERCASE";
2707
2714
  var HTML_SVG_TAG_WILL_LOWERCASE = "HTML_SVG_TAG_WILL_LOWERCASE";
2715
+ var HTML_SVG_ATTR_WILL_LOWERCASE = "HTML_SVG_ATTR_WILL_LOWERCASE";
2716
+ var HTML_MATHML_ATTR_WILL_LOWERCASE = "HTML_MATHML_ATTR_WILL_LOWERCASE";
2708
2717
  var HTML_TAG_NOT_ALLOWED_IN_PARENT = "HTML_TAG_NOT_ALLOWED_IN_PARENT";
2709
2718
  var HTML_TEXT_NOT_ALLOWED_IN_PARENT = "HTML_TEXT_NOT_ALLOWED_IN_PARENT";
2710
2719
  var HTML_VOID_ELEMENT_HAS_CLOSE_TAG = "HTML_VOID_ELEMENT_HAS_CLOSE_TAG";
@@ -2712,6 +2721,13 @@ var HTML_DUPLICATE_FORM = "HTML_DUPLICATE_FORM";
2712
2721
  var HTML_NESTED_INTERACTIVE = "HTML_NESTED_INTERACTIVE";
2713
2722
  var HTML_MISNESTED_FORMATTING = "HTML_MISNESTED_FORMATTING";
2714
2723
  var HTML_UNEXPECTED_END_TAG = "HTML_UNEXPECTED_END_TAG";
2724
+ var HTML_DUPLICATE_ATTRIBUTE = "HTML_DUPLICATE_ATTRIBUTE";
2725
+ var HTML_ATTRIBUTES_ON_END_TAG = "HTML_ATTRIBUTES_ON_END_TAG";
2726
+ var HTML_SELF_CLOSING_END_TAG = "HTML_SELF_CLOSING_END_TAG";
2727
+ var HTML_MISSING_ATTRIBUTE_VALUE = "HTML_MISSING_ATTRIBUTE_VALUE";
2728
+ var HTML_CDATA_IN_HTML_NAMESPACE = "HTML_CDATA_IN_HTML_NAMESPACE";
2729
+ var HTML_BOGUS_COMMENT = "HTML_BOGUS_COMMENT";
2730
+ var HTML_UNCLOSED_BEFORE_END = "HTML_UNCLOSED_BEFORE_END";
2715
2731
  var LEVEL_ERROR = "error";
2716
2732
  var LEVEL_WARN = "warn";
2717
2733
  var TABLE_SCOPE_TAGS = new Set([
@@ -2726,6 +2742,18 @@ var TABLE_SCOPE_TAGS = new Set([
2726
2742
  "table"
2727
2743
  ]);
2728
2744
  var TABLE_BODY_CELL_TAGS = new Set(["td", "th"]);
2745
+ var IMPLIED_END_TAGS = new Set([
2746
+ "dd",
2747
+ "dt",
2748
+ "li",
2749
+ "optgroup",
2750
+ "option",
2751
+ "p",
2752
+ "rb",
2753
+ "rp",
2754
+ "rt",
2755
+ "rtc"
2756
+ ]);
2729
2757
  function lintHtml(html, onFinding, opts = {}) {
2730
2758
  const TokenizerClass = opts.TokenizerClass ?? HtmlTokenizer;
2731
2759
  const ctx = new LinterCtx(html, onFinding, opts);
@@ -2766,30 +2794,75 @@ class LinterCtx {
2766
2794
  this.openElements.push({ name: "select", ns: NS.html, start: -1 });
2767
2795
  }
2768
2796
  this.insertionMode = ctxInfo.mode;
2769
- this.originalInsertionMode = MODES.inBody;
2770
2797
  this.templateInsertionModes = ctxName === "template" ? [MODES.inTemplate] : [];
2771
2798
  this.activeFormatting = [];
2772
- this.formPointer = null;
2773
2799
  this.framesetOk = true;
2774
2800
  this.svgCamelElements = opts.svgCamelElements ?? STANDARD_SVG_CAMEL_ELEMENTS;
2775
- this.transparentTagPrefixes = opts.transparentTagPrefixes ?? [];
2801
+ this.transparentTagPrefixes = (opts.transparentTagPrefixes ?? []).map((p) => p.toLowerCase());
2776
2802
  this.currentTagName = "";
2777
2803
  this.currentTagRawName = "";
2778
2804
  this.currentTagStart = 0;
2805
+ this.currentAttrs = [];
2806
+ this.currentAttr = null;
2779
2807
  this.tokenizer = null;
2780
- this.textRestoreMode = null;
2781
2808
  }
2782
2809
  onopentagname(start, end) {
2783
2810
  const raw = this.html.slice(start, end);
2784
2811
  this.currentTagRawName = raw;
2785
2812
  this.currentTagName = raw.toLowerCase();
2786
2813
  this.currentTagStart = start;
2814
+ this.currentAttrs = [];
2815
+ this.currentAttr = null;
2816
+ }
2817
+ onattribname(start, end) {
2818
+ const rawName = this.html.slice(start, end);
2819
+ this.currentAttr = {
2820
+ name: rawName.toLowerCase(),
2821
+ rawName,
2822
+ nameStart: start,
2823
+ value: null,
2824
+ valueStart: -1,
2825
+ valueEnd: -1,
2826
+ quote: QuoteType.NoValue
2827
+ };
2828
+ }
2829
+ onattribdata(start, end) {
2830
+ if (!this.currentAttr)
2831
+ return;
2832
+ this.currentAttr.valueStart = start;
2833
+ this.currentAttr.valueEnd = end;
2834
+ this.currentAttr.value = this.html.slice(start, end);
2835
+ }
2836
+ onattribend(quote, _end) {
2837
+ const a = this.currentAttr;
2838
+ if (!a)
2839
+ return;
2840
+ a.quote = quote;
2841
+ const dup = this.currentAttrs.find((x) => x.name === a.name);
2842
+ if (dup) {
2843
+ this.report(HTML_DUPLICATE_ATTRIBUTE, LEVEL_WARN, a.nameStart, {
2844
+ name: a.name,
2845
+ firstAt: dup.nameStart
2846
+ });
2847
+ } else {
2848
+ this.currentAttrs.push(a);
2849
+ }
2850
+ if (a.quote === QuoteType.Unquoted && a.value === "") {
2851
+ this.report(HTML_MISSING_ATTRIBUTE_VALUE, LEVEL_WARN, a.nameStart, {
2852
+ name: a.name
2853
+ });
2854
+ }
2855
+ this.currentAttr = null;
2787
2856
  }
2788
- onattribname(_start, _end) {}
2789
- onattribdata(_start, _end) {}
2790
2857
  onattribentity(_cp) {}
2791
- onattribend(_quote, _end) {}
2792
2858
  ontextentity(_cp, _end) {}
2859
+ getAttr(name) {
2860
+ const a = this.currentAttrs.find((x) => x.name === name);
2861
+ return a ? a.value : null;
2862
+ }
2863
+ hasAttr(name) {
2864
+ return this.currentAttrs.some((x) => x.name === name);
2865
+ }
2793
2866
  onopentagend(endIndex) {
2794
2867
  this.handleStartTag(false, endIndex);
2795
2868
  }
@@ -2799,20 +2872,56 @@ class LinterCtx {
2799
2872
  onclosetag(start, end) {
2800
2873
  const raw = this.html.slice(start, end);
2801
2874
  const name = raw.toLowerCase();
2875
+ let i = end;
2876
+ let lastNonWs = -1;
2877
+ while (i < this.html.length) {
2878
+ const c = this.html.charCodeAt(i);
2879
+ if (c === 62)
2880
+ break;
2881
+ if (c !== 32 && c !== 9 && c !== 10 && c !== 13 && c !== 12)
2882
+ lastNonWs = i;
2883
+ i++;
2884
+ }
2885
+ if (lastNonWs >= 0) {
2886
+ if (this.html.charCodeAt(lastNonWs) === 47 && lastNonWs === i - 1) {
2887
+ let firstNonWs = -1;
2888
+ for (let j = end;j < lastNonWs; j++) {
2889
+ const c2 = this.html.charCodeAt(j);
2890
+ if (c2 !== 32 && c2 !== 9 && c2 !== 10 && c2 !== 13 && c2 !== 12) {
2891
+ firstNonWs = j;
2892
+ break;
2893
+ }
2894
+ }
2895
+ if (firstNonWs < 0) {
2896
+ this.report(HTML_SELF_CLOSING_END_TAG, LEVEL_WARN, start, { tag: name });
2897
+ } else {
2898
+ this.report(HTML_ATTRIBUTES_ON_END_TAG, LEVEL_WARN, start, { tag: name });
2899
+ }
2900
+ } else {
2901
+ this.report(HTML_ATTRIBUTES_ON_END_TAG, LEVEL_WARN, start, { tag: name });
2902
+ }
2903
+ }
2802
2904
  this.handleEndTag(name, start);
2803
2905
  }
2804
2906
  ontext(start, end) {
2805
2907
  if (start >= end)
2806
2908
  return;
2909
+ const top = this.currentNode();
2910
+ if (top && top.ns !== NS.html && !this.isIntegrationPoint(top))
2911
+ return;
2807
2912
  this.handleText(start, end);
2808
2913
  }
2809
- oncomment(_start, _end, _endOffset) {}
2914
+ oncomment(start, _end, endOffset) {
2915
+ if (endOffset === 0) {
2916
+ this.report(HTML_BOGUS_COMMENT, LEVEL_WARN, start, {
2917
+ mode: this.insertionMode
2918
+ });
2919
+ }
2920
+ }
2810
2921
  oncdata(start, _end, _endOffset) {
2811
2922
  if (this.currentNamespace() === NS.html) {
2812
- this.report(HTML_TAG_NOT_ALLOWED_IN_PARENT, LEVEL_WARN, start, {
2813
- tag: "[CDATA[",
2814
- mode: this.insertionMode,
2815
- action: "ignored"
2923
+ this.report(HTML_CDATA_IN_HTML_NAMESPACE, LEVEL_WARN, start, {
2924
+ mode: this.insertionMode
2816
2925
  });
2817
2926
  }
2818
2927
  }
@@ -2850,14 +2959,37 @@ class LinterCtx {
2850
2959
  const f = this.openElements[i];
2851
2960
  if (f.name === target && f.ns === NS.html)
2852
2961
  return true;
2853
- if (f.ns === NS.html && scopeSet.has(f.name))
2854
- return false;
2855
- if (f.ns !== NS.html) {
2962
+ if (f.ns === NS.html) {
2963
+ if (scopeSet.has(f.name))
2964
+ return false;
2965
+ } else if (this.isScopeBoundary(f)) {
2856
2966
  return false;
2857
2967
  }
2858
2968
  }
2859
2969
  return false;
2860
2970
  }
2971
+ isScopeBoundary(frame) {
2972
+ if (frame.ns === NS.math) {
2973
+ return MATHML_TEXT_INTEGRATION_POINT_NAMES.has(frame.name) || frame.name === "annotation-xml";
2974
+ }
2975
+ if (frame.ns === NS.svg) {
2976
+ return SVG_HTML_INTEGRATION_POINT_NAMES.has(frame.name);
2977
+ }
2978
+ return false;
2979
+ }
2980
+ isIntegrationPoint(frame) {
2981
+ if (frame.ns === NS.math) {
2982
+ if (MATHML_TEXT_INTEGRATION_POINT_NAMES.has(frame.name))
2983
+ return true;
2984
+ if (frame.name === "annotation-xml" && frame.htmlIntegration)
2985
+ return true;
2986
+ return false;
2987
+ }
2988
+ if (frame.ns === NS.svg) {
2989
+ return SVG_HTML_INTEGRATION_POINT_NAMES.has(frame.name);
2990
+ }
2991
+ return false;
2992
+ }
2861
2993
  hasInDefaultScope(target) {
2862
2994
  return this.hasInScope(target, SCOPE_DEFAULT);
2863
2995
  }
@@ -2928,13 +3060,43 @@ class LinterCtx {
2928
3060
  });
2929
3061
  }
2930
3062
  }
3063
+ const targetNs = ns !== NS.html ? ns : name === "svg" ? NS.svg : name === "math" ? NS.math : NS.html;
3064
+ if (targetNs === NS.svg) {
3065
+ for (const a of this.currentAttrs) {
3066
+ const canonical = SVG_ATTR_LOWERCASE_TO_CAMEL.get(a.name);
3067
+ if (canonical && a.rawName !== canonical) {
3068
+ this.report(HTML_SVG_ATTR_WILL_LOWERCASE, LEVEL_ERROR, a.nameStart, {
3069
+ raw: a.rawName,
3070
+ canonical
3071
+ });
3072
+ }
3073
+ }
3074
+ } else if (targetNs === NS.math) {
3075
+ for (const a of this.currentAttrs) {
3076
+ const canonical = MATHML_ATTR_LOWERCASE_TO_CAMEL.get(a.name);
3077
+ if (canonical && a.rawName !== canonical) {
3078
+ this.report(HTML_MATHML_ATTR_WILL_LOWERCASE, LEVEL_ERROR, a.nameStart, {
3079
+ raw: a.rawName,
3080
+ canonical
3081
+ });
3082
+ }
3083
+ }
3084
+ }
2931
3085
  if (this.isTransparentTag(name))
2932
3086
  return;
2933
- if (ns !== NS.html && !this.shouldBreakoutFromForeign(name)) {
3087
+ const top = this.currentNode();
3088
+ const inForeign = ns !== NS.html && !(top && this.isIntegrationPoint(top));
3089
+ if (inForeign && !this.shouldBreakoutFromForeign(name)) {
2934
3090
  this.startTagInForeign(name, raw, selfClosing, start);
2935
3091
  return;
2936
3092
  }
2937
- if (ns !== NS.html && this.shouldBreakoutFromForeign(name)) {
3093
+ if (inForeign && this.shouldBreakoutFromForeign(name)) {
3094
+ this.report(HTML_TAG_NOT_ALLOWED_IN_PARENT, LEVEL_WARN, start, {
3095
+ tag: raw,
3096
+ parent: this.currentNode()?.name ?? "(root)",
3097
+ mode: this.insertionMode,
3098
+ action: "foreign-breakout"
3099
+ });
2938
3100
  while (this.openElements.length && this.currentNode()?.ns !== NS.html) {
2939
3101
  this.openElements.pop();
2940
3102
  }
@@ -2944,20 +3106,28 @@ class LinterCtx {
2944
3106
  shouldBreakoutFromForeign(name) {
2945
3107
  if (FOREIGN_BREAKOUT_TAGS.has(name))
2946
3108
  return true;
2947
- if (name === "font")
2948
- return true;
3109
+ if (name === "font") {
3110
+ return this.hasAttr("color") || this.hasAttr("face") || this.hasAttr("size");
3111
+ }
2949
3112
  return false;
2950
3113
  }
2951
3114
  startTagInForeign(name, raw, selfClosing, start) {
2952
3115
  const ns = name === "svg" ? NS.svg : name === "math" ? NS.math : this.currentNamespace();
2953
3116
  const top = this.currentNode();
2954
- if (top && top.ns === NS.math && MATHML_TEXT_INTEGRATION_POINTS.has(top.name) && name !== "mglyph" && name !== "malignmark") {
3117
+ if (top && this.isIntegrationPoint(top) && name !== "mglyph" && name !== "malignmark") {
2955
3118
  this.dispatchStartTag(name, raw, selfClosing, start, start + raw.length);
2956
3119
  return;
2957
3120
  }
2958
3121
  if (selfClosing)
2959
3122
  return;
2960
- this.push(raw, ns, start);
3123
+ const frame = { name, ns, start };
3124
+ if (ns === NS.math && name === "annotation-xml") {
3125
+ const enc = (this.getAttr("encoding") ?? "").toLowerCase();
3126
+ if (enc === "text/html" || enc === "application/xhtml+xml") {
3127
+ frame.htmlIntegration = true;
3128
+ }
3129
+ }
3130
+ this.openElements.push(frame);
2961
3131
  }
2962
3132
  dispatchStartTag(name, raw, selfClosing, start, endIndex) {
2963
3133
  switch (this.insertionMode) {
@@ -2967,10 +3137,6 @@ class LinterCtx {
2967
3137
  return this.startInBody(name, raw, selfClosing, start, endIndex);
2968
3138
  case MODES.inTable:
2969
3139
  return this.startInTable(name, raw, selfClosing, start, endIndex);
2970
- case MODES.inTableText:
2971
- this.flushTableText();
2972
- this.insertionMode = this.originalInsertionMode;
2973
- return this.dispatchStartTag(name, raw, selfClosing, start, endIndex);
2974
3140
  case MODES.inCaption:
2975
3141
  return this.startInCaption(name, raw, selfClosing, start, endIndex);
2976
3142
  case MODES.inColumnGroup:
@@ -2985,8 +3151,6 @@ class LinterCtx {
2985
3151
  return this.startInSelect(name, raw, selfClosing, start, endIndex);
2986
3152
  case MODES.inSelectInTable:
2987
3153
  return this.startInSelectInTable(name, raw, selfClosing, start, endIndex);
2988
- case MODES.text:
2989
- return;
2990
3154
  }
2991
3155
  }
2992
3156
  startInBody(name, raw, selfClosing, start, _endIndex) {
@@ -3028,7 +3192,7 @@ class LinterCtx {
3028
3192
  return;
3029
3193
  }
3030
3194
  if (name === "form") {
3031
- if (this.formPointer !== null && !this.openElementsHas("template")) {
3195
+ if (this.openElementsHas("form") && !this.openElementsHas("template")) {
3032
3196
  this.report(HTML_DUPLICATE_FORM, LEVEL_ERROR, start, {
3033
3197
  tag: raw,
3034
3198
  mode: this.insertionMode
@@ -3038,7 +3202,6 @@ class LinterCtx {
3038
3202
  if (this.hasInButtonScope("p"))
3039
3203
  this.implicitlyClose("p", start, raw);
3040
3204
  this.push(raw, NS.html, start);
3041
- this.formPointer = this.currentNode();
3042
3205
  return;
3043
3206
  }
3044
3207
  if (name === "li") {
@@ -3111,7 +3274,7 @@ class LinterCtx {
3111
3274
  return;
3112
3275
  }
3113
3276
  if (name === "a") {
3114
- if (this.openElementsHas("a") || this.activeFormattingHas("a")) {
3277
+ if (this.activeFormattingHas("a")) {
3115
3278
  this.report(HTML_NESTED_INTERACTIVE, LEVEL_WARN, start, {
3116
3279
  tag: raw,
3117
3280
  mode: this.insertionMode
@@ -3128,7 +3291,7 @@ class LinterCtx {
3128
3291
  return;
3129
3292
  }
3130
3293
  if (name === "nobr") {
3131
- if (this.hasInDefaultScope("nobr")) {
3294
+ if (this.activeFormattingHas("nobr")) {
3132
3295
  this.report(HTML_NESTED_INTERACTIVE, LEVEL_WARN, start, {
3133
3296
  tag: raw,
3134
3297
  mode: this.insertionMode
@@ -3212,7 +3375,6 @@ class LinterCtx {
3212
3375
  if (e === null)
3213
3376
  break;
3214
3377
  if (e.name === name) {
3215
- this.activeFormatting.splice(i, 1);
3216
3378
  const idx = this.openElements.indexOf(e);
3217
3379
  if (idx >= 0)
3218
3380
  this.openElements.splice(idx, 1);
@@ -3305,6 +3467,9 @@ class LinterCtx {
3305
3467
  return this.startInBody(name, raw, selfClosing, start, endIndex);
3306
3468
  }
3307
3469
  if (name === "input") {
3470
+ const type = (this.getAttr("type") ?? "").toLowerCase();
3471
+ if (type === "hidden")
3472
+ return;
3308
3473
  this.report(HTML_TAG_NOT_ALLOWED_IN_PARENT, LEVEL_WARN, start, {
3309
3474
  tag: raw,
3310
3475
  parent: "table",
@@ -3340,18 +3505,6 @@ class LinterCtx {
3340
3505
  this.openElements.pop();
3341
3506
  }
3342
3507
  }
3343
- flushTableText() {
3344
- if (!this.pendingTableText)
3345
- return;
3346
- const { hasNonWhitespace, start, snippet } = this.pendingTableText;
3347
- if (hasNonWhitespace) {
3348
- this.report(HTML_TEXT_NOT_ALLOWED_IN_PARENT, LEVEL_ERROR, start, {
3349
- mode: this.originalInsertionMode,
3350
- snippet
3351
- });
3352
- }
3353
- this.pendingTableText = null;
3354
- }
3355
3508
  startInCaption(name, raw, selfClosing, start, endIndex) {
3356
3509
  if (name === "caption" || name === "col" || name === "colgroup" || name === "tbody" || name === "td" || name === "tfoot" || name === "th" || name === "thead" || name === "tr") {
3357
3510
  if (this.hasInTableScope("caption")) {
@@ -3584,14 +3737,24 @@ class LinterCtx {
3584
3737
  return;
3585
3738
  const ns = this.currentNamespace();
3586
3739
  if (ns !== NS.html) {
3587
- for (let i = this.openElements.length - 1;i >= 0; i--) {
3588
- const f = this.openElements[i];
3589
- if (f.ns === NS.html)
3740
+ let stackIdx = this.openElements.length - 1;
3741
+ let first = true;
3742
+ while (stackIdx > 0) {
3743
+ const f = this.openElements[stackIdx];
3744
+ if (!first && f.ns === NS.html)
3590
3745
  break;
3591
- if (f.name.toLowerCase() === name) {
3592
- this.openElements.length = i;
3746
+ if (f.ns !== NS.html && f.name.toLowerCase() === name) {
3747
+ this.openElements.length = stackIdx;
3593
3748
  return;
3594
3749
  }
3750
+ if (first) {
3751
+ this.report(HTML_UNEXPECTED_END_TAG, LEVEL_WARN, start, {
3752
+ tag: name,
3753
+ mode: this.insertionMode
3754
+ });
3755
+ first = false;
3756
+ }
3757
+ stackIdx--;
3595
3758
  }
3596
3759
  }
3597
3760
  if (VOID_ELEMENTS.has(name)) {
@@ -3622,19 +3785,33 @@ class LinterCtx {
3622
3785
  });
3623
3786
  return;
3624
3787
  }
3788
+ const endIsTableStructural = TABLE_SCOPE_TAGS.has(name);
3625
3789
  for (let i = this.openElements.length - 1;i >= 0; i--) {
3626
3790
  const f = this.openElements[i];
3627
3791
  if (f.ns === NS.html && f.name === name) {
3792
+ for (let j = this.openElements.length - 1;j > i; j--) {
3793
+ const popped = this.openElements[j];
3794
+ if (popped.ns === NS.html && popped.name !== name && !IMPLIED_END_TAGS.has(popped.name) && !(endIsTableStructural && TABLE_SCOPE_TAGS.has(popped.name))) {
3795
+ this.report(HTML_UNCLOSED_BEFORE_END, LEVEL_WARN, start, {
3796
+ tag: name,
3797
+ unclosed: popped.name,
3798
+ mode: this.insertionMode
3799
+ });
3800
+ break;
3801
+ }
3802
+ }
3628
3803
  this.openElements.length = i;
3629
- if (name === "form")
3630
- this.formPointer = null;
3631
3804
  if (TABLE_SCOPE_TAGS.has(name) || name === "select" || name === "template") {
3632
3805
  this.resetInsertionModeAppropriately();
3633
3806
  }
3634
3807
  return;
3635
3808
  }
3636
- if (f.ns === NS.html && SPECIAL_ELEMENTS.has(f.name)) {
3637
- break;
3809
+ if (f.ns === NS.html && SPECIAL_ELEMENTS.has(f.name) && !IMPLIED_END_TAGS.has(f.name) && !(endIsTableStructural && TABLE_SCOPE_TAGS.has(f.name))) {
3810
+ this.report(HTML_UNEXPECTED_END_TAG, LEVEL_WARN, start, {
3811
+ tag: name,
3812
+ mode: this.insertionMode
3813
+ });
3814
+ return;
3638
3815
  }
3639
3816
  }
3640
3817
  }
@@ -4294,6 +4471,24 @@ function lintIdToMessage(id, info) {
4294
4471
  return `Misnested formatting tag </${info.tag}> — adoption agency will reorder nodes${fmtLocationSuffix(info)}`;
4295
4472
  case "HTML_UNEXPECTED_END_TAG":
4296
4473
  return `Unexpected end tag </${info.tag}>${fmtLocationSuffix(info)}`;
4474
+ case "HTML_UNCLOSED_BEFORE_END":
4475
+ return `<${info.unclosed}> still open when </${info.tag}> was seen — implicitly closed${fmtLocationSuffix(info)}`;
4476
+ case "HTML_DUPLICATE_ATTRIBUTE":
4477
+ return `Duplicate attribute '${info.name}' — second occurrence dropped${fmtLocationSuffix(info)}`;
4478
+ case "HTML_ATTRIBUTES_ON_END_TAG":
4479
+ return `Attributes on end tag </${info.tag}> — dropped by the parser${fmtLocationSuffix(info)}`;
4480
+ case "HTML_SELF_CLOSING_END_TAG":
4481
+ return `Self-closing end tag </${info.tag}/> — trailing '/' is meaningless${fmtLocationSuffix(info)}`;
4482
+ case "HTML_MISSING_ATTRIBUTE_VALUE":
4483
+ return `Attribute '${info.name}' is missing a value${fmtLocationSuffix(info)}`;
4484
+ case "HTML_CDATA_IN_HTML_NAMESPACE":
4485
+ return `CDATA section in HTML namespace — reinterpreted as a bogus comment${fmtLocationSuffix(info)}`;
4486
+ case "HTML_BOGUS_COMMENT":
4487
+ return `Bogus comment — content dropped by the parser${fmtLocationSuffix(info)}`;
4488
+ case "HTML_SVG_ATTR_WILL_LOWERCASE":
4489
+ return `SVG attribute '${info.raw}' will be rewritten to '${info.canonical}'${fmtLocationSuffix(info)}`;
4490
+ case "HTML_MATHML_ATTR_WILL_LOWERCASE":
4491
+ return `MathML attribute '${info.raw}' will be rewritten to '${info.canonical}'${fmtLocationSuffix(info)}`;
4297
4492
  case "LINT_ERROR":
4298
4493
  return info.message;
4299
4494
  default: