Skip to content

Read tables back as Markdown instead of smearing their cells - #650

Open
jeremy wants to merge 1 commit into
composer-reset-keeps-modefrom
html-to-markdown-tables
Open

Read tables back as Markdown instead of smearing their cells#650
jeremy wants to merge 1 commit into
composer-reset-keeps-modefrom
html-to-markdown-tables

Conversation

@jeremy

@jeremy jeremy commented Aug 22, 2026

Copy link
Copy Markdown
Member

Stacked on #651 — the composer Reset fix is a prerequisite: without it, the todos description composer drops to single-line mode on Reset() and flattens the freshly-converted pipe table on SetValue(). This PR's base is composer-reset-keeps-mode; merge #651 first.

Follow-up to #637/#648. HTMLToMarkdown had no table handling: <table> markup survived every conversion pass untouched until the final tag-stripping regex deleted the tags and ran the cell text together. Any table-bearing message, comment, or to-do description displayed as one smeared line, and the TUI in-place editors refused table-bearing content wholesale to avoid destroying it on resubmit.

What changed

Converter. A new pass converts each <table> block to a GFM pipe table while row/cell tags are still intact. BC3 rich text is sanitized editor output — always flat editor-authored grids, no nesting, no layout tables — so a non-greedy regex extractor is consistent and sufficient; no DOM walker, no new dependency. Emission shape:

  • First row is the header whether its cells are <th> or <td> (GFM has no headerless tables); the widest row sizes the table and narrower rows are padded — no row is ever truncated, so no cell data is dropped.
  • align attributes — exactly what MarkdownToHTML emits for GFM column alignment via TableCellAlignAttribute — map back to :--- / :---: / ---:.
  • Cell content gets the same inline conversions as body text (bold/italic/code/links/strikethrough, bc-attachment mentions → **@Name**); block boundaries collapse to spaces.
  • Escaping is GFM-exact: pipes escape as \| and literal backslashes double (GFM processes escapes left to right, so a lone \ before an escaped pipe would swallow its backslash and turn the pipe back into a delimiter). Code spans pass through as placeholders — backslashes are literal inside code, so only their pipes are escaped.
  • Entities decode before escaping: an encoded pipe (&#124;) is still a pipe — goldmark decodes it on the next render — so cell text is fully entity-decoded once tags are gone, then escaped. The emitted tables are parked behind placeholders until HTMLToMarkdown's document-level unescape pass has run, so nothing double-decodes.
  • Pipe tables round-trip byte-identical through MarkdownToHTMLHTMLToMarkdown (asserted in TestEditLoopRoundTrip, alignment and the Restore the lost blank line before Markdown tables #648 separator included), and a goldmark-backed test (TestHTMLToMarkdownTableGoldmarkRoundTrip) verifies the parsed cell content survives the full HTML → Markdown → HTML cycle: backslash-before-pipe text, code spans containing pipes and backslashes, pipes in link destinations, adjacent tables.
  • Complex tables still display, best-effort: colspan/rowspan cells emit as ordinary cells — a merged grid displays better flattened than smeared.

Guards. The blanket HasTableHTML gate on the three TUI in-place editors is replaced by HasComplexTableHTML, which fails closed on every shape the pipe-table round trip can't preserve — checking every table in the content, not just the first:

  • merged cells (colspan/rowspan, matched case- and whitespace-insensitively)
  • captions
  • nested tables, and tables nested inside a blockquote or list item (reachable from CLI-posted > | a | b | markdown)
  • attachments, images, or block elements (ul/ol/pre/blockquote/headings) inside the table
  • cells spanning multiple paragraphs or containing <br> (the converter flattens those to one line for display)
  • unclosed tables, and tables the converter can't extract a grid from (rowless/cellless structures that would otherwise vanish with their content)

Mentions are the one rich element cells may keep: they convert to **@Name** exactly as they already do in body text, so editing them loses no more than any body-text edit does. Simple grids now open for editing like any other content, and the guard message names the real blocker ("too complex to edit as Markdown").

The mention-conversion closure is extracted to a named mentionMarkdown function so cell conversion reuses it — behavior unchanged. The skill's "Table boundary" paragraph is updated to match the new behavior.

Testing

  • TestHTMLToMarkdownTable: header promotion, alignment, escaping (pipe, backslash-before-pipe, code spans), inline formatting and mentions in cells, multi-paragraph/<br> cells (display), ragged rows padded and wide rows widening the table, <p><br></p> separators, best-effort colspan display, empty table.
  • TestHTMLToMarkdownTableGoldmarkRoundTrip: parsed-content verification through goldmark, not just Markdown bytes — including entity-encoded pipes and backslashes in text and in code spans.
  • TestEditLoopRoundTrip: five new byte-identical cases (plain, aligned, paragraph-then-table, escaped-pipe-after-backslash, code-span-with-pipe).
  • TestHasComplexTableHTML replaces TestHasTableHTML (the blanket predicate is folded into the new one) with cases covering both halves of every rule.
  • TUI guard tests prove both halves at all three sites: complex tables blocked, simple tables enter edit mode — the todos test asserts the exact multiline pipe table in the composer, which only holds with Keep rich composers rich across Reset #651 underneath.
  • Full bin/ci green.

Copilot AI balanced review requested due to automatic review settings August 22, 2026 08:27
@github-actions github-actions Bot added tui Terminal UI tests Tests (unit and e2e) skills Agent skills labels Aug 22, 2026

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Converts Basecamp HTML tables back to GFM Markdown and permits safe table editing in the TUI.

Changes:

  • Adds HTML-table conversion with formatting, alignment, and round-trip tests.
  • Replaces blanket table guards with complex-table detection.
  • Updates TUI tests and skill guidance.

Tip

If you aren't ready for review, convert to a draft PR.
Click "Convert to draft" or run gh pr ready --undo.
Click "Ready for review" or run gh pr ready to reengage.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
skills/basecamp/SKILL.md Documents table conversion and editing boundaries.
internal/tui/workspace/views/todos.go Allows simple table description editing.
internal/tui/workspace/views/todos_test.go Tests to-do table guards.
internal/tui/workspace/views/detail.go Updates message and comment table guards.
internal/tui/workspace/views/detail_test.go Tests detail-view table editing.
internal/richtext/richtext.go Implements conversion and complexity detection.
internal/richtext/richtext_test.go Adds conversion and round-trip coverage.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +713 to +715
html = reTableBlock.ReplaceAllStringFunc(html, func(s string) string {
return convertTableHTML(s) + "\n\n"
})
Comment thread internal/richtext/richtext.go Outdated
// Markdown edit loop would flatten them.
var (
reTableMergedCell = regexp.MustCompile(`(?i)\b(?:colspan|rowspan)\s*=`)
reTableBlockInCell = regexp.MustCompile(`(?i)<(?:ul|ol|pre|blockquote|h[1-6])[\s/>]`)
Comment thread internal/richtext/richtext.go Outdated
if closeTag != nil {
block = rest[:closeTag[0]]
}
if reTableHTML.MatchString(block) || reTableMergedCell.MatchString(block) || reTableBlockInCell.MatchString(block) {
Comment thread internal/richtext/richtext.go Outdated
// inside cells. Both are shapes a GFM pipe table cannot represent, so a
// Markdown edit loop would flatten them.
var (
reTableMergedCell = regexp.MustCompile(`(?i)\b(?:colspan|rowspan)\s*=`)

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5a4f14d597

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

// Fail closed on complex tables (merged cells, nested tables, blocks in
// cells): HTMLToMarkdown flattens those shapes, so an edit-and-resubmit
// would lose structure. Simple grids round-trip and stay editable.
if richtext.HasComplexTableHTML(description) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep table descriptions multiline before enabling edits

When a simple table is edited as a to-do description, this newly relaxed guard lets execution reach descComposer.Reset(), which switches the composer to single-line quick mode; the subsequent SetValue therefore flattens the generated pipe-table newlines. Saving without making any changes sends a single line such as | Foo | | --- | | Baz |, which MarkdownToHTML no longer recognizes as a table and silently replaces the stored table with a paragraph. Restore rich mode before populating the composer or continue blocking tables on this edit path.

Useful? React with 👍 / 👎.

Comment thread internal/richtext/richtext.go Outdated
// Markdown edit loop would flatten them.
var (
reTableMergedCell = regexp.MustCompile(`(?i)\b(?:colspan|rowspan)\s*=`)
reTableBlockInCell = regexp.MustCompile(`(?i)<(?:ul|ol|pre|blockquote|h[1-6])[\s/>]`)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Classify paragraph-bearing cells as complex

For a table cell containing <p> or <br> content, this predicate returns false even though cellMarkdown collapses those boundaries to spaces. The message, comment, and to-do editors consequently allow the table to be saved, and even an unchanged submission irreversibly rewrites <p>one</p><p>two</p> or one<br>two as one two. Include these lossy cell shapes in the complexity check, or preserve their boundaries in the emitted Markdown.

Useful? React with 👍 / 👎.

Comment thread internal/richtext/richtext.go Outdated
Comment on lines +875 to +876
if len(row) > width {
row = row[:width]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Block edits before truncating overlong table rows

When a table has a later row with more cells than its first row, this conversion silently discards every extra cell, while HasComplexTableHTML still classifies the table as editable. Opening and saving such a message, comment, or to-do therefore permanently deletes cell contents even if the user makes no changes. Treat inconsistent row widths as complex for edit guarding, or expand the emitted header width instead of truncating data.

Useful? React with 👍 / 👎.

Copilot AI review requested due to automatic review settings August 22, 2026 08:39
@jeremy
jeremy force-pushed the html-to-markdown-tables branch from 5a4f14d to fc1e379 Compare August 22, 2026 08:39
@jeremy
jeremy force-pushed the html-to-markdown-tables branch from fc1e379 to f5514c5 Compare August 22, 2026 08:40

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.

Suppressed comments (2)

internal/richtext/richtext.go:932

  • Using a single backtick delimiter corrupts valid code-span cells that contain a literal backtick. For example, HTML containing <code>abis emitted as ``ab ``, which no longer parses as the original code span; because HasComplexTableHTML permits inline code, an unchanged edit can lose formatting. Choose a delimiter longer than the longest backtick run in the content and apply CommonMark code-span padding rules.
	for i, code := range codes {
		code = reWhitespaceRun.ReplaceAllString(strings.TrimSpace(code), " ")
		code = strings.ReplaceAll(code, "|", `\|`)
		s = strings.Replace(s, "\x00"+strconv.Itoa(i)+"\x00", "`"+code+"`", 1)

internal/richtext/richtext.go:1664

  • This pattern scans the entire table block, so ordinary cell text such as set colspan=2 is misclassified as a merged cell and the otherwise-simple table is refused by every editor. Restrict the match to th/td start-tag attributes (or apply an attribute-only pattern to cell[1]) rather than matching text content.
	reTableMergedCell   = regexp.MustCompile(`(?i)\b(?:colspan|rowspan)\s*=`)

// represent (see richtext.HasComplexTableHTML): HTMLToMarkdown flattens
// them, so an edit-and-resubmit would lose structure. Simple grids
// round-trip and stay editable.
if richtext.HasComplexTableHTML(description) {
Comment thread internal/richtext/richtext.go Outdated
Comment on lines +821 to +824
// column alignment — mapped back to :--- / :---: / ---: markers. Later rows
// are padded or truncated to the header's width. Cells carrying
// colspan/rowspan emit as ordinary cells: a merged grid displays better
// flattened than smeared, and editing such tables stays guarded by
Copilot AI review requested due to automatic review settings August 22, 2026 08:44

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f5514c5cab

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

for i, code := range codes {
code = reWhitespaceRun.ReplaceAllString(strings.TrimSpace(code), " ")
code = strings.ReplaceAll(code, "|", `\|`)
s = strings.Replace(s, "\x00"+strconv.Itoa(i)+"\x00", "`"+code+"`", 1)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Choose a safe delimiter for table code spans

When a simple table contains a code span with a literal backtick, such as HTML generated from ``a`b``, this always re-emits the cell as `a`b`, so Goldmark closes the span at the embedded backtick. Opening and saving the table unchanged therefore changes the code contents and markup; select a delimiter longer than the longest backtick run and apply the required padding.

Useful? React with 👍 / 👎.

// display, so resubmitting that text would lose the structure.
var (
reTableMergedCell = regexp.MustCompile(`(?i)\b(?:colspan|rowspan)\s*=`)
reTableComplexInner = regexp.MustCompile(`(?i)<(?:ul|ol|pre|blockquote|h[1-6]|caption|figure|img|bc-attachment)[\s/>]`)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Treat div-separated table cells as complex

The new paragraph/line checks still let a cell such as <td><div>one</div><div>two</div></td> through because div is absent from this pattern, even though it is recognized as safe rich-text HTML elsewhere. cellMarkdown then replaces both tags with spaces, and an unchanged edit resubmits one two, permanently losing the block boundary; classify these block containers as complex or preserve their boundaries.

Useful? React with 👍 / 👎.

// table cannot represent — cellMarkdown flattens them to single-line text for
// display, so resubmitting that text would lose the structure.
var (
reTableMergedCell = regexp.MustCompile(`(?i)\b(?:colspan|rowspan)\s*=`)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Restrict merged-cell detection to tag attributes

A simple table is incorrectly blocked from all TUI edit paths whenever ordinary cell text contains colspan= or rowspan=—for example, <td>Set colspan=2 in the template</td> matches this unscoped regex even though no cell is merged. Limit the check to attributes on opening th/td tags so textual mentions of these attribute names do not force users to edit the content on the web.

Useful? React with 👍 / 👎.

Comment on lines +713 to +715
html = reTableBlock.ReplaceAllStringFunc(html, func(s string) string {
return convertTableHTML(s) + "\n\n"
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Convert tables before wrapping blockquotes

When a table is nested in a blockquote, the blockquote pass runs first and prefixes the still-HTML table as a single line; this later replacement inserts a multiline pipe table while leaving > only on its first row. The separator and body rows therefore escape the quote and Goldmark cannot parse the result as the nested table that produced the HTML, so CLI/TUI display no longer renders the promised best-effort grid. Convert the table within the blockquote before splitting and prefixing its lines.

Useful? React with 👍 / 👎.

Comment on lines +1719 to +1722
for _, cell := range reTableCellHTML.FindAllStringSubmatch(block, -1) {
if reBR.MatchString(cell[2]) || len(reOpeningP.FindAllString(cell[2], -1)) > 1 {
return true
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Block tables with multiple header rows from editing

A valid table such as <thead><tr><th>A</th></tr><tr><th>B</th></tr></thead> passes this complexity scan, but convertTableHTML promotes only the first row to the GFM header. Saving an unchanged edit consequently recreates the second row with <td> cells in <tbody>, permanently losing its header semantics; detect additional header rows (or any later th cells) and keep these tables on the complex-table path.

Useful? React with 👍 / 👎.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.

Suppressed comments (2)

internal/tui/workspace/views/todos.go:1049

  • This guard now admits simple tables, but this editor immediately calls Reset(), which switches the composer to quick mode, and then SetValue() puts the multiline pipe table into the single-line text input. The line breaks are flattened, so submitting without making any edit no longer parses as a table and destroys it. Restore rich mode before pre-populating Markdown (for example, use InsertPaste, which expands on multiline/Markdown input) before allowing this path.
	if richtext.HasComplexTableHTML(description) {

internal/richtext/richtext.go:823

  • This doc comment contradicts the implementation below: rows are no longer truncated to the header width; the widest row determines the width and narrower rows are padded. Update it so callers do not infer that cell data may be discarded.
// column alignment — mapped back to :--- / :---: / ---: markers. Later rows
// are padded or truncated to the header's width. Cells carrying
// colspan/rowspan emit as ordinary cells: a merged grid displays better

Comment on lines +1719 to +1723
for _, cell := range reTableCellHTML.FindAllStringSubmatch(block, -1) {
if reBR.MatchString(cell[2]) || len(reOpeningP.FindAllString(cell[2], -1)) > 1 {
return true
}
}
Comment on lines +929 to +932
for i, code := range codes {
code = reWhitespaceRun.ReplaceAllString(strings.TrimSpace(code), " ")
code = strings.ReplaceAll(code, "|", `\|`)
s = strings.Replace(s, "\x00"+strconv.Itoa(i)+"\x00", "`"+code+"`", 1)
HTMLToMarkdown had no table handling: <table> markup survived every pass
untouched until the final tag-stripping regex deleted the tags and ran the
cell text together. Display of any table-bearing message, comment, or to-do
description came out as one smeared line, and the TUI in-place editors had to
refuse table-bearing content wholesale to avoid destroying it on resubmit.

Convert tables to GFM pipe tables instead. A new pass runs while row and cell
tags are still intact: each <table> block is extracted (BC3 rich text is
sanitized editor output — always flat editor-authored grids, so a non-greedy
block match is safe), rows and cells are pulled out by regex, and the block is
emitted as a pipe table. The first row is the header whether its cells are
<th> or <td> (GFM has no headerless tables); align attributes — exactly what
MarkdownToHTML emits for GFM column alignment — map back to :--- / :---: /
---: markers; the widest row sizes the table, with narrower rows padded, so no
row is ever truncated. Cell content runs through the same inline conversions
as body text (bold, italic, code, links, strikethrough, mentions,
attachments) and block boundaries collapse to spaces.

Escaping is GFM-exact: pipes escape as \| and literal backslashes double —
GFM processes escapes left to right, so a lone \ before an escaped pipe would
swallow its backslash and turn the pipe back into a delimiter. Code spans
pass through as placeholders since backslashes are literal inside code; only
their pipes are escaped. Cell entities are fully decoded before escaping —
an encoded pipe is still a pipe, and goldmark would decode &#124; on the
next render — and the emitted tables are parked behind placeholders until
HTMLToMarkdown's document-level unescape pass has run, so nothing
double-decodes. A goldmark-backed test proves the parsed cell content
survives the full HTML → Markdown → HTML cycle (backslash-pipe text, code
spans with pipes and backslashes, entity-encoded pipes and backslashes in
text and code, pipes in link destinations, adjacent tables), and pipe tables
round-trip byte-identical through MarkdownToHTML → HTMLToMarkdown, alignment
and the #648 blank-line separator included.

Shapes GFM can't represent still display, best-effort — colspan/rowspan cells
emit as ordinary cells, a merged grid displaying better flattened than
smeared — but editing them stays blocked: the blanket HasTableHTML gate on
the three TUI in-place editors is replaced by HasComplexTableHTML, which
fails closed on merged cells, captions, nested tables, attachments/images or
block elements inside the table, cells spanning multiple paragraphs or lines,
a table nested in a blockquote or list, unclosed tables, and tables the
converter can't extract a grid from (they'd otherwise vanish with their
content) — checking every table in the content, not just the first. Mentions are the
one rich element cells may keep: they convert to **@name** exactly as they
do in body text. Simple grids open for editing like any other content.

The mention-conversion closure moves to a named mentionMarkdown function so
cell conversion can reuse it; behavior is unchanged.

Editing simple tables in the todos view requires the composer Reset fix
(previous commit): without it, Reset dropped the description composer to
single-line mode and SetValue flattened the freshly-converted pipe table.
The todos test asserts the exact multiline table to prove the pairing.
Copilot AI review requested due to automatic review settings August 22, 2026 09:05
@jeremy
jeremy force-pushed the html-to-markdown-tables branch from f5514c5 to cceb8a7 Compare August 22, 2026 09:05
@jeremy
jeremy changed the base branch from main to composer-reset-keeps-mode August 22, 2026 09:05

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.

Suppressed comments (1)

internal/richtext/richtext.go:1683

  • This pattern scans the entire table body, so ordinary cell text such as document colspan= here is mistaken for a merged-cell attribute and the TUI unnecessarily blocks an otherwise simple table. Restrict the match to th/td opening tags.
	reTableMergedCell   = regexp.MustCompile(`(?i)\b(?:colspan|rowspan)\s*=`)

// text. Pipes would split the row. Backslashes must double: GFM processes
// escapes left to right, so a lone literal `\` before an escaped pipe would
// swallow its backslash and turn the pipe back into a delimiter.
var reCellEscape = regexp.MustCompile(`[\\|]`)
Comment on lines +947 to +951
for i, code := range codes {
code = reWhitespaceRun.ReplaceAllString(strings.TrimSpace(code), " ")
code = html.UnescapeString(code)
code = strings.ReplaceAll(code, "|", `\|`)
s = strings.Replace(s, "\x00"+strconv.Itoa(i)+"\x00", "`"+code+"`", 1)

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: cceb8a7010

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +943 to +944
s = strings.ReplaceAll(html.UnescapeString(s), "\u00a0", " ")
s = reCellEscape.ReplaceAllString(s, `\${0}`)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Escape Markdown syntax in literal cell text

When a simple cell contains literal Markdown punctuation, such as HTML produced from the valid GFM input \*literal\*, this pass emits *literal* because it escapes only backslashes and pipes. Opening and saving the table unchanged therefore converts the literal asterisks into emphasis; angle-bracket text can similarly become raw HTML. Escape Markdown metacharacters in text nodes while preserving the Markdown deliberately generated for inline elements.

Useful? React with 👍 / 👎.

Comment on lines +947 to +949
for i, code := range codes {
code = reWhitespaceRun.ReplaceAllString(strings.TrimSpace(code), " ")
code = html.UnescapeString(code)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve significant whitespace inside code spans

For a simple table containing an inline code span with repeated whitespace, such as <code>a b</code>, this collapses the code content to a b. GFM code spans preserve interior repeated spaces, so an unchanged TUI edit permanently changes the code value; normalize line endings as required for code spans, but do not collapse all whitespace runs.

Useful? React with 👍 / 👎.

s = reHTMLB.ReplaceAllString(s, "**$1**")
s = reHTMLEm.ReplaceAllString(s, "*$1*")
s = reHTMLI.ReplaceAllString(s, "*$1*")
s = reHTMLLink.ReplaceAllString(s, "[$2]($1)")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve titles on links inside editable tables

When a simple table originates from valid GFM such as [docs](https://example.com "API docs"), MarkdownToHTML stores the title on the anchor, but this replacement reconstructs only its label and destination. Opening and saving the table unchanged therefore permanently removes the link title; include the optional title when emitting the Markdown link or classify such anchors as complex.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

skills Agent skills tests Tests (unit and e2e) tui Terminal UI

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants