Show stage labels as literal Markdown text

This commit is contained in:
2026-11-10 20:24:18 +00:00
parent bae96e5dd5
commit a0e45c8f92
3 changed files with 41 additions and 7 deletions

View File

@@ -219,6 +219,8 @@ selection rules and native comparison status.
## R6 — P3: Render Stage IDs as Data in Markdown
**Status:** Completed. Markdown escapes stage labels as literal cell text. Tests cover Markdown syntax, a pipe next to a backslash, and exact YAML IDs. A rendered table check confirmed the label stays in one cell.
**Category**
Incomplete output escaping in Stage 13.

View File

@@ -48,11 +48,11 @@ func Markdown(snapshot Snapshot, secrets ...string) []byte {
fmt.Fprintln(&b, "## Work-unit errors")
fmt.Fprintln(&b)
writeMatrix(&b, snapshot, safe, false)
writeMatrix(&b, snapshot, r, false)
fmt.Fprintln(&b)
fmt.Fprintln(&b, "## Recoveries")
fmt.Fprintln(&b)
writeMatrix(&b, snapshot, safe, true)
writeMatrix(&b, snapshot, r, true)
fmt.Fprintln(&b)
fmt.Fprintln(&b, "Legend: `—` = not started; `n*` = interrupted with n recorded errors or recoveries. Completed error cells are 0 or 1.")
fmt.Fprintln(&b)
@@ -71,7 +71,7 @@ func showCount(value *int) string {
return fmt.Sprint(*value)
}
func writeMatrix(b *strings.Builder, snapshot Snapshot, safe func(string) string, recovery bool) {
func writeMatrix(b *strings.Builder, snapshot Snapshot, redactor redaction.Redactor, recovery bool) {
if len(snapshot.StageNames) == 0 || len(snapshot.WorkUnits) == 0 {
fmt.Fprintln(b, "No work units.")
return
@@ -79,7 +79,7 @@ func writeMatrix(b *strings.Builder, snapshot Snapshot, safe func(string) string
columns := snapshot.StageNames
fmt.Fprint(b, "| Chunk |")
for _, name := range columns {
fmt.Fprintf(b, " %s |", safe(name))
fmt.Fprintf(b, " %s |", stageCellEscaper.Replace(html.EscapeString(redactor.String(name))))
}
fmt.Fprint(b, "\n| --- |")
for range columns {
@@ -120,6 +120,14 @@ func writeMatrix(b *strings.Builder, snapshot Snapshot, safe func(string) string
}
}
// Encode input backslashes so they cannot interfere with Markdown escapes.
var stageCellEscaper = strings.NewReplacer(
"\r\n", "<br>", "\r", "<br>", "\n", "<br>",
"\\", "&#92;", "|", "\\|", "`", "\\`",
"[", "\\[", "]", "\\]", "*", "\\*", "_", "\\_", "~", "\\~",
"://", "&#58;//", "www.", "www&#46;", "@", "&#64;",
)
func writeReasons(b *strings.Builder, title string, counts map[string]int, safe func(string) string) {
fmt.Fprintf(b, "### %s\n\n", title)
if len(counts) == 0 {

View File

@@ -33,11 +33,11 @@ func TestMarkdownFixedLayoutAndCounts(t *testing.T) {
// Compare only complete table rows, preserving order and states while leaving
// prose and spacing outside the fixed matrices free to change.
wantRows := []string{
"| Chunk | grammar_1 | grammar_2 |",
"| Chunk | grammar\\_1 | grammar\\_2 |",
"| --- | ---: | ---: |",
"| Chunk 1 (segments 1–2) | 1 | 0* |",
"| Chunk 2 (segments 3–4) | 0 | — |",
"| Chunk | grammar_1 | grammar_2 |",
"| Chunk | grammar\\_1 | grammar\\_2 |",
"| --- | ---: | ---: |",
"| Chunk 1 (segments 1–2) | 1 | 2* |",
"| Chunk 2 (segments 3–4) | 0 | — |",
@@ -93,11 +93,35 @@ func TestMarkdownNoWorkAndEscapedStageLabel(t *testing.T) {
t.Fatal(text)
}
text = string(Markdown(Snapshot{RunID: "r", StageNames: []string{"bad|label`\nsecret"}, WorkUnits: []WorkUnit{{StageIndex: 0, ChunkIndex: 0, State: "completed"}}}, "secret"))
if !strings.Contains(text, "bad\\|label\\`<br>[REDACTED]") {
if !strings.Contains(text, "bad\\|label\\`<br>\\[REDACTED\\]") {
t.Fatal(text)
}
}
func TestStageIDIsLiteralInMarkdownAndExactInYAML(t *testing.T) {
id := `[party](https://example.invalid/) *bold* _emphasis_ ~~strike~~ \| <tag>`
snapshot := Snapshot{
StageNames: []string{StageLabel(0, "grammar", &id), StageLabel(1, "grammar", nil)},
WorkUnits: []WorkUnit{{StageIndex: 0, ChunkIndex: 0, State: "completed", ErrorCount: 1}},
Errors: []ErrorRecord{{Scope: "work_unit", StageID: &id}},
}
header := `| Chunk | 1: grammar (\[party\](https&#58;//example.invalid/) \*bold\* \_emphasis\_ \~\~strike\~\~ &#92;\| &lt;tag&gt;) | 2: grammar |`
if rows := markdownTableRows(string(Markdown(snapshot))); len(rows) == 0 || rows[0] != header {
t.Fatalf("stage labels did not stay in their cells: %v", rows)
}
data, err := YAML(snapshot.ErrorsDocument())
if err != nil {
t.Fatal(err)
}
var decoded struct{ Errors []ErrorRecord }
if err := yaml.Unmarshal(data, &decoded); err != nil {
t.Fatal(err)
}
if len(decoded.Errors) != 1 || decoded.Errors[0].StageID == nil || *decoded.Errors[0].StageID != id {
t.Fatalf("stage ID changed in YAML: %s", data)
}
}
func markdownTableRows(text string) []string {
var rows []string
for _, line := range strings.Split(text, "\n") {