Repository: kepano/defuddle (2026-04-12T01:55:09Z)

Issue:
Empty tables with no text content preserved in output
Tables whose cells contain zero text produce empty markdown tables in the output.

## Example

https://en.wikisource.org/wiki/Popular_Science_Monthly/Volume_77/October_1910/The_Moral_Equivalent_of_War

Output starts with:

```
|  |  |  |  |  |  |
| --- | --- | --- | --- | --- | --- |
```

This comes from a `<table class="pr_quality">` — a proofreading progress bar rendered as colored cells:

```html
<table class="pr_quality" title="1 validated page, 10 only proofread pages...">
  <tr>
    <td class="quality4" style="width: 8.33%"></td>
    <td class="quality3" style="width: 83.33%"></td>
    <td class="quality2" style="width: 0%"></td>
    <td class="quality1" style="width: 0%"></td>
    <td class="quality0" style="width: 8.33%"></td>
    <td class="qualitye" style="width: 0%"></td>
  </tr>
</table>
```

## Root cause

Defuddle preserves tables as content, but tables with zero text across all cells are visual chrome (progress bars, spacers, layout dividers), never data tables.

## Fix direction

Strip tables where no cell contains text content.


Discussion:

Patch:
diff --git a/src/standardize.ts b/src/standardize.ts
index 922876f96..297ab05e8 100644
--- a/src/standardize.ts
+++ b/src/standardize.ts
@@ -1173,6 +1173,18 @@ function standardizeElements(element: Element, doc: Document, subProfile?: Recor
 		}
 	});
 
+	// Remove tables with no text and no media in any cell
+	for (const table of Array.from(element.querySelectorAll('table'))) {
+		if (!table.parentNode) continue;
+		const cells = table.querySelectorAll('td, th');
+		if (cells.length > 0
+			&& Array.from(cells).every(cell => !(cell.textContent || '').trim())
+			&& !table.querySelector('img, picture, video, audio, iframe, svg, math')) {
+			table.remove();
+			processedCount++;
+		}
+	}
+
 	// Unwrap single-column layout tables (used for styling/positioning, not data)
 	const tables = Array.from(element.querySelectorAll('table'));
 	tables.forEach(table => {


Test patch:
diff --git a/tests/expected/elements--empty-table.md b/tests/expected/elements--empty-table.md
new file mode 100644
index 000000000..b8535dc48
--- /dev/null
+++ b/tests/expected/elements--empty-table.md
@@ -0,0 +1,12 @@
+```json
+{
+  "title": "Empty table test",
+  "author": "",
+  "site": "",
+  "published": ""
+}
+```
+
+## Article Title
+
+This is the article content. It has enough words to be considered real content by the scoring algorithm. The quick brown fox jumps over the lazy dog. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
\ No newline at end of file
diff --git a/tests/fixtures/elements--empty-table.html b/tests/fixtures/elements--empty-table.html
new file mode 100644
index 000000000..8a10cdcc3
--- /dev/null
+++ b/tests/fixtures/elements--empty-table.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <title>Empty table test</title>
+</head>
+<body>
+  <article>
+    <table class="progress-bar">
+      <tr>
+        <td style="width: 10%; background: green;"></td>
+        <td style="width: 80%; background: yellow;"></td>
+        <td style="width: 10%; background: red;"></td>
+      </tr>
+    </table>
+    <h1>Article Title</h1>
+    <p>This is the article content. It has enough words to be considered real content by the scoring algorithm. The quick brown fox jumps over the lazy dog. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p>
+  </article>
+</body>
+</html>