`)
lastPath := "\x00" // impossible path, so the first real one always prints
inHunk := false
closeHunk := func() {
if inHunk {
b.WriteString(`
`)
inHunk = false
}
}
for _, c := range d.Changes {
if c.Kind == prosediff.ChangeEqual {
continue // the review shows only what changed
}
blk := c.New
if blk == nil {
blk = c.Old
}
if path := strings.Join(blk.HeadingPath, " › "); path != lastPath {
closeHunk()
lastPath = path
shown := path
if shown == "" {
shown = "(document preamble)"
}
b.WriteString(``)
b.WriteString(template.HTMLEscapeString(shown))
b.WriteString(`
`)
inHunk = true
}
writeBlock(&b, c)
}
closeHunk()
b.WriteString(`
`)
view.HTML = template.HTML(b.String())
return view
}
// writeBlock renders one changed block. Insert/delete/move show the whole
// block; a modify chooses among a code line diff, an inline word diff and the
// two-column view, on the rules the design pins.
func writeBlock(b *strings.Builder, c prosediff.BlockChange) {
switch c.Kind {
case prosediff.ChangeInsert:
writeWholeBlock(b, "ph-insert", "added", c.New)
case prosediff.ChangeDelete:
writeWholeBlock(b, "ph-delete", "removed", c.Old)
case prosediff.ChangeMoveIn:
fmt.Fprintf(b, `%s moved here (was line %d)
`,
template.HTMLEscapeString(c.New.Label()), c.Old.StartLine)
case prosediff.ChangeMoveOut:
fmt.Fprintf(b, `%s moved away (now line %d)
`,
template.HTMLEscapeString(c.Old.Label()), c.New.StartLine)
case prosediff.ChangeModify:
writeModify(b, c)
}
}
// writeWholeBlock renders an inserted or deleted block: its whole text, in a
// for a non-prose kind so code keeps its wrapping, and reflowed prose
// otherwise.
func writeWholeBlock(b *strings.Builder, class, verb string, blk *prosediff.Block) {
label := verb + " " + blk.Label()
fmt.Fprintf(b, `%s`,
class, template.HTMLEscapeString(label))
writeBody(b, blk.Text, blk.Kind.Prose())
b.WriteString(`
`)
}
// writeModify renders a modified block. A code, frontmatter or HTML block has a
// line-oriented edit script and renders line by line; a prose block has a word
// edit script and renders inline when it stayed similar enough to follow, and
// two-column when it did not.
func writeModify(b *strings.Builder, c prosediff.BlockChange) {
label := "changed " + c.New.Label()
if c.StructureOnly {
label = fmt.Sprintf("changed %s → %s (structure)", c.Old.Label(), c.New.Label())
}
if c.Moved {
label += fmt.Sprintf(" (moved from line %d)", c.Old.StartLine)
}
if len(c.Lines) > 0 {
fmt.Fprintf(b, `%s`,
template.HTMLEscapeString(label))
writeLineSpans(b, c.Lines)
b.WriteString(`
`)
return
}
if c.Similarity >= inlineSimilarityThreshold {
fmt.Fprintf(b, `%s`,
template.HTMLEscapeString(label))
writeInlineSpans(b, c.Words)
b.WriteString(`
`)
return
}
// The two-column fallback: the block was rewritten enough that inline marks
// would shred it. The old column keeps deletions, the new keeps insertions,
// each still marked, so a reviewer reads two coherent paragraphs side by side.
fmt.Fprintf(b, `%s (rewritten)`,
template.HTMLEscapeString(label))
writeColumnSpans(b, c.Words, true)
b.WriteString(`
`)
writeColumnSpans(b, c.Words, false)
b.WriteString(`
`)
}
// writeBody renders whole-block text: escaped, in a when it is not prose.
func writeBody(b *strings.Builder, text string, prose bool) {
if prose {
b.WriteString(``)
b.WriteString(template.HTMLEscapeString(text))
b.WriteString(`
`)
return
}
b.WriteString(``)
b.WriteString(template.HTMLEscapeString(text))
b.WriteString(`
`)
}
// writeInlineSpans renders a word edit script inline, marking deletions and
// insertions where they sit. The Space flag reproduces prosediff's own spacing:
// a span that replaces the one before it carries Space=false, so a one-word
// substitution does not render with a gap in the middle.
func writeInlineSpans(b *strings.Builder, spans []prosediff.Span) {
emitted := false
for _, s := range spans {
if s.Space && emitted {
b.WriteByte(' ')
}
emitted = true
writeSpan(b, s)
}
}
// writeColumnSpans renders one side of the two-column view: the old side keeps
// equal and deleted words, the new side keeps equal and inserted ones. Both
// still mark their changes, so each column is a readable paragraph that also
// shows what moved.
func writeColumnSpans(b *strings.Builder, spans []prosediff.Span, old bool) {
emitted := false
for _, s := range spans {
keep := s.Op == prosediff.OpEqual ||
(old && s.Op == prosediff.OpDelete) ||
(!old && s.Op == prosediff.OpInsert)
if !keep {
continue
}
if s.Space && emitted {
b.WriteByte(' ')
}
emitted = true
writeSpan(b, s)
}
}
// writeSpan writes one span's escaped text, wrapped in or for a
// change and bare for an equal run.
func writeSpan(b *strings.Builder, s prosediff.Span) {
esc := template.HTMLEscapeString(s.Text)
switch s.Op {
case prosediff.OpDelete:
b.WriteString("")
b.WriteString(esc)
b.WriteString("")
case prosediff.OpInsert:
b.WriteString("")
b.WriteString(esc)
b.WriteString("")
default:
b.WriteString(esc)
}
}
// writeLineSpans renders a line-oriented edit script (a code fence, frontmatter
// or HTML block) one line per row inside a , each line class-marked.
func writeLineSpans(b *strings.Builder, spans []prosediff.Span) {
for _, s := range spans {
class := "ph-line-eq"
switch s.Op {
case prosediff.OpDelete:
class = "ph-line-del"
case prosediff.OpInsert:
class = "ph-line-ins"
}
fmt.Fprintf(b, `%s`+"\n", class, template.HTMLEscapeString(s.Text))
}
}