swc-project/swc · warning · swc_html_parser::error::Error

UnclosedElementsCell

UnclosedElementsCell

Error message

A table cell was implicitly closed, but there were open elements

What it means

`close_the_cell` runs when a table cell must be closed (a new td/th or row starts). After generating implied end tags, the current node should be the td/th itself; if formatting/inline elements are still open on top of it, `UnclosedElementsCell` is reported (crates/swc_html_parser/src/parser/mod.rs:7765, using the offending node's start span). The parser then pops until a td/th is popped, implicitly closing those elements.

Source

Thrown at crates/swc_html_parser/src/parser/mod.rs:7765

        // 3. Pop elements from the stack of open elements until a p element has been
        // popped from the stack.
        let popped = self.open_elements_stack.pop_until_tag_name_popped(&["p"]);

        if is_close_p {
            self.update_end_tag_span(popped.as_ref(), token_and_info.span)
        }
    }

    fn close_the_cell(&mut self) {
        // Generate implied end tags.
        self.open_elements_stack.generate_implied_end_tags();

        // If the current node is not now a td element or a th element, then this is a
        // parse error.
        match self.open_elements_stack.items.last() {
            Some(node) if !is_html_element!(node, "td" | "th") => {
                self.errors.push(Error::new(
                    *node.start_span.borrow(),
                    ErrorKind::UnclosedElementsCell,
                ));
            }
            _ => {}
        }

        // Pop elements from the stack of open elements stack until a td
        // element or a th element has been popped from the stack.
        self.open_elements_stack
            .pop_until_tag_name_popped(&["td", "th"]);

        // Clear the list of active formatting elements up to the last marker.
        self.active_formatting_elements.clear_to_last_marker();

        // Switch the insertion mode to "in row".
        self.insertion_mode = InsertionMode::InRow;

View on GitHub (pinned to 5176682b65)

Solutions

  1. Close inline/formatting tags before the next <td>/<th> or end of row
  2. Fix the table exporter that leaves formatting open between cells
  3. Re-serialize through a tolerant parser to normalize cells before downstream processing
  4. Note the error span points at the still-open node's start tag, which helps locate the unclosed element

Example fix

<!-- before -->
<table><tr><td><b>x<td>y</td></tr></table>
<!-- after -->
<table><tr><td><b>x</b></td><td>y</td></tr></table>
Defensive patterns

Strategy: validation

Validate before calling

// Ensure inline tags are closed before the next cell starts
function assertCellsSelfContained(rowHtml: string): void {
  for (const m of rowHtml.matchAll(/<t[dh]\b[^>]*>([\s\S]*?)(?=<t[dh]\b|<\/tr>)/gi)) {
    const inner = m[1];
    const opens = (inner.match(/<(b|i|em|strong|span|a|u|s|font)\b/gi) || []).length;
    const closes = (inner.match(/<\/(b|i|em|strong|span|a|u|s|font)>/gi) || []).length;
    if (opens !== closes) throw new Error('inline tag not closed at end of cell');
  }
}

Try / catch

for err in parser.take_errors() {
    if matches!(err.kind, ErrorKind::UnclosedElementsCell) {
        // error span = start tag of the still-open node; use it to locate the culprit
    }
}

Prevention

When it happens

Trigger: Parsing `<table><tr><td><b>x<td>y</td></tr></table>` — the second `<td>` triggers close_the_cell; implied end tags do not pop `<b>` (formatting elements are not in the implied set), the current node is `b`, so the error fires and b is force-closed with the cell.

Common situations: Spreadsheet-style exports where cell content keeps formatting tags open across cells; CMS table editors that inject `<b>`/`<span>` per cell but close them later; table fragments concatenated without closing inline tags.

Related errors


AI-assisted analysis of swc-project/swc@5176682b65 (2026-08-17). Data as JSON: /api/errors/7290050a1ced5c2c. Report an issue: GitHub.