patch.go
package testmark
import (
"bytes"
"unicode"
)
func Patch(oldDoc *Document, hunks ...Hunk) (newDoc *Document) {
// First pool up the hunk names we've been asked to patch.
// We want to go over things in the order already present in the document,
// so the order our varargs came in is not relevant nor helpful.
//
// Also, validate them real quick.
// Empty names and names with whitespace are unacceptable.
newHunks := make(map[string]Hunk, len(hunks))
for _, hunk := range hunks {
if hunk.Name == "" || bytes.IndexFunc([]byte(hunk.Name), unicode.IsSpace) >= 0 {
panic("hunk name must not be empty and cannot contain whitespace")
}
newHunks[hunk.Name] = hunk
}
// Mutation is bad.
// Immediately start making a new document.
// Prep it with about the same amount of memory as the old one.
newDoc = &Document{
Lines: make([][]byte, 0, len(oldDoc.Lines)),
DataHunks: make([]DocHunk, 0, len(oldDoc.DataHunks)),
HunksByName: make(map[string]DocHunk, len(oldDoc.HunksByName)),
}
// Range over the document and apply patches.
// We'll build up a whole new document as we go (byte slices and all!).
var leftOff int
for _, hunk := range oldDoc.DataHunks {
// Copy any prose lines from wherever we left off, up to the start of the new hunk.
// And advance the marker for leftOff marker to past the end of the old hunk.
newDoc.Lines = append(newDoc.Lines, oldDoc.Lines[leftOff:hunk.LineStart]...)
leftOff = hunk.LineEnd + 1
var newBodyLines [][]byte
if newHunk, exists := newHunks[hunk.Name]; exists {
// Split our new hunk's body into lines, ready to append to the total content lines.
// The rest... copy it into 'hunk', actually, it's a local variable and it makes the code slightly more DRY.
newBodyLines = bytes.Split(newHunk.Body, sigilLineBreak)
// If the last byte was a linebreak, the split will tend to exaggerate it a bit, so let's trim that back down.
if len(newHunk.Body) > 0 && newHunk.Body[len(newHunk.Body)-1] == '\n' {
newBodyLines = newBodyLines[0 : len(newBodyLines)-1]
}
hunk.InfoString = newHunk.InfoString
// Yeet from newHunks, as it's now handled.
delete(newHunks, hunk.Name)
} else {
// Just... keep the old lines, which we can sub-slice back out of the old document.
newBodyLines = oldDoc.Lines[hunk.LineStart+2 : hunk.LineEnd]
}
// Append the hunk framing, and the body lines.
// Watch how this changes the offsets, so we can build a new DocHunk with info that's correct.
// (If you're just going to serialize this, it wouldn't matter, but if you want to patch multiple times, it matters.)
newLineStart := len(newDoc.Lines)
newDoc.Lines = appendHunkLines(newDoc.Lines, hunk.Name, hunk.InfoString, newBodyLines)
newLineEnd := len(newDoc.Lines)
docHunk := DocHunk{
LineStart: newLineStart,
LineEnd: newLineEnd,
Hunk: hunk.Hunk,
}
// Append the updated hunk info to newDoc.
newDoc.DataHunks = append(newDoc.DataHunks, docHunk)
newDoc.HunksByName[hunk.Name] = docHunk
}
// Copy any remaining trailing prose lines.
newDoc.Lines = append(newDoc.Lines, oldDoc.Lines[leftOff:]...)
// Now for any hunks we have left... We'll just stick them on the end, I guess.
// And *now* the dang order of our original args matters. We wouldn't want this to be randomized.
for _, hunk := range hunks {
// If it was already done, skip it.
if _, stillTodo := newHunks[hunk.Name]; !stillTodo {
continue
}
// If we're about to need to append something, make sure there's at least one blank line first.
if len(newDoc.Lines[len(newDoc.Lines)-1]) > 0 {
newDoc.Lines = append(newDoc.Lines, []byte{})
}
// Append it.
newDoc.Lines = appendHunkLines(newDoc.Lines, hunk.Name, hunk.InfoString, bytes.Split(hunk.Body, sigilLineBreak))
// And one more trailing line, at the end.
newDoc.Lines = append(newDoc.Lines, []byte{})
}
// If we've ended up with multiple trailing blank lines, that's usually considered undesirable,
// so let's nip those back down.
l := len(newDoc.Lines)
if l > 1 && len(newDoc.Lines[l-1]) == 0 {
newDoc.Lines = newDoc.Lines[0 : l-1]
}
return
}
func appendHunkLines(lines [][]byte, hunkName string, hunkBlockTag string, hunkBodyLines [][]byte) [][]byte {
lines = append(lines, bytes.Join([][]byte{sigilTestmark, {'('}, []byte(hunkName), {')'}}, nil))
lines = append(lines, bytes.Join([][]byte{sigilCodeBlock, []byte(hunkBlockTag)}, nil))
lines = append(lines, hunkBodyLines...)
lines = append(lines, sigilCodeBlock)
return lines
}
type PatchAccumulator struct {
Patches []Hunk
}
func (pa *PatchAccumulator) AppendPatchIfBodyDiffers(hunk Hunk, newBody []byte) {
if !bytes.Equal(hunk.Body, newBody) {
hunk.Body = newBody
pa.AppendPatch(hunk)
}
}
func (pa *PatchAccumulator) AppendPatch(hunk Hunk) {
if pa.Patches == nil {
pa.Patches = make([]Hunk, 0)
}
pa.Patches = append(pa.Patches, hunk)
}
patch.go · 4.8 KiB · b9084569f7