▍ humdrum codex / glint v1.0.2

feat: syntax-highlight YAML frontmatter (keys/values/markers/comments)

8817cc29ec73341aea92ed8204679afa21a91174
humdrum <me@humdrum.me> · 2026-06-28 09:17

parent b379b91c

2 files changed

internal/editor/scanner.go +47 −1
@@ -55,8 +55,9 @@ 	if st.inFrontmatter {
 		if trimmed == "---" {
 			st.inFrontmatter = false
 			st.frontmatterDone = true
+			return wholeLine(line, muted)
 		}
-		return wholeLine(line, muted)
+		return scanFrontmatter(line, th)
 	}
 	if row == 0 && trimmed == "---" {
 		st.inFrontmatter = true
@@ -90,6 +91,51 @@ 	if line == "" {
 		return nil
 	}
 	return []Span{{Text: line, Style: style}}
+}
+
+// scanFrontmatter highlights one YAML frontmatter line, keeping every character
+// visible: the spans concatenate back to the raw line exactly.
+//   - "# comment"         -> whole line muted
+//   - "  - item"          -> leading ws + dash (ListMarker) + item (Text)
+//   - "key: value"        -> "key:" (Accent) + " value" (Text)
+//   - anything else       -> whole line Text
+func scanFrontmatter(line string, th theme.Theme) []Span {
+	muted := lipgloss.NewStyle().Foreground(th.Blockquote)
+	accent := lipgloss.NewStyle().Foreground(th.Accent)
+	plain := lipgloss.NewStyle().Foreground(th.Text)
+	marker := lipgloss.NewStyle().Foreground(th.ListMarker)
+
+	trimmed := strings.TrimSpace(line)
+
+	// Comment line.
+	if strings.HasPrefix(trimmed, "#") {
+		return wholeLine(line, muted)
+	}
+
+	// List item: optional leading whitespace, then "- ...".
+	if loc := listRe.FindStringIndex(line); loc != nil && strings.HasPrefix(trimmed, "-") {
+		mk := line[:loc[1]]   // leading ws + "- "
+		rest := line[loc[1]:] // item text
+		spans := []Span{{Text: mk, Style: marker}}
+		if rest != "" {
+			spans = append(spans, Span{Text: rest, Style: plain})
+		}
+		return spans
+	}
+
+	// key: value — split at the first colon. Keep the colon with the key.
+	if idx := strings.IndexByte(line, ':'); idx >= 0 {
+		key := line[:idx+1]   // includes the colon
+		rest := line[idx+1:]  // remainder (may start with a space)
+		spans := []Span{{Text: key, Style: accent}}
+		if rest != "" {
+			spans = append(spans, Span{Text: rest, Style: plain})
+		}
+		return spans
+	}
+
+	// Bare line (e.g. a continuation): plain text.
+	return wholeLine(line, plain)
 }
 
 // scanInline emits styled spans for inline constructs, keeping all markup
internal/editor/scanner_test.go +50 −3
@@ -82,10 +82,12 @@ func TestScanLeadingFrontmatter(t *testing.T) {
 	th := theme.FlexokiDark()
 	lines := []string{"---", "title: x", "---", "body"}
 	out := ScanLines(lines, th)
-	if out[1][0].Style.GetForeground() != th.Blockquote {
-		t.Errorf("frontmatter body not muted")
+	if out[0][0].Style.GetForeground() != th.Muted {
+		t.Errorf("opening --- not muted")
 	}
-	// after the closing ---, normal text resumes
+	if out[1][0].Style.GetForeground() != th.Accent {
+		t.Errorf("frontmatter key not accent-colored")
+	}
 	if out[3][0].Style.GetForeground() != th.Text {
 		t.Errorf("post-frontmatter line should be plain text")
 	}
@@ -119,3 +121,48 @@ 	if spanText(out[0]) != "- **x**" {
 		t.Errorf("list line text altered: %q", spanText(out[0]))
 	}
 }
+
+func TestScanFrontmatterHighlightsKeysAndValues(t *testing.T) {
+	th := theme.FlexokiDark()
+	lines := []string{"---", "title: Hello", "tags:", "  - one", "  - two", "# a comment", "---", "body"}
+	out := ScanLines(lines, th)
+
+	// Char-for-char preservation across every frontmatter line.
+	for i, raw := range lines {
+		if got := spanText(out[i]); got != raw {
+			t.Errorf("line %d: span text %q != raw %q", i, got, raw)
+		}
+	}
+	// Opening and closing --- are muted.
+	if out[0][0].Style.GetForeground() != th.Muted {
+		t.Errorf("opening --- not muted")
+	}
+	if out[6][0].Style.GetForeground() != th.Muted {
+		t.Errorf("closing --- not muted")
+	}
+	// "title:" key portion is Accent; the value is Text.
+	if out[1][0].Style.GetForeground() != th.Accent {
+		t.Errorf("key not Accent-colored, got %v", out[1][0].Style.GetForeground())
+	}
+	if last := out[1][len(out[1])-1]; last.Style.GetForeground() != th.Text {
+		t.Errorf("value not Text-colored, got %v", last.Style.GetForeground())
+	}
+	// List item marker "-" is ListMarker-colored.
+	foundMarker := false
+	for _, sp := range out[3] {
+		if sp.Style.GetForeground() == th.ListMarker {
+			foundMarker = true
+		}
+	}
+	if !foundMarker {
+		t.Errorf("list item dash not ListMarker-colored")
+	}
+	// Comment line is muted.
+	if out[5][0].Style.GetForeground() != th.Muted {
+		t.Errorf("comment not muted")
+	}
+	// After the closing ---, normal text resumes.
+	if out[7][0].Style.GetForeground() != th.Text {
+		t.Errorf("post-frontmatter line should be plain text")
+	}
+}