read.go
package testmark
import (
"bytes"
"fmt"
"io"
"io/ioutil"
"os"
"unicode"
)
func Read(r io.Reader) (*Document, error) {
data, err := ioutil.ReadAll(r)
if err != nil {
return nil, err
}
return Parse(data)
}
func ReadFile(name string) (*Document, error) {
data, err := os.ReadFile(name)
if err != nil {
return nil, err
}
return Parse(data)
}
var (
sigilLineBreak = []byte{'\n'}
sigilCodeBlock = []byte("```")
sigilTestmark = []byte("[testmark]:# ")
)
func Parse(data []byte) (*Document, error) {
doc := Document{
Original: data,
HunksByName: make(map[string]DocHunk),
}
// Markdown can be effectively parsed line by line.
doc.Lines = bytes.Split(data, sigilLineBreak)
// The first layer of our parse is "is this in a code block".
// Code blocks are the only feature of markdown that meaningfully changes what mode you're in at the start of a line.
// After that, we look for our magic prefix (on any lines that *aren't* already in a code block).
// Then, the rest is... pretty straightforward.
var offset int
var inCodeBlock bool
var expectCodeBlock bool
var codeBlockOffset int
hunkInProgress := DocHunk{LineStart: -1}
for i, line := range doc.Lines {
// Check for transition in or out of codeblock.
if bytes.HasPrefix(line, sigilCodeBlock) {
switch inCodeBlock {
case false: // starting a block
if expectCodeBlock {
hunkInProgress.BlockTag = string(line[len(sigilCodeBlock):])
codeBlockOffset = offset + len(line) + 1
}
expectCodeBlock = false
case true: // ending a block
if hunkInProgress.LineStart > -1 {
hunkInProgress.LineEnd = i
hunkInProgress.Body = doc.Original[codeBlockOffset:offset]
doc.DataHunks = append(doc.DataHunks, hunkInProgress)
doc.HunksByName[hunkInProgress.Name] = hunkInProgress
hunkInProgress = DocHunk{LineStart: -1}
}
}
inCodeBlock = !inCodeBlock
goto next
}
// If we're in a code block, just fly by.
if inCodeBlock {
goto next
}
// If we were expecting a code block just now, we didn't get it.
if expectCodeBlock {
// ... Log a complaint? I don't think halting with a parse error would be helpful.
// But definitely don't wait around for arbitrarily distant code blocks.
expectCodeBlock = false
}
// Look for testmark block indicators.
if bytes.HasPrefix(line, sigilTestmark) {
remainder := line[len(sigilTestmark):]
nameEnd := bytes.IndexFunc(remainder, unicode.IsSpace)
if nameEnd < 0 {
nameEnd = len(remainder)
}
name := string(remainder[0:nameEnd])
if len(name) == 0 {
// ... Log a complaint? Empty block names are very silly.
}
if already, exists := doc.HunksByName[name]; exists {
// You can actually ignore this error, and things will even still mostly work. HunksByName will only look up the first occurence, and Patch will change only the first occurence, and that is weird, but perhaps fine.
return &doc, fmt.Errorf("repeated testmark hunk name %q, first seen on line %d, and again on line %d", name, already.LineStart+1, i+1)
}
expectCodeBlock = true
hunkInProgress.LineStart = i
hunkInProgress.Name = name
}
// Any other text? It's prose. No action.
next:
// Track total offset, so we can use it to subslice out document hunks.
// Mind: this is going to be off by one at the very end of the file... but that turns out never to matter to us.
offset += len(line) + 1
}
return &doc, nil
}
read.go · 3.4 KiB · 2d2d4c2b79