Skip to content

Commit

Permalink
non void elements must have an explicit closing tag
Browse files Browse the repository at this point in the history
  • Loading branch information
AndrewJakubowicz committed Jan 8, 2024
1 parent dfbbfae commit e0bda3c
Show file tree
Hide file tree
Showing 4 changed files with 47 additions and 56 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,6 @@ import { getSourceLocation, IP5TagNode, P5Node } from "../parse-html-p5/parse-ht
import { parseHtmlNodeAttrs } from "./parse-html-attribute.js";
import { ParseHtmlContext } from "./parse-html-context.js";

// List taken from https://html.spec.whatwg.org/multipage/syntax.html#void-elements
const VOID_ELEMENTS = new Set(["area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "source", "track", "wbr"]);

/**
* Parses multiple p5Nodes into multiple html nodes.
* @param p5Nodes
Expand Down Expand Up @@ -52,7 +49,6 @@ export function parseHtmlNode(p5Node: IP5TagNode, parent: HtmlNode | undefined,

const htmlNodeBase: IHtmlNodeBase = {
tagName: p5Node.tagName.toLowerCase(),
selfClosed: isSelfClosed(p5Node, context),
attributes: [],
location: makeHtmlNodeLocation(p5Node, context),
children: [],
Expand All @@ -72,29 +68,6 @@ export function parseHtmlNode(p5Node: IP5TagNode, parent: HtmlNode | undefined,
return htmlNode;
}

/**
* Returns if this node was explicitly self-closed or void.
*
* Note: Self-closing tags do not actually exist in HTML
* https://developer.mozilla.org/en-US/docs/Glossary/Void_element#self-closing_tags
*
* Therefore this function returns `true` if `p5Node` is
* a void element, or was explicitly self-closed using XML/JSX
* syntax. If the node is implicitly closed, for example a
* `p` element followed by a `div`, then `false` is returned.
*
* @param p5Node
* @param context
*/
function isSelfClosed(p5Node: IP5TagNode, context: ParseHtmlContext) {
if (VOID_ELEMENTS.has(p5Node.tagName.toLowerCase())) {
return true;
}
const loc = getSourceLocation(p5Node)!;
const isSelfClosed = context.html[loc.startTag.endOffset - 2] === "/";
return isSelfClosed;
}

/**
* Creates source code location from a p5Node.
* @param p5Node
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,6 @@ export interface IHtmlNodeBase {
attributes: HtmlNodeAttr[];
parent?: HtmlNode;
children: HtmlNode[];
/**
* Is true when an HTML node is either a void element, or was
* explicitly closed with self-closing XML syntax.
*/
selfClosed: boolean;
document: HtmlDocument;
}

Expand Down
47 changes: 33 additions & 14 deletions packages/lit-analyzer/src/lib/rules/no-unclosed-tag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,29 @@ import { RuleModule } from "../analyze/types/rule/rule-module.js";
import { isCustomElementTagName } from "../analyze/util/is-valid-name.js";
import { rangeFromHtmlNode } from "../analyze/util/range-util.js";

// List taken from https://html.spec.whatwg.org/multipage/syntax.html#void-elements
// and parse5 list of void elements: https://github.com/inikulin/parse5/blob/86f09edd5a6840ab2269680b0eef2945e78c38fd/packages/parse5/lib/serializer/index.ts#L7-L26
const VOID_ELEMENTS = new Set([
"area",
"base",
"basefont",
"bgsound",
"br",
"col",
"embed",
"frame",
"hr",
"img",
"input",
"keygen",
"link",
"meta",
"param",
"source",
"track",
"wbr"
]);

/**
* This rule validates that all tags are closed properly.
*/
Expand All @@ -11,22 +34,18 @@ const rule: RuleModule = {
priority: "low"
},
visitHtmlNode(htmlNode, context) {
const {
selfClosed,
location: { endTag }
} = htmlNode;
if (!selfClosed && endTag == null) {
// Report specifically that a custom element cannot be self closing
// if the user is trying to close a custom element.
const isCustomElement = isCustomElementTagName(htmlNode.tagName);

context.report({
location: rangeFromHtmlNode(htmlNode),
message: `This tag isn't closed.${isCustomElement ? " Custom elements cannot be self closing." : ""}`
});
if (VOID_ELEMENTS.has(htmlNode.tagName.toLowerCase()) || htmlNode.location.endTag != null) {
return;
}

return;
// Report specifically that a custom element cannot be self closing
// if the user is trying to close a custom element.
const isCustomElement = isCustomElementTagName(htmlNode.tagName);

context.report({
location: rangeFromHtmlNode(htmlNode),
message: `This tag isn't closed.${isCustomElement ? " Custom elements cannot be self closing." : ""}`
});
}
};

Expand Down
24 changes: 14 additions & 10 deletions packages/lit-analyzer/src/test/rules/no-unclosed-tag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,20 @@ tsTest("Report unclosed tags", t => {
hasDiagnostic(t, diagnostics, "no-unclosed-tag");
});

tsTest("Don't report self closed tags", t => {
const { diagnostics } = getDiagnostics("html`<img />`", { rules: { "no-unclosed-tag": true } });
tsTest("Don't report void elements", t => {
const { diagnostics } = getDiagnostics("html`<img>`", { rules: { "no-unclosed-tag": true } });
hasNoDiagnostics(t, diagnostics);
});

tsTest("Don't report void tags", t => {
const { diagnostics } = getDiagnostics("html`<img>`", { rules: { "no-unclosed-tag": true } });
tsTest("Don't report void elements with self closing syntax", t => {
const { diagnostics } = getDiagnostics("html`<img />`", { rules: { "no-unclosed-tag": true } });
hasNoDiagnostics(t, diagnostics);
});

// The `<p>` tag will be closed automatically if immediately followed by a lot of other elements,
// including `<div>`.
// Ref: https://html.spec.whatwg.org/multipage/grouping-content.html#the-p-element
tsTest("Report unclosed 'p' tag that is implicitly closed via tag omission", t => {
tsTest("Report unclosed 'p' tag that was implicitly closed via tag omission", t => {
const { diagnostics } = getDiagnostics("html`<p><div></div></p>`", { rules: { "no-unclosed-tag": true } });
hasDiagnostic(t, diagnostics, "no-unclosed-tag");
});
Expand All @@ -30,14 +30,18 @@ tsTest("Report unclosed 'p' tag that is implicitly closed via tag omission conta
hasDiagnostic(t, diagnostics, "no-unclosed-tag");
});

// Self-closing tags do not exist in HTML, but we can use them to check
// that the user intended that the tag be closed.
tsTest("Don't report self closing 'p' tag", t => {
// Self-closing tags do not exist in HTML. They are only valid in SVG and MathML.
tsTest("Report non-void element using self closing syntax", t => {
const { diagnostics } = getDiagnostics("html`<p /><div></div>`", { rules: { "no-unclosed-tag": true } });
hasNoDiagnostics(t, diagnostics);
hasDiagnostic(t, diagnostics, "no-unclosed-tag");
});

tsTest("Don't report self closing 'p' tag containing text content", t => {
tsTest("Report self closing 'p' tag containing text content", t => {
const { diagnostics } = getDiagnostics("html`<p />Unclosed Content<div></div>`", { rules: { "no-unclosed-tag": true } });
hasDiagnostic(t, diagnostics, "no-unclosed-tag");
});

tsTest("Don't report explicit closing 'p' tag containing text content", t => {
const { diagnostics } = getDiagnostics("html`<p>Unclosed Content</p><div></div>`", { rules: { "no-unclosed-tag": true } });
hasNoDiagnostics(t, diagnostics);
});

0 comments on commit e0bda3c

Please sign in to comment.