patch.go
package testmark
import (
"bytes"
"unicode"
)
// NewHunk creates a new hunk with the given name, info string, and body.
func NewHunk(name, infoString string, body []byte) *Hunk {
if name == "" || bytes.IndexFunc([]byte(name), unicode.IsSpace) >= 0 {
panic("hunk name must not be empty and cannot contain whitespace")
}
return &Hunk{
name: name,
infoString: infoString,
body: body,
}
}
// NewHunkString creates a new hunk with a string body.
func NewHunkString(name, infoString, body string) *Hunk {
return NewHunk(name, infoString, []byte(body))
}
// Patch applies the given hunks to the document and returns a new document.
func (d *Document) Patch(hunks ...*Hunk) *Document {
// Create hunk map for efficient lookup
newHunks := make(map[string]*Hunk, len(hunks))
for _, hunk := range hunks {
newHunks[hunk.name] = hunk
}
// Create new document structure
newDoc := &Document{
hunksByName: make(map[string]*Hunk),
}
// Build new content line by line
var newLines [][]byte
var leftOff int
// Process existing hunks
for _, info := range d.hunks {
// Copy prose lines before this hunk
newLines = append(newLines, d.lines[leftOff:info.lineStart]...)
leftOff = info.lineEnd + 1
// Determine what content to use
var bodyLines [][]byte
var infoString string
var hunkToUse *Hunk
if newHunk, exists := newHunks[info.hunk.name]; exists {
// Use new hunk content
bodyLines = bytes.Split(newHunk.body, sigilLineBreak)
if len(newHunk.body) > 0 && newHunk.body[len(newHunk.body)-1] == '\n' {
bodyLines = bodyLines[0 : len(bodyLines)-1]
}
infoString = newHunk.infoString
hunkToUse = newHunk
delete(newHunks, info.hunk.name) // Mark as processed
} else {
// Keep existing content
bodyLines = d.lines[info.lineStart+2 : info.lineEnd]
infoString = info.hunk.infoString
hunkToUse = info.hunk
}
// Add hunk lines
newLineStart := len(newLines)
newLines = appendHunkLines(newLines, hunkToUse.name, infoString, bodyLines)
newLineEnd := len(newLines)
// Store in new document
newInfo := hunkInfo{
lineStart: newLineStart,
lineEnd: newLineEnd - 1,
hunk: hunkToUse,
}
newDoc.hunks = append(newDoc.hunks, newInfo)
newDoc.hunksByName[hunkToUse.name] = hunkToUse
}
// Copy remaining prose lines
newLines = append(newLines, d.lines[leftOff:]...)
// Add any completely new hunks at the end
for _, hunk := range hunks {
if _, stillNew := newHunks[hunk.name]; stillNew {
// Add blank line if needed
if len(newLines) > 0 && len(newLines[len(newLines)-1]) > 0 {
newLines = append(newLines, []byte{})
}
// Add the new hunk
newLineStart := len(newLines)
bodyLines := bytes.Split(hunk.body, sigilLineBreak)
if len(hunk.body) > 0 && hunk.body[len(hunk.body)-1] == '\n' {
bodyLines = bodyLines[0 : len(bodyLines)-1]
}
newLines = appendHunkLines(newLines, hunk.name, hunk.infoString, bodyLines)
newLineEnd := len(newLines)
// Store in new document
newInfo := hunkInfo{
lineStart: newLineStart,
lineEnd: newLineEnd - 1,
hunk: hunk,
}
newDoc.hunks = append(newDoc.hunks, newInfo)
newDoc.hunksByName[hunk.name] = hunk
// Add trailing blank line
newLines = append(newLines, []byte{})
}
}
// Clean up multiple trailing blank lines
for len(newLines) > 1 && len(newLines[len(newLines)-1]) == 0 && len(newLines[len(newLines)-2]) == 0 {
newLines = newLines[:len(newLines)-1]
}
// Rebuild document content
newDoc.lines = newLines
newDoc.original = bytes.Join(newLines, sigilLineBreak)
// Build directory index
buildDirIndex(newDoc)
return newDoc
}
// appendHunkLines adds the testmark formatting around hunk content
func appendHunkLines(lines [][]byte, hunkName string, infoString string, bodyLines [][]byte) [][]byte {
// Add comment line
commentLine := append(sigilTestmark, append([]byte("("), append([]byte(hunkName), ')')...)...)
lines = append(lines, commentLine)
// Add code block start
codeBlockStart := append(sigilCodeBlock, []byte(infoString)...)
lines = append(lines, codeBlockStart)
// Add body lines
lines = append(lines, bodyLines...)
// Add code block end
lines = append(lines, sigilCodeBlock)
return lines
}
// PatchAccumulator methods
// Add appends a hunk to the patch list.
func (pa *PatchAccumulator) Add(hunk *Hunk) {
pa.patches = append(pa.patches, *hunk)
}
// Count returns the number of accumulated patches.
func (pa *PatchAccumulator) Count() int {
return len(pa.patches)
}
// ApplyTo applies all accumulated patches to a document.
func (pa *PatchAccumulator) ApplyTo(doc *Document) *Document {
patches := make([]*Hunk, len(pa.patches))
for i, p := range pa.patches {
copy := p
patches[i] = ©
}
return doc.Patch(patches...)
}
v2/patch.go · 4.7 KiB · 13b63c6f85