Skip to content

Commit 778e4c7

Browse files
alicodingclaude
andauthored
fix: Confluence markdown table fidelity + fixture corpus (goal 0021 Phase 4) (#78)
ToMarkdown wired only base+commonmark plugins via the library's bare ConvertString, silently dropping plugin/table so every Confluence table collapsed to a run-on line. Rewired through converter.NewConverter with an explicit plugin list (base, commonmark, table, strikethrough), signature unchanged. Added a 12-case realistic Confluence Cloud fixture corpus with hand-reviewed goldens, a table-driven test naming each case's pinned property (structural fix vs. currently-degrading), and the per-case assessment + follow-up candidates in the goal file. Claude-Session: https://claude.ai/code/session_01FJ8wStsHyu7XPLTspNjMnQ Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 7fe2df7 commit 778e4c7

27 files changed

Lines changed: 308 additions & 3 deletions

docs/goals/0021-mcp-dogfood-gap-closure.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,47 @@ not tool-surface ergonomics)
228228
- Full report with sources + verified converter outputs in the
229229
session transcript; per-case snippets land with the fixture
230230
corpus itself.
231+
232+
**Delivered 2026-08-13:** the table-collapse defect fixed
233+
(`internal/adapters/markdown/markdown.go` now builds the converter
234+
explicitly with `plugin/table` + `plugin/strikethrough` via
235+
`converter.NewConverter`, instead of the bare `ConvertString` that
236+
silently omitted `plugin/table`); the 12-case fixture corpus
237+
committed at `internal/adapters/markdown/testdata/confluence/`
238+
(one `.html` fixture + one hand-reviewed `.golden.md` per case),
239+
proven by a table-driven test
240+
(`TestToMarkdown_ConfluenceFixtures`,
241+
`internal/adapters/markdown/markdown_confluence_test.go`) that names
242+
each case's pinned property. Per-case assessment against the
243+
post-fix converter:
244+
245+
| Case | Verdict | Pinned property |
246+
|---|---|---|
247+
| Table w/ colspan+rowspan | structural-loss-fixed | Real pipe table; spanned cells land top-left with blanks elsewhere (GFM ceiling) |
248+
| Code-block macro (`data-syntaxhighlighter-params`) | degrades-acceptably | Code content survives in a fenced block; language hint dropped (no info-string) |
249+
| Info/warning panels | degrades-acceptably | Body text survives; info-vs-warning panel type lost (both become plain paragraphs) |
250+
| 3-level nested lists | survives | Nesting depth and item text survive as indented list items |
251+
| `ak-task-list` task list | degrades-acceptably | Item text survives as a plain bullet list; DONE/TODO state dropped (no GFM checkbox) |
252+
| expand-container macro | degrades-acceptably | Control and content text both survive; expand/collapse semantics lost (flattened) |
253+
| Status lozenge | degrades-acceptably | Label text survives; semantic color/status type lost |
254+
| columnLayout two-equal | degrades-acceptably | Both column bodies survive as sequential paragraphs (acceptable linearization) |
255+
| Confluence page link | survives | Real markdown link with href and text preserved |
256+
| Emoticon (`data-emoji-fallback`) | degrades-acceptably (worst case) | Becomes a dead markdown image link — the fallback character is unused and the relative src doesn't resolve |
257+
| Panel inside a table cell | structural-loss-fixed | Enclosing table survives as a one-cell pipe table; panel type inside the cell lost, same as standalone |
258+
| Bare `<pre>` (negative control) | survives | Plain preformatted text with no macro wrapper survives unchanged |
259+
260+
Follow-up candidates named here, **not implemented this pass**
261+
each needs its own scoping decision before landing:
262+
- `syntaxhighlighter-brush`→language-hint rule (feed the fenced
263+
code block's info-string from `data-syntaxhighlighter-params`).
264+
- Task-list checkbox rule (`ak-task-list`/`data-task-state`
265+
GFM `- [x]`/`- [ ]`).
266+
- Emoji `data-emoji-fallback` rule (emit the fallback character
267+
instead of a dead image link).
268+
- Panel-type labeling (info/warning/note/tip distinguished in the
269+
markdown output, not collapsed to identical plain paragraphs).
270+
- Expand→details (Spenhouet's own precedent: re-render as HTML
271+
`<details>`/`<summary>` rather than flattening).
231272
- **§2.1 M365 bridge dry run** — compose capture→code-exec→clipboard
232273
end-to-end with the pieces that exist; name what's still missing
233274
(DOM capture, auto-paste target).

internal/adapters/markdown/markdown.go

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,28 @@
33
// touches call sites.
44
package markdown
55

6-
import htmltomarkdown "github.com/JohannesKaufmann/html-to-markdown/v2"
6+
import (
7+
"github.com/JohannesKaufmann/html-to-markdown/v2/converter"
8+
"github.com/JohannesKaufmann/html-to-markdown/v2/plugin/base"
9+
"github.com/JohannesKaufmann/html-to-markdown/v2/plugin/commonmark"
10+
"github.com/JohannesKaufmann/html-to-markdown/v2/plugin/strikethrough"
11+
"github.com/JohannesKaufmann/html-to-markdown/v2/plugin/table"
12+
)
713

814
// ToMarkdown converts HTML to Markdown, preserving structure (headings,
9-
// bold, lists) instead of flattening it to plain text.
15+
// bold, lists, tables, strikethrough) instead of flattening it to plain
16+
// text. Built via converter.NewConverter with an explicit plugin list
17+
// rather than the library's package-level ConvertString, which wires only
18+
// base+commonmark and silently collapses every table to a single run-on
19+
// line (no plugin/table).
1020
func ToMarkdown(html string) (string, error) {
11-
return htmltomarkdown.ConvertString(html)
21+
conv := converter.NewConverter(
22+
converter.WithPlugins(
23+
base.NewBasePlugin(),
24+
commonmark.NewCommonmarkPlugin(),
25+
table.NewTablePlugin(),
26+
strikethrough.NewStrikethroughPlugin(),
27+
),
28+
)
29+
return conv.ConvertString(html)
1230
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
package markdown
2+
3+
import (
4+
"os"
5+
"path/filepath"
6+
"strings"
7+
"testing"
8+
)
9+
10+
// TestToMarkdown_ConfluenceFixtures pins ToMarkdown's behavior against a
11+
// realistic Confluence Cloud markup corpus (goal 0021 Phase 4). Each case
12+
// name states the property the golden pins — for a degrading case, that
13+
// property is the CURRENT loss, deliberately preserved so a future fix is
14+
// a visible test change, not a silent regression.
15+
func TestToMarkdown_ConfluenceFixtures(t *testing.T) {
16+
tests := []struct {
17+
name string
18+
fixture string
19+
property string
20+
}{
21+
{
22+
name: "table with colspan and rowspan",
23+
fixture: "table-colspan-rowspan",
24+
property: "table structure survives as a real pipe table; spanned cells land top-left with blanks elsewhere",
25+
},
26+
{
27+
name: "code block macro",
28+
fixture: "code-block-macro",
29+
property: "code content survives in a fenced block; the syntaxhighlighter language hint is currently dropped (no info-string)",
30+
},
31+
{
32+
name: "info and warning panels",
33+
fixture: "info-warning-panels",
34+
property: "panel body text survives; the info-vs-warning panel type is currently lost (both become plain paragraphs)",
35+
},
36+
{
37+
name: "three-level nested lists",
38+
fixture: "nested-lists-3-level",
39+
property: "nesting depth and item text survive as indented list items",
40+
},
41+
{
42+
name: "ak-task-list task list",
43+
fixture: "ak-task-list",
44+
property: "task item text survives as a plain bullet list; DONE/TODO checkbox state is currently dropped (no GFM checkbox syntax)",
45+
},
46+
{
47+
name: "expand-container macro",
48+
fixture: "expand-container",
49+
property: "control and content text both survive; the expand/collapse semantics are currently lost (flattened to sequential paragraphs)",
50+
},
51+
{
52+
name: "status lozenge",
53+
fixture: "status-lozenge",
54+
property: "lozenge label text survives; the semantic color/status type is currently lost (plain text, no styling marker)",
55+
},
56+
{
57+
name: "columnLayout two-equal",
58+
fixture: "column-layout-two-equal",
59+
property: "both column bodies survive as sequential paragraphs (acceptable linearization of a side-by-side layout)",
60+
},
61+
{
62+
name: "confluence page link",
63+
fixture: "page-link",
64+
property: "the link survives as a real markdown link with its href and text preserved",
65+
},
66+
{
67+
name: "emoticon with data-emoji-fallback",
68+
fixture: "emoticon-emoji-fallback",
69+
property: "the emoticon currently becomes a dead markdown image link (the data-emoji-fallback character is not used, and the relative image src does not resolve)",
70+
},
71+
{
72+
name: "panel inside a table cell",
73+
fixture: "panel-inside-table-cell",
74+
property: "the enclosing table structure survives as a one-cell pipe table; the panel type inside the cell is currently lost, same as a standalone panel",
75+
},
76+
{
77+
name: "bare pre negative control",
78+
fixture: "bare-pre-negative-control",
79+
property: "plain preformatted text with no macro wrapper survives unchanged in a fenced block",
80+
},
81+
}
82+
83+
for _, tt := range tests {
84+
t.Run(tt.name, func(t *testing.T) {
85+
htmlBytes, err := os.ReadFile(filepath.Join("testdata", "confluence", tt.fixture+".html"))
86+
if err != nil {
87+
t.Fatalf("reading fixture: %v", err)
88+
}
89+
goldenBytes, err := os.ReadFile(filepath.Join("testdata", "confluence", tt.fixture+".golden.md"))
90+
if err != nil {
91+
t.Fatalf("reading golden: %v", err)
92+
}
93+
want := strings.TrimSuffix(string(goldenBytes), "\n")
94+
95+
got, err := ToMarkdown(string(htmlBytes))
96+
if err != nil {
97+
t.Fatalf("ToMarkdown returned error: %v", err)
98+
}
99+
if got != want {
100+
t.Errorf("ToMarkdown(%s) pinning %q\ngot:\n%s\nwant:\n%s", tt.fixture, tt.property, got, want)
101+
}
102+
})
103+
}
104+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
- Ship the fix
2+
- Write the follow-up
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<ul class="ak-task-list">
2+
<li class="ak-task-item" data-task-state="DONE">
3+
<span class="ak-task-item-icon"></span>
4+
<span>Ship the fix</span>
5+
</li>
6+
<li class="ak-task-item" data-task-state="TODO">
7+
<span class="ak-task-item-icon"></span>
8+
<span>Write the follow-up</span>
9+
</li>
10+
</ul>
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
```
2+
plain preformatted text
3+
no macro wrapper at all
4+
```
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
<pre>plain preformatted text
2+
no macro wrapper at all</pre>
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
```
2+
public class Foo {
3+
public static void main(String[] args) {
4+
System.out.println("hi");
5+
}
6+
}
7+
```
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<div class="code panel pdl">
2+
<div class="codeContent panelContent pdl">
3+
<pre class="syntaxhighlighter-pre" data-syntaxhighlighter-params="brush: java; gutter: false; theme: Confluence" data-theme="Confluence">public class Foo {
4+
public static void main(String[] args) {
5+
System.out.println("hi");
6+
}
7+
}</pre>
8+
</div>
9+
</div>
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Left column content.
2+
3+
Right column content.

0 commit comments

Comments
 (0)