Show stage labels as literal Markdown text
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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>",
|
||||
"\\", "\", "|", "\\|", "`", "\\`",
|
||||
"[", "\\[", "]", "\\]", "*", "\\*", "_", "\\_", "~", "\\~",
|
||||
"://", "://", "www.", "www.", "@", "@",
|
||||
)
|
||||
|
||||
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 {
|
||||
|
||||
@@ -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://example.invalid/) \*bold\* \_emphasis\_ \~\~strike\~\~ \\| <tag>) | 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") {
|
||||
|
||||
Reference in New Issue
Block a user