mirror of
https://github.com/runbear-io/beardrive.git
synced 2026-08-25 08:08:08 +02:00
A doc's YAML frontmatter rendered as a table pinned to the top of the reading column, so on anything with more than two or three keys the document itself started below the fold. It is a panel beside the prose now — a sticky rail on a wide window, a closed disclosure above the body on anything narrower — and the reading column starts with the document. The table was built on the server and handed to the client inside one HTML string, so this is not a CSS change: markdown.go splits the parse (frontmatterPairs) from the markup, /api/render gains an ordered `frontmatter` field, and the viewer switches to RenderMarkdownPairs. RenderMarkdown keeps its exact output — it is the public share page, and every /s/ link ever minted serves it. shares_test now pins that, because nothing else would have failed if a later cleanup moved shares.go onto the pairs path. Values cross the wire as literal text plus a `code` flag rather than pre-escaped HTML, so the panel is ordinary React text nodes and never touches dangerouslySetInnerHTML: "a value containing markup renders as text" holds by construction. The rail's breakpoint is 1400px, not the 1180px the plan named — 768 of prose + 28 + 240 of rail needs 1036px of column, and at 1280 the reading measure lost 110px, which is the squeeze the panel exists to avoid. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
156 lines
4.6 KiB
Go
156 lines
4.6 KiB
Go
package webapp
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// A leading YAML frontmatter block renders as a key/value table (author
|
|
// key order, escaped values) instead of goldmark's thematic-break soup;
|
|
// everything that isn't a clean frontmatter mapping renders exactly as
|
|
// before.
|
|
func TestRenderMarkdownFrontmatter(t *testing.T) {
|
|
src := `---
|
|
title: Q3 findings
|
|
tags: [churn, revenue]
|
|
owner: snow@runbear.io
|
|
meta:
|
|
reviewed: true
|
|
---
|
|
|
|
# Body
|
|
|
|
Hello.`
|
|
out, err := RenderMarkdown([]byte(src))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, want := range []string{
|
|
`<table class="frontmatter">`,
|
|
`<th scope="row">title</th><td>Q3 findings</td>`,
|
|
`<td>churn, revenue</td>`, // flat lists comma-join
|
|
`owner`, `snow@runbear.io`,
|
|
`<code>reviewed: true</code>`, // nested values as compact YAML
|
|
`<h1 id="body">Body</h1>`, // the body still renders
|
|
} {
|
|
if !strings.Contains(out, want) {
|
|
t.Errorf("missing %q in:\n%s", want, out)
|
|
}
|
|
}
|
|
if strings.Contains(out, "<hr") {
|
|
t.Errorf("frontmatter fences leaked as thematic breaks:\n%s", out)
|
|
}
|
|
// Key order preserved: title row precedes owner row.
|
|
if strings.Index(out, ">title<") > strings.Index(out, ">owner<") {
|
|
t.Errorf("frontmatter keys reordered:\n%s", out)
|
|
}
|
|
}
|
|
|
|
func TestRenderMarkdownFrontmatterEscapes(t *testing.T) {
|
|
out, err := RenderMarkdown([]byte("---\nnote: <script>alert(1)</script>\n---\nx"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if strings.Contains(out, "<script>") {
|
|
t.Fatalf("frontmatter value not escaped:\n%s", out)
|
|
}
|
|
if !strings.Contains(out, "<script>") {
|
|
t.Fatalf("escaped value missing:\n%s", out)
|
|
}
|
|
}
|
|
|
|
func TestRenderMarkdownFrontmatterFallthrough(t *testing.T) {
|
|
cases := map[string]struct {
|
|
src string
|
|
wantTable bool
|
|
want string
|
|
}{
|
|
"no frontmatter": {"# Hi\n\ntext", false, "<h1"},
|
|
"mid-doc fences": {"para\n\n---\n\nmore", false, "<hr"},
|
|
"unclosed fence": {"---\ntitle: x\n\nbody", false, ""},
|
|
"non-mapping yaml": {"---\n- just\n- a list\n---\nbody", false, ""},
|
|
"invalid yaml": {"---\n: : :\n---\nbody", false, ""},
|
|
"empty frontmatter hidden": {"---\n---\nbody", false, "<p>body</p>"},
|
|
}
|
|
for name, c := range cases {
|
|
out, err := RenderMarkdown([]byte(c.src))
|
|
if err != nil {
|
|
t.Fatalf("%s: %v", name, err)
|
|
}
|
|
if got := strings.Contains(out, `class="frontmatter"`); got != c.wantTable {
|
|
t.Errorf("%s: table presence = %v, want %v\n%s", name, got, c.wantTable, out)
|
|
}
|
|
if c.want != "" && !strings.Contains(out, c.want) {
|
|
t.Errorf("%s: missing %q in:\n%s", name, c.want, out)
|
|
}
|
|
}
|
|
}
|
|
|
|
// The viewer's split: frontmatter comes back as ordered data and the HTML
|
|
// is body-only, with the same value rules the table has always applied.
|
|
func TestFrontmatterPairs(t *testing.T) {
|
|
src := `---
|
|
title: Q3 findings
|
|
tags: [churn, revenue]
|
|
owner: snow@runbear.io
|
|
meta:
|
|
reviewed: true
|
|
---
|
|
|
|
# Body
|
|
|
|
Hello.`
|
|
pairs, out, err := RenderMarkdownPairs([]byte(src))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
want := []FrontmatterPair{
|
|
{Key: "title", Value: "Q3 findings"},
|
|
{Key: "tags", Value: "churn, revenue"}, // flat lists comma-join
|
|
{Key: "owner", Value: "snow@runbear.io"},
|
|
{Key: "meta", Value: "reviewed: true", Code: true}, // nested: compact YAML
|
|
}
|
|
if len(pairs) != len(want) {
|
|
t.Fatalf("pairs = %+v, want %d", pairs, len(want))
|
|
}
|
|
for i, w := range want {
|
|
if pairs[i] != w { // index-wise: author order is the contract
|
|
t.Errorf("pair %d = %+v, want %+v", i, pairs[i], w)
|
|
}
|
|
}
|
|
if strings.Contains(out, `class="frontmatter"`) {
|
|
t.Errorf("table leaked into the viewer's html:\n%s", out)
|
|
}
|
|
if !strings.HasPrefix(strings.TrimSpace(out), `<h1 id="body">Body</h1>`) {
|
|
t.Errorf("body does not start with its heading:\n%s", out)
|
|
}
|
|
}
|
|
|
|
// Same tri-state as the table: anything that isn't a clean YAML mapping
|
|
// falls through with the source untouched, and empty frontmatter is hidden.
|
|
func TestFrontmatterPairsFallthrough(t *testing.T) {
|
|
cases := map[string]struct {
|
|
src string
|
|
want string
|
|
}{
|
|
"no frontmatter": {"# Hi\n\ntext", "<h1"},
|
|
"mid-doc fences": {"para\n\n---\n\nmore", "<hr"},
|
|
"unclosed fence": {"---\ntitle: x\n\nbody", ""},
|
|
"non-mapping yaml": {"---\n- just\n- a list\n---\nbody", ""},
|
|
"invalid yaml": {"---\n: : :\n---\nbody", ""},
|
|
"empty frontmatter hidden": {"---\n---\nbody", "<p>body</p>"},
|
|
}
|
|
for name, c := range cases {
|
|
pairs, out, err := RenderMarkdownPairs([]byte(c.src))
|
|
if err != nil {
|
|
t.Fatalf("%s: %v", name, err)
|
|
}
|
|
if len(pairs) != 0 {
|
|
t.Errorf("%s: pairs = %+v, want none", name, pairs)
|
|
}
|
|
if c.want != "" && !strings.Contains(out, c.want) {
|
|
t.Errorf("%s: missing %q in:\n%s", name, c.want, out)
|
|
}
|
|
}
|
|
}
|