coverage~bigbes/sr-ht-spec64cae3afprosediff/prosediff.go

Coverage
100.0% 26/26 statements
Δ
+0.0
Blob
e0651d6
Uncovered nothing — every instrumented line ran
1 // Package prosediff diffs two revisions of a markdown document the way a
2 // human reads it: as blocks of prose, not as lines of text.
3 //
4 // Markdown reflows. A one-word edit rewraps a paragraph, and a line-oriented
5 // differ then reports the whole paragraph as replaced — useless for reviewing
6 // written text. This package instead:
7 //
8 // 1. segments each revision into blocks (headings, paragraphs, list items,
9 // code fences, table rows, block quotes) using goldmark's parser;
10 // 2. aligns the two block sequences, recognising unchanged, added, removed,
11 // modified and (exactly) moved blocks;
12 // 3. diffs word-by-word inside a modified prose block, and line-by-line
13 // inside a modified code fence.
14 //
15 // Whitespace and line wrapping alone never produce a diff in prose. They
16 // always do in code fences, which is the point of separating the two.
17 //
18 // The package renders plain text only. Rendering to HTML is the web layer's
19 // job; Diff is the data structure it consumes.
20 package prosediff
21
22 // ChangeKind classifies what happened to one block.
23 type ChangeKind string
24
25 const (
26 // ChangeEqual: the block is present unchanged in both revisions.
27 ChangeEqual ChangeKind = "equal"
28 // ChangeInsert: the block exists only in the new revision.
29 ChangeInsert ChangeKind = "insert"
30 // ChangeDelete: the block exists only in the old revision.
31 ChangeDelete ChangeKind = "delete"
32 // ChangeModify: the same block, edited. Words (or Lines) carry the detail.
33 ChangeModify ChangeKind = "modify"
34 // ChangeMoveOut marks, at its old position, a block that moved elsewhere.
35 ChangeMoveOut ChangeKind = "move_out"
36 // ChangeMoveIn marks, at its new position, a block that moved from elsewhere.
37 ChangeMoveIn ChangeKind = "move_in"
38 )
39
40 // BlockChange is one entry of a diff, in reading order: for each changed
41 // region, the old blocks first and then the new ones.
42 type BlockChange struct {
43 Kind ChangeKind
44
45 // Old is the block in the old revision; nil for ChangeInsert.
46 Old *Block
47 // New is the block in the new revision; nil for ChangeDelete.
48 New *Block
49
50 // Words is the inline edit script for a modified prose block.
51 Words []Span
52 // Lines is the line-oriented edit script for a modified code fence,
53 // frontmatter block or HTML block.
54 Lines []Span
55
56 // Similarity is the token-level similarity that justified pairing a
57 // ChangeModify, in [0,1]. Zero for every other kind.
58 Similarity float64
59
60 // StructureOnly marks a modification whose content is untouched: only
61 // the heading level, list depth or quote depth changed.
62 StructureOnly bool
63
64 // Moved marks a ChangeModify whose block also changed position: it sits
65 // inside a run of moved blocks. Its ChangeMoveOut counterpart appears at
66 // the old position.
67 Moved bool
68 }
69
70 // Stats summarizes a diff, cheap enough for a listing page.
71 type Stats struct {
72 BlocksEqual int
73 BlocksInserted int
74 BlocksDeleted int
75 BlocksModified int
76 BlocksMoved int
77 WordsInserted int
78 WordsDeleted int
79 }
80
81 // Changed reports whether the two revisions differ at all.
82 2 func (s Stats) Changed() bool {
83 2 return s.BlocksInserted+s.BlocksDeleted+s.BlocksModified+s.BlocksMoved > 0
84 2 }
85
86 // Diff is the whole comparison of two document revisions.
87 type Diff struct {
88 OldBlocks []Block
89 NewBlocks []Block
90 Changes []BlockChange
91 Stats Stats
92 }
93
94 // Compare segments both revisions and aligns them. It has no failure mode:
95 // any byte slice is a parseable markdown document.
96 32 func Compare(oldSrc, newSrc []byte) *Diff {
97 32 old := Segment(oldSrc)
98 32 nw := Segment(newSrc)
99 32 d := &Diff{OldBlocks: old, NewBlocks: nw}
100 32 d.Changes = align(old, nw)
101 32 d.Stats = computeStats(d.Changes)
102 32 return d
103 32 }
104
105 32 func computeStats(changes []BlockChange) Stats {
106 32 var s Stats
107 96 for _, c := range changes {
108 96 switch c.Kind {
109 50 case ChangeEqual:
110 50 s.BlocksEqual++
111 11 case ChangeInsert:
112 11 s.BlocksInserted++
113 11 s.WordsInserted += len(Tokenize(c.New.Text))
114 5 case ChangeDelete:
115 5 s.BlocksDeleted++
116 5 s.WordsDeleted += len(Tokenize(c.Old.Text))
117 19 case ChangeModify:
118 19 s.BlocksModified++
119 54 for _, sp := range c.Words {
120 54 switch sp.Op {
121 14 case OpInsert:
122 14 s.WordsInserted += len(Tokenize(sp.Text))
123 13 case OpDelete:
124 13 s.WordsDeleted += len(Tokenize(sp.Text))
125 }
126 }
127 19 for _, sp := range c.Lines {
128 15 switch sp.Op {
129 4 case OpInsert:
130 4 s.WordsInserted += len(Tokenize(sp.Text))
131 4 case OpDelete:
132 4 s.WordsDeleted += len(Tokenize(sp.Text))
133 }
134 }
135 5 case ChangeMoveIn:
136 5 s.BlocksMoved++
137 }
138 }
139 32 return s
140 }