Erweiterung und CSS
- Code bekommt größere Schrift - In einer Datei können neue Slides erstellt werden
This commit is contained in:
parent
291ee93b5b
commit
0fa66d9f48
10 changed files with 372 additions and 37 deletions
51
AGENTS.md
Normal file
51
AGENTS.md
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
# AGENTS.md
|
||||
|
||||
Go-Tool zum Präsentieren von Markdown-Slides im Browser. Details siehe
|
||||
[README.md](README.md) (Nutzung) und [docs/architektur.md](docs/architektur.md)
|
||||
(Code-Aufbau).
|
||||
|
||||
## Setup
|
||||
|
||||
```sh
|
||||
go build -o slidewalk ./cmd/slidewalk
|
||||
```
|
||||
|
||||
## Tests
|
||||
|
||||
```sh
|
||||
go test ./...
|
||||
```
|
||||
|
||||
Vor jedem Commit: `go test ./...` und `gofmt -l .` (muss leer sein) laufen
|
||||
lassen.
|
||||
|
||||
## Struktur
|
||||
|
||||
- `internal/parser` — Markdown-Dateien → `[]Slide`
|
||||
- `internal/render` — `[]Slide` → eine HTML-Seite
|
||||
- `internal/watch` — Dev-Server mit Live-Reload
|
||||
- `internal/export` — statischer HTML-Export
|
||||
- `web` — Templates/CSS/JS, per `embed.FS` eingebettet
|
||||
- `cmd/slidewalk` — CLI-Einstiegspunkt
|
||||
|
||||
`watch` und `export` rufen ausschließlich `parser.ParseDir` und
|
||||
`render.Render` auf — keine zweite Stelle, die Markdown parst oder HTML baut.
|
||||
|
||||
## Konventionen
|
||||
|
||||
- Reines Markdown als Slide-Quelle, eine Datei pro Slide, Reihenfolge über
|
||||
Dateiname-Präfix (`010-intro.md`).
|
||||
- Keine neuen externen Laufzeit-Abhängigkeiten ohne Rücksprache — Ziel ist
|
||||
ein einziges, eingebettetes Binary ohne CDN-Requests.
|
||||
- Table-driven Tests neben dem jeweiligen Package (`*_test.go`).
|
||||
|
||||
## Arbeitsweise
|
||||
|
||||
- Lean und minimalistisch: die einfachste Lösung, die das Problem löst.
|
||||
Keine Abstraktionen, Konfigurierbarkeit oder Optionen für hypothetische
|
||||
Zukunftsfälle.
|
||||
- Keine Features, kein Refactoring, keine Aufräumarbeiten, die nicht
|
||||
explizit gefragt wurden.
|
||||
- Antworten knapp und auf das Wesentliche konzentriert — keine
|
||||
Wiederholung des Offensichtlichen, keine ausschweifenden Erklärungen,
|
||||
keine Zusammenfassungen am Ende, wenn der Diff für sich spricht.
|
||||
19
README.md
19
README.md
|
|
@ -113,6 +113,25 @@ Stichpunkten im Vortrag. Zusätzlich (oder alternativ) lässt sich jedes
|
|||
beliebige Element per raw HTML mit `class="fragment"` einzeln als Fragment
|
||||
markieren.
|
||||
|
||||
## Manueller Seitenumbruch
|
||||
|
||||
Eine Zeile mit `<!-- new slide -->` teilt eine einzelne Slide-Datei an
|
||||
dieser Stelle in mehrere Slides auf — praktisch, wenn zusammengehöriger
|
||||
Inhalt trotzdem auf mehrere Slides verteilt werden soll, ohne die Datei
|
||||
selbst zu splitten. Alle so entstehenden Slides teilen sich das
|
||||
Frontmatter der Datei (`title`, `class`, `notes`, `incremental`).
|
||||
|
||||
```markdown
|
||||
# Erster Teil
|
||||
|
||||
<!-- new slide -->
|
||||
|
||||
# Zweiter Teil
|
||||
```
|
||||
|
||||
Der Marker muss allein auf seiner Zeile stehen. Innerhalb eines Codeblocks
|
||||
(```` ``` ```` oder `~~~`) wird er ignoriert und nicht als Umbruch gewertet.
|
||||
|
||||
## Frontmatter-Referenz
|
||||
|
||||
Jede Slide-Datei kann optional ein YAML-Frontmatter am Dateianfang haben:
|
||||
|
|
|
|||
|
|
@ -47,8 +47,9 @@ machen.
|
|||
|
||||
| Datei | Enthält |
|
||||
| --------------------- | ------- |
|
||||
| `parser.go` | Öffentliche API: `Slide`-Struct, `ParseDir(dir)`, Datei-Discovery (`discoverSlideFiles`), Zusammenbau eines einzelnen Slides (`parseSlide`) |
|
||||
| `parser.go` | Öffentliche API: `Slide`-Struct, `ParseDir(dir)`, Datei-Discovery (`discoverSlideFiles`), Zusammenbau der Slides einer Datei (`parseSlideFile`) |
|
||||
| `frontmatter.go` | YAML-Frontmatter-Handling: `splitFrontmatter` trennt den `---`-Block vom Markdown-Body, `parseFrontmatter` parst ihn in das `frontmatter`-Struct (`title`, `class`, `notes`, `skip`, `incremental`) |
|
||||
| `split.go` | `splitSlideBreaks` teilt den Body einer Datei an `<!-- new slide -->`-Markierungszeilen in mehrere Teile, fence-bewusst (ignoriert Marker innerhalb von ```` ``` ````/`~~~`-Codeblöcken) |
|
||||
| `markdown.go` | Die `goldmark`-Instanz (`md`) mit allen Extensions, plus der custom Fenced-Code-Block-Renderer (Mermaid-Sonderfall + Chroma-Syntax-Highlighting) |
|
||||
| `*_test.go` | Table-driven Tests je Datei |
|
||||
|
||||
|
|
@ -57,14 +58,19 @@ machen.
|
|||
1. `discoverSlideFiles(dir)` listet alle `.md`-Dateien im Ordner (nicht
|
||||
rekursiv) und sortiert sie alphabetisch — das ist der Mechanismus hinter
|
||||
der Präfix-Reihenfolge (`010-...`, `020-...`).
|
||||
2. Für jede Datei: `os.ReadFile` → `parseSlide(name, data)`.
|
||||
3. `parseSlide` (`parser.go:134`) ruft zuerst `parseFrontmatter` auf. Ist
|
||||
`skip: true` gesetzt, bricht es sofort ab (zweiter Rückgabewert `skip
|
||||
bool`), ohne den Markdown-Body überhaupt zu rendern.
|
||||
4. Sonst: `renderMarkdown(body)` aus `markdown.go` wandelt den Rest in HTML
|
||||
um, das Ergebnis landet als `template.HTML` im `Slide`-Struct.
|
||||
5. `ParseDir` filtert alle als "skip" markierten Slides aus der
|
||||
zurückgegebenen Liste heraus.
|
||||
2. Für jede Datei: `os.ReadFile` → `parseSlideFile(name, data)`.
|
||||
3. `parseSlideFile` (`parser.go:134`) ruft zuerst `parseFrontmatter` auf.
|
||||
Ist `skip: true` gesetzt, bricht es sofort ab (leere Slide-Liste), ohne
|
||||
den Markdown-Body überhaupt zu rendern.
|
||||
4. Sonst: `splitSlideBreaks(body)` aus `split.go` teilt den Body an
|
||||
`<!-- new slide -->`-Zeilen in einen oder mehrere Teile (ohne Marker: ein
|
||||
Teil = der unveränderte Body). Für jeden Teil wandelt
|
||||
`renderMarkdown(part)` aus `markdown.go` den Markdown-Text in HTML um;
|
||||
das Ergebnis landet als `template.HTML` in einem eigenen `Slide`-Struct.
|
||||
Alle Teile einer Datei teilen sich deren Frontmatter (`Title`, `Class`,
|
||||
`Notes`, `Incremental`).
|
||||
5. `ParseDir` reiht die Slides aller Dateien aneinander; als "skip"
|
||||
markierte Dateien tragen keine Slides bei.
|
||||
|
||||
### Fenced-Code-Blöcke: Mermaid & Syntax-Highlighting (`markdown.go`)
|
||||
|
||||
|
|
|
|||
|
|
@ -9,8 +9,11 @@ Ein vollständiges, lauffähiges Beispiel-Deck liegt unter
|
|||
|
||||
## 1. Datei- und Ordnerstruktur
|
||||
|
||||
- **Eine Datei = ein Slide.** Jede `.md`-Datei in dem Ordner, den du an
|
||||
`slidewalk` übergibst, wird zu genau einem Slide.
|
||||
- **Eine Datei = ein Slide** — es sei denn, die Datei enthält
|
||||
`<!-- new slide -->`-Markierungen, dann wird sie in mehrere Slides
|
||||
aufgeteilt, siehe [Abschnitt 6a](#6a-manueller-seitenumbruch-in-einer-datei).
|
||||
Jede `.md`-Datei in dem Ordner, den du an `slidewalk` übergibst, wird
|
||||
standardmäßig zu genau einem Slide.
|
||||
- **Reihenfolge über Dateiname-Präfix.** Die Dateien werden alphabetisch
|
||||
sortiert; ein numerischer Präfix mit Lücken macht das Einschieben späterer
|
||||
Slides einfach, ohne bestehende Dateien umbenennen zu müssen:
|
||||
|
|
@ -332,6 +335,39 @@ einzelne Listeneinträge etc.):
|
|||
<p class="fragment">Dieser Absatz erscheint erst auf Tastendruck.</p>
|
||||
```
|
||||
|
||||
## 6a. Manueller Seitenumbruch in einer Datei
|
||||
|
||||
Eine Zeile mit `<!-- new slide -->` teilt die aktuelle Datei an dieser
|
||||
Stelle in mehrere Slides auf — praktisch, wenn zusammengehöriger Inhalt aus
|
||||
Vortrags-Sicht trotzdem auf mehrere Slides verteilt werden soll, ohne die
|
||||
Datei selbst in mehrere Dateien aufzuteilen:
|
||||
|
||||
```markdown
|
||||
---
|
||||
title: Ergebnisse
|
||||
---
|
||||
|
||||
# Ergebnisse (1/2)
|
||||
|
||||
- Punkt eins
|
||||
- Punkt zwei
|
||||
|
||||
<!-- new slide -->
|
||||
|
||||
# Ergebnisse (2/2)
|
||||
|
||||
- Punkt drei
|
||||
```
|
||||
|
||||
Alle so entstehenden Slides teilen sich das Frontmatter der Datei
|
||||
(`title`, `class`, `notes`, `incremental` gelten für jeden Teil gleich).
|
||||
|
||||
Der Marker muss allein auf seiner Zeile stehen (führende/nachfolgende
|
||||
Leerzeichen sind egal). Innerhalb eines Codeblocks — egal ob mit
|
||||
```` ``` ```` oder `~~~` eingezäunt — wird er als reiner Text behandelt und
|
||||
löst keinen Umbruch aus; so lässt sich der Marker auch in Beispiel-Code
|
||||
zeigen, ohne die Slide ungewollt zu splitten.
|
||||
|
||||
## 7. Übersichts-/Grid-Modus
|
||||
|
||||
Taste `O` schaltet einen Übersichts-Modus um, der alle Slides gleichzeitig
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@
|
|||
// - Datei-Discovery: Slide-Dateien in einem Ordner finden und anhand ihres
|
||||
// Dateiname-Präfixes (z. B. "010-intro.md") sortieren
|
||||
// - YAML-Frontmatter der Felder title, class, notes und skip parsen
|
||||
// - eine Datei anhand von "<!-- new slide -->"-Markierungszeilen in
|
||||
// mehrere Slides aufteilen, die sich das Frontmatter der Datei teilen
|
||||
// - Markdown-Inhalt zu HTML rendern (inkl. GFM-Erweiterungen und
|
||||
// Mermaid-Codeblöcken)
|
||||
//
|
||||
|
|
|
|||
|
|
@ -31,14 +31,16 @@ type Slide struct {
|
|||
|
||||
// ParseDir discovers Markdown slide files in dir, sorted by filename, parses
|
||||
// each one, and returns the resulting slides. Files whose frontmatter sets
|
||||
// skip: true are omitted from the result.
|
||||
// skip: true are omitted from the result. A file whose body contains one or
|
||||
// more "<!-- new slide -->" marker lines yields one slide per part, all
|
||||
// sharing that file's frontmatter.
|
||||
func ParseDir(dir string) ([]Slide, error) {
|
||||
names, err := discoverSlideFiles(dir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("discovering slide files in %s: %w", dir, err)
|
||||
}
|
||||
|
||||
slides := make([]Slide, 0, len(names))
|
||||
var slides []Slide
|
||||
for _, name := range names {
|
||||
path := filepath.Join(dir, name)
|
||||
data, err := os.ReadFile(path)
|
||||
|
|
@ -46,14 +48,11 @@ func ParseDir(dir string) ([]Slide, error) {
|
|||
return nil, fmt.Errorf("reading slide file %s: %w", name, err)
|
||||
}
|
||||
|
||||
slide, skip, err := parseSlide(name, data)
|
||||
fileSlides, err := parseSlideFile(name, data)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parsing slide file %s: %w", name, err)
|
||||
}
|
||||
if skip {
|
||||
continue
|
||||
}
|
||||
slides = append(slides, slide)
|
||||
slides = append(slides, fileSlides...)
|
||||
}
|
||||
return slides, nil
|
||||
}
|
||||
|
|
@ -128,29 +127,34 @@ func trimLeadingZeros(digits string) string {
|
|||
return digits[i:]
|
||||
}
|
||||
|
||||
// parseSlide parses a single slide file's raw content into a Slide. The
|
||||
// second return value reports whether the slide's frontmatter sets
|
||||
// skip: true, in which case the Slide should be discarded by the caller.
|
||||
func parseSlide(filename string, data []byte) (Slide, bool, error) {
|
||||
// parseSlideFile parses a single slide file's raw content into one or more
|
||||
// Slides: one per "<!-- new slide -->"-separated part of its body, all
|
||||
// sharing the file's frontmatter. It returns no Slides if the frontmatter
|
||||
// sets skip: true.
|
||||
func parseSlideFile(filename string, data []byte) ([]Slide, error) {
|
||||
fm, body, err := parseFrontmatter(data)
|
||||
if err != nil {
|
||||
return Slide{}, false, err
|
||||
return nil, err
|
||||
}
|
||||
if fm.Skip {
|
||||
return Slide{}, true, nil
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
htmlBody, err := renderMarkdown(body)
|
||||
if err != nil {
|
||||
return Slide{}, false, fmt.Errorf("rendering markdown: %w", err)
|
||||
parts := splitSlideBreaks(body)
|
||||
slides := make([]Slide, 0, len(parts))
|
||||
for _, part := range parts {
|
||||
htmlBody, err := renderMarkdown(part)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("rendering markdown: %w", err)
|
||||
}
|
||||
slides = append(slides, Slide{
|
||||
Filename: filename,
|
||||
Title: fm.Title,
|
||||
Class: fm.Class,
|
||||
Notes: fm.Notes,
|
||||
Incremental: fm.Incremental,
|
||||
HTML: template.HTML(htmlBody), //nolint:gosec // slide content is trusted local input, not user-supplied.
|
||||
})
|
||||
}
|
||||
|
||||
return Slide{
|
||||
Filename: filename,
|
||||
Title: fm.Title,
|
||||
Class: fm.Class,
|
||||
Notes: fm.Notes,
|
||||
Incremental: fm.Incremental,
|
||||
HTML: template.HTML(htmlBody), //nolint:gosec // slide content is trusted local input, not user-supplied.
|
||||
}, false, nil
|
||||
return slides, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package parser
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
|
@ -111,6 +112,55 @@ func TestParseDir_Incremental(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestParseDir_NewSlideMarkerSplitsOneFileIntoMultipleSlides(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
writeFiles(t, dir, map[string]string{
|
||||
"010-combo.md": "---\ntitle: Combo\nclass: center\n---\n# One\n\n<!-- new slide -->\n\n# Two\n",
|
||||
"020-after.md": "# Three\n",
|
||||
})
|
||||
|
||||
slides, err := ParseDir(dir)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseDir() error = %v", err)
|
||||
}
|
||||
|
||||
if len(slides) != 3 {
|
||||
t.Fatalf("len(slides) = %d, want 3: %+v", len(slides), slides)
|
||||
}
|
||||
if !bytes.Contains([]byte(slides[0].HTML), []byte("One")) {
|
||||
t.Errorf("slides[0].HTML = %q, want it to contain %q", slides[0].HTML, "One")
|
||||
}
|
||||
if !bytes.Contains([]byte(slides[1].HTML), []byte("Two")) {
|
||||
t.Errorf("slides[1].HTML = %q, want it to contain %q", slides[1].HTML, "Two")
|
||||
}
|
||||
if !bytes.Contains([]byte(slides[2].HTML), []byte("Three")) {
|
||||
t.Errorf("slides[2].HTML = %q, want it to contain %q", slides[2].HTML, "Three")
|
||||
}
|
||||
// Both parts of the split file share its frontmatter.
|
||||
if slides[0].Title != "Combo" || slides[1].Title != "Combo" {
|
||||
t.Errorf("slides[0..1].Title = %q, %q, want both %q", slides[0].Title, slides[1].Title, "Combo")
|
||||
}
|
||||
if slides[0].Class != "center" || slides[1].Class != "center" {
|
||||
t.Errorf("slides[0..1].Class = %q, %q, want both %q", slides[0].Class, slides[1].Class, "center")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseDir_NewSlideMarkerIgnoredInsideFencedCodeBlock(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
writeFiles(t, dir, map[string]string{
|
||||
"010-doc.md": "# Doc\n\n```\n<!-- new slide -->\n```\n",
|
||||
})
|
||||
|
||||
slides, err := ParseDir(dir)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseDir() error = %v", err)
|
||||
}
|
||||
|
||||
if len(slides) != 1 {
|
||||
t.Fatalf("len(slides) = %d, want 1 (marker inside fence must not split): %+v", len(slides), slides)
|
||||
}
|
||||
}
|
||||
|
||||
func writeFiles(t *testing.T, dir string, files map[string]string) {
|
||||
t.Helper()
|
||||
for name, content := range files {
|
||||
|
|
|
|||
92
internal/parser/split.go
Normal file
92
internal/parser/split.go
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
package parser
|
||||
|
||||
import "bytes"
|
||||
|
||||
// slideBreakMarker is a line that, on its own, forces a new slide within a
|
||||
// single source file, even though the surrounding content stays in one
|
||||
// file (and shares its frontmatter).
|
||||
var slideBreakMarker = []byte("<!-- new slide -->")
|
||||
|
||||
// splitSlideBreaks splits body into one or more parts at lines consisting
|
||||
// solely of slideBreakMarker. Matches inside fenced code blocks (``` or
|
||||
// ~~~) are ignored, so a marker shown as example text isn't mistaken for an
|
||||
// actual break. If no marker is found, it returns body unchanged as the
|
||||
// only part.
|
||||
func splitSlideBreaks(body []byte) [][]byte {
|
||||
lines := bytes.Split(body, []byte("\n"))
|
||||
|
||||
var parts [][]byte
|
||||
var current [][]byte
|
||||
var fence fenceState
|
||||
|
||||
for _, line := range lines {
|
||||
trimmed := bytes.TrimSpace(line)
|
||||
if !fence.active && bytes.Equal(trimmed, slideBreakMarker) {
|
||||
parts = append(parts, bytes.TrimSpace(bytes.Join(current, []byte("\n"))))
|
||||
current = nil
|
||||
continue
|
||||
}
|
||||
fence.toggle(trimmed)
|
||||
current = append(current, line)
|
||||
}
|
||||
parts = append(parts, bytes.TrimSpace(bytes.Join(current, []byte("\n"))))
|
||||
|
||||
if len(parts) == 1 {
|
||||
return [][]byte{body}
|
||||
}
|
||||
|
||||
nonEmpty := parts[:0]
|
||||
for _, p := range parts {
|
||||
if len(p) > 0 {
|
||||
nonEmpty = append(nonEmpty, p)
|
||||
}
|
||||
}
|
||||
return nonEmpty
|
||||
}
|
||||
|
||||
// fenceState tracks whether the line currently being scanned lies inside a
|
||||
// fenced code block, so a slide-break marker appearing as example text
|
||||
// inside a fence isn't treated as an actual split point.
|
||||
type fenceState struct {
|
||||
active bool
|
||||
char byte
|
||||
count int
|
||||
}
|
||||
|
||||
// toggle updates the fence state for one already-trimmed line.
|
||||
func (f *fenceState) toggle(trimmed []byte) {
|
||||
ch, count, ok := parseFenceLine(trimmed)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
switch {
|
||||
case !f.active:
|
||||
f.active, f.char, f.count = true, ch, count
|
||||
case ch == f.char && count >= f.count:
|
||||
f.active = false
|
||||
}
|
||||
}
|
||||
|
||||
// parseFenceLine reports whether trimmed is a fenced-code-block delimiter
|
||||
// line (a run of three or more backticks or tildes), returning the fence
|
||||
// character and run length.
|
||||
func parseFenceLine(trimmed []byte) (ch byte, count int, ok bool) {
|
||||
if len(trimmed) < 3 {
|
||||
return 0, 0, false
|
||||
}
|
||||
ch = trimmed[0]
|
||||
if ch != '`' && ch != '~' {
|
||||
return 0, 0, false
|
||||
}
|
||||
for count < len(trimmed) && trimmed[count] == ch {
|
||||
count++
|
||||
}
|
||||
if count < 3 {
|
||||
return 0, 0, false
|
||||
}
|
||||
// A backtick fence's info string cannot itself contain a backtick.
|
||||
if ch == '`' && bytes.IndexByte(trimmed[count:], '`') != -1 {
|
||||
return 0, 0, false
|
||||
}
|
||||
return ch, count, true
|
||||
}
|
||||
75
internal/parser/split_test.go
Normal file
75
internal/parser/split_test.go
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
package parser
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSplitSlideBreaks_NoMarkerReturnsBodyUnchanged(t *testing.T) {
|
||||
body := []byte("# A\n\nsome text\n")
|
||||
got := splitSlideBreaks(body)
|
||||
if len(got) != 1 || !bytes.Equal(got[0], body) {
|
||||
t.Errorf("splitSlideBreaks() = %q, want [%q]", got, body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSplitSlideBreaks_SplitsOnMarkerLine(t *testing.T) {
|
||||
body := []byte("# A\n\n<!-- new slide -->\n\n# B\n")
|
||||
got := splitSlideBreaks(body)
|
||||
want := [][]byte{[]byte("# A"), []byte("# B")}
|
||||
if len(got) != len(want) {
|
||||
t.Fatalf("splitSlideBreaks() = %q, want %q", got, want)
|
||||
}
|
||||
for i := range want {
|
||||
if !bytes.Equal(got[i], want[i]) {
|
||||
t.Errorf("part %d = %q, want %q", i, got[i], want[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSplitSlideBreaks_HandlesMultipleMarkersAndSurroundingWhitespace(t *testing.T) {
|
||||
body := []byte("<!-- new slide -->\n# A\n<!-- new slide -->\n# B\n<!-- new slide -->\n")
|
||||
got := splitSlideBreaks(body)
|
||||
want := [][]byte{[]byte("# A"), []byte("# B")}
|
||||
if len(got) != len(want) {
|
||||
t.Fatalf("splitSlideBreaks() = %q, want %q", got, want)
|
||||
}
|
||||
for i := range want {
|
||||
if !bytes.Equal(got[i], want[i]) {
|
||||
t.Errorf("part %d = %q, want %q", i, got[i], want[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSplitSlideBreaks_IgnoresMarkerInsideBacktickFence(t *testing.T) {
|
||||
body := []byte("# A\n\n```\n<!-- new slide -->\n```\n\n# B\n")
|
||||
got := splitSlideBreaks(body)
|
||||
if len(got) != 1 {
|
||||
t.Fatalf("splitSlideBreaks() = %q, want a single part (marker is inside a fence)", got)
|
||||
}
|
||||
if !bytes.Equal(got[0], body) {
|
||||
t.Errorf("splitSlideBreaks()[0] = %q, want unchanged body %q", got[0], body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSplitSlideBreaks_IgnoresMarkerInsideTildeFence(t *testing.T) {
|
||||
body := []byte("# A\n\n~~~\n<!-- new slide -->\n~~~\n\n# B\n")
|
||||
got := splitSlideBreaks(body)
|
||||
if len(got) != 1 {
|
||||
t.Fatalf("splitSlideBreaks() = %q, want a single part (marker is inside a fence)", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSplitSlideBreaks_SplitsAfterFenceCloses(t *testing.T) {
|
||||
body := []byte("```\ncode\n```\n\n<!-- new slide -->\n\n# B\n")
|
||||
got := splitSlideBreaks(body)
|
||||
want := [][]byte{[]byte("```\ncode\n```"), []byte("# B")}
|
||||
if len(got) != len(want) {
|
||||
t.Fatalf("splitSlideBreaks() = %q, want %q", got, want)
|
||||
}
|
||||
for i := range want {
|
||||
if !bytes.Equal(got[i], want[i]) {
|
||||
t.Errorf("part %d = %q, want %q", i, got[i], want[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
2
web/assets/vendor/style.css
vendored
2
web/assets/vendor/style.css
vendored
|
|
@ -161,7 +161,7 @@ code {
|
|||
|
||||
pre {
|
||||
font-family: var(--font-family-mono);
|
||||
font-size: var(--font-size-sm);
|
||||
font-size: 0.9em;
|
||||
background: var(--color-code-bg);
|
||||
padding: var(--spacing-md);
|
||||
border-radius: 0.5em;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue