92 lines
2.5 KiB
Go
92 lines
2.5 KiB
Go
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
|
|
}
|