Patch feature and Write feature.
commit 8ed56c1d8ad05109ba183d2cec2f3f733ed2dc84 · parent 6515cef47b
· browse this tree
Eric Myhre, 2021-08-17 19:17:36 UTC
diff --git a/patch.go b/patch.go new file mode 100644 index 0000000..19b3e32 --- /dev/null +++ b/patch.go @@ -0,0 +1,82 @@ +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) + hunk.BlockTag = newHunk.BlockTag + + // 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 = append(newDoc.Lines, bytes.Join([][]byte{sigilTestmark, []byte(hunk.Name)}, nil)) + newDoc.Lines = append(newDoc.Lines, bytes.Join([][]byte{sigilCodeBlock, []byte(hunk.BlockTag)}, nil)) + newDoc.Lines = append(newDoc.Lines, newBodyLines...) + newDoc.Lines = append(newDoc.Lines, sigilCodeBlock) + 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. + // TODO + + return +} diff --git a/patch_test.go b/patch_test.go new file mode 100644 index 0000000..e3f1153 --- /dev/null +++ b/patch_test.go @@ -0,0 +1,19 @@ +package testmark + +import ( + "path/filepath" + "testing" +) + +func TestPatch(t *testing.T) { + testdata, err := filepath.Abs("testdata") + if err != nil { + panic(err) + } + doc, err := ReadFile(filepath.Join(testdata, "example.md")) + doc = Patch(doc, + Hunk{Name: "more-data", BlockTag: "text", Body: []byte("you have been...\nreplaced.\nand gotten\nrather longer.")}, + Hunk{Name: "this-is-the-data-name", BlockTag: "", Body: []byte("this one gets shorter.")}, + ) + t.Logf("%s", doc.String()) +} diff --git a/read.go b/read.go index a489ec5..808fee1 100644 --- a/read.go +++ b/read.go @@ -38,7 +38,7 @@ func Parse(data []byte) (*Document, error) { } // Markdown can be effectively parsed line by line. - doc.OriginalLines = bytes.Split(data, sigilLineBreak) + 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. @@ -48,8 +48,8 @@ func Parse(data []byte) (*Document, error) { var inCodeBlock bool var expectCodeBlock bool var codeBlockOffset int - hunkInProgress := DocHunk{Line: -1} - for i, line := range doc.OriginalLines { + 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 { @@ -60,12 +60,12 @@ func Parse(data []byte) (*Document, error) { } expectCodeBlock = false case true: // ending a block - if hunkInProgress.Line > -1 { - hunkInProgress.EndLine = i + 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{Line: -1} + hunkInProgress = DocHunk{LineStart: -1} } } inCodeBlock = !inCodeBlock @@ -93,10 +93,10 @@ func Parse(data []byte) (*Document, error) { // ... Log a complaint? Empty block names are very silly. } if already, exists := doc.HunksByName[name]; exists { - return &doc, fmt.Errorf("repeated testmark hunk name %q, first seen on line %d, and again on line %d", name, already.Line+1, i+1) + 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.Line = i + hunkInProgress.LineStart = i hunkInProgress.Name = name } // Any other text? It's prose. No action. diff --git a/read_test.go b/read_test.go index 47e9138..d06481f 100644 --- a/read_test.go +++ b/read_test.go @@ -13,6 +13,6 @@ func TestRead(t *testing.T) { } doc, err := ReadFile(filepath.Join(testdata, "example.md")) for _, hunk := range doc.DataHunks { - fmt.Printf("hunk %q is on lines %d:%d, has body %q\n", hunk.Name, hunk.Line+1, hunk.EndLine+1, string(hunk.Body)) + fmt.Printf("hunk %q is on lines %d:%d, has body %q\n", hunk.Name, hunk.LineStart+1, hunk.LineEnd+1, string(hunk.Body)) } } diff --git a/testdata/example.md b/testdata/example.md index f2742b0..b81d5b3 100644 --- a/testdata/example.md +++ b/testdata/example.md @@ -71,3 +71,5 @@ you can't easily describe data that doesn't have a trailing linebreak. ``` A markdown codeblock always has a trailing linebreak before its close indicator, you see. ``` + +That's a problem in many formats though, frankly. diff --git a/testmark.go b/testmark.go index 11f9d43..d6ad9eb 100644 --- a/testmark.go +++ b/testmark.go @@ -4,14 +4,16 @@ type Document struct { // The whole thing, complete, but split into lines. // We always save this, because if we are going to write this document back out, // it's going to be by patching this. (We don't try to understand, much less normalize, a full markdown AST!) + // May be nil if this document is the result of Patch operations rather than Parse. Original []byte - // Original, sliced into lines. Shares backing memory with Original. + // The document, sliced into lines. Shares backing memory with Original, if Original is non-nil. // Useful because we made it during parse anyway, and it can save us a lot of work during edits. - OriginalLines [][]byte + Lines [][]byte // Each data hunk. // Contains just offset information, and the parsed name header. + // Is in order of hunk appearance. DataHunks []DocHunk // Like it says on the tin. @@ -24,10 +26,10 @@ type DocHunk struct { // The code block indicator is necessarily is the following line, // and the code block body one line after that. // N.B. zero-indexed. You probably want to +1 before printing to a human. - Line int + LineStart int // Index into Document.OriginalLines that contains the closing code block indicator. - EndLine int + LineEnd int Hunk } diff --git a/write.go b/write.go new file mode 100644 index 0000000..0a6856e --- /dev/null +++ b/write.go @@ -0,0 +1,29 @@ +package testmark + +import ( + "io" + "strings" +) + +func (d Document) String() string { + var sb strings.Builder + Write(&d, &sb) + return sb.String() +} + +func Write(doc *Document, wr io.Writer) (int, error) { + n := 0 + for _, line := range doc.Lines { + if n2, err := wr.Write(line); err != nil { + return n + n2, err + } else { + n += n2 + } + if n2, err := wr.Write(sigilLineBreak); err != nil { + return n + n2, err + } else { + n += n2 + } + } + return n, nil +}