package web
import (
"fmt"
"html/template"
"strings"
"sourcecraft.dev/bigbes/sr-ht-spec/prosediff"
)
// inlineSimilarityThreshold is the Phase 0 verdict's presentation switch: a
// modified prose block whose token similarity is at or above it renders as an
// inline word diff, and one below it renders as a two-column old/new view.
//
// 13% of real prose modifications shred into interleaved fragments — those
// paragraphs really were rewritten sentence by sentence — and every one of them
// scores at or below 0.73. Rendering them inline makes one review in eight
// unreadable, which is the one where the agent changed the most. prosediff
// exports BlockChange.Similarity for exactly this decision and computes no HTML
// itself; this is where the decision is made.
const inlineSimilarityThreshold = 0.75
// diffView is the whole rendered diff of one document, ready for the proposal
// template. Unchanged reports the degenerate case — a proposal that touches a
// document without changing it — so the page can say so rather than show an
// empty diff.
type diffView struct {
HTML template.HTML
Stats prosediff.Stats
Unchanged bool
}
// renderDocDiff diffs the approved (old) and proposed (new) source of one
// document and renders it to HTML.
//
// It walks prosediff's block-change model rather than its text renderer: the
// text renderer is for a terminal, and the review page needs headings grouped,
// word edits marked with <ins>/<del>, and — the load-bearing part — the
// two-column fallback for shredded blocks. Every piece of document content is
// HTML-escaped before it reaches the output; the only markup this produces is
// its own structure.
func renderDocDiff(oldSrc, newSrc []byte) diffView {
d := prosediff.Compare(oldSrc, newSrc)
view := diffView{Stats: d.Stats, Unchanged: !d.Stats.Changed()}
if view.Unchanged {
return view
}
var b strings.Builder
b.WriteString(`<div class="prosediff">`)
lastPath := "\x00" // impossible path, so the first real one always prints
inHunk := false
closeHunk := func() {
if inHunk {
b.WriteString(`</div></section>`)
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(`<section class="ph-hunk"><div class="ph-path">`)
b.WriteString(template.HTMLEscapeString(shown))
b.WriteString(`</div><div class="ph-blocks">`)
inHunk = true
}
writeBlock(&b, c)
}
closeHunk()
b.WriteString(`</div>`)
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, `<div class="ph-block ph-move"><span class="ph-label">%s moved here (was line %d)</span></div>`,
template.HTMLEscapeString(c.New.Label()), c.Old.StartLine)
case prosediff.ChangeMoveOut:
fmt.Fprintf(b, `<div class="ph-block ph-move"><span class="ph-label">%s moved away (now line %d)</span></div>`,
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
// <pre> 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, `<div class="ph-block %s"><span class="ph-label">%s</span>`,
class, template.HTMLEscapeString(label))
writeBody(b, blk.Text, blk.Kind.Prose())
b.WriteString(`</div>`)
}
// 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, `<div class="ph-block ph-modify"><span class="ph-label">%s</span><pre class="ph-code">`,
template.HTMLEscapeString(label))
writeLineSpans(b, c.Lines)
b.WriteString(`</pre></div>`)
return
}
if c.Similarity >= inlineSimilarityThreshold {
fmt.Fprintf(b, `<div class="ph-block ph-modify"><span class="ph-label">%s</span><div class="ph-body ph-inline">`,
template.HTMLEscapeString(label))
writeInlineSpans(b, c.Words)
b.WriteString(`</div></div>`)
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, `<div class="ph-block ph-modify ph-columns"><span class="ph-label">%s (rewritten)</span><div class="ph-cols"><div class="ph-col ph-old">`,
template.HTMLEscapeString(label))
writeColumnSpans(b, c.Words, true)
b.WriteString(`</div><div class="ph-col ph-new">`)
writeColumnSpans(b, c.Words, false)
b.WriteString(`</div></div></div>`)
}
// writeBody renders whole-block text: escaped, in a <pre> when it is not prose.
func writeBody(b *strings.Builder, text string, prose bool) {
if prose {
b.WriteString(`<div class="ph-body">`)
b.WriteString(template.HTMLEscapeString(text))
b.WriteString(`</div>`)
return
}
b.WriteString(`<pre class="ph-code">`)
b.WriteString(template.HTMLEscapeString(text))
b.WriteString(`</pre>`)
}
// 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 <del> or <ins> 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("<del>")
b.WriteString(esc)
b.WriteString("</del>")
case prosediff.OpInsert:
b.WriteString("<ins>")
b.WriteString(esc)
b.WriteString("</ins>")
default:
b.WriteString(esc)
}
}
// writeLineSpans renders a line-oriented edit script (a code fence, frontmatter
// or HTML block) one line per row inside a <pre>, 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, `<span class="%s">%s</span>`+"\n", class, template.HTMLEscapeString(s.Text))
}
}