package render import ( "fmt" "html" "strings" "git.pablomurad.com/pablo/patchwork/internal/diff" "git.pablomurad.com/pablo/patchwork/internal/doc" ) // Kind labels shown to readers. var kindLabel = map[string]string{ "correction": "Correction", "expansion": "Expansion", "position": "Change of position", "reorganization": "Reorganization", } // EntryTitle is a feed entry's title: what kind of revision, of which page. func EntryTitle(r Revision) string { if l, ok := kindLabel[r.Kind]; ok { return l + ": " + r.Page.Title } return "Revised: " + r.Page.Title } // EntryHTML is a feed entry's content: the note, a summary line, the // excerpt, and links. Feed readers strip styles, so the diff relies on what // and mean, and every block is a plain paragraph. func EntryHTML(r Revision) string { var b strings.Builder if r.Note != "" { fmt.Fprintf(&b, "

%s

\n", esc(r.Note)) } var facts []string if l, ok := kindLabel[r.Kind]; ok { facts = append(facts, l) } if r.Maturity != "" { facts = append(facts, "maturity: "+r.Maturity) } facts = append(facts, fmt.Sprintf("+%d / −%d words", r.Stats.WordsAdded, r.Stats.WordsRemoved)) fmt.Fprintf(&b, "

%s

\n", esc(strings.Join(facts, " · "))) for _, m := range r.MetaChanges { if m.Key == "maturity" { continue // already in the summary line } fmt.Fprintf(&b, "

%s: %s %s

\n", esc(m.Key), esc(metaText(m.Old)), esc(metaText(m.New))) } b.WriteString("
\n") for _, op := range r.Excerpt.Blocks { b.WriteString(opHTML(op)) } if r.Excerpt.Truncated { b.WriteString("

…

\n") } fmt.Fprintf(&b, "

Read version %d · Current page · History

\n", esc(r.VersionURL), r.Number, esc(r.Page.URL), esc(r.HistoryURL)) return b.String() } func opHTML(op diff.Op) string { wrap := func(inner string) string { switch op.Kind { case doc.KindCode, doc.KindTable: return "
" + inner + "
\n" case doc.KindHeading: return "

" + inner + "

\n" case doc.KindQuote: return "
" + inner + "
\n" case doc.KindListItem: return "\n" } return "

" + inner + "

\n" } switch op.Op { case diff.OpGap: if op.Removed { return fmt.Sprintf("

[%d more removed]

\n", op.Count) } return fmt.Sprintf("

[%d unchanged]

\n", op.Count) case diff.OpEq: return wrap(esc(op.Text)) case diff.OpInsert: return wrap("" + esc(op.Text) + "") case diff.OpDelete: return wrap("" + esc(op.Text) + "") case diff.OpMove: return "

[moved]

\n" + wrap(esc(op.Text)) case diff.OpModify: if op.Replaced { // Rewritten block: old and new side by side would run together. var out strings.Builder for _, s := range op.Segments { tag := "ins" if s.T == diff.SegDel { tag = "del" } out.WriteString(wrap("<" + tag + ">" + esc(s.S) + "")) } return out.String() } var in strings.Builder for _, s := range op.Segments { switch s.T { case diff.SegDel: in.WriteString("" + esc(s.S) + "") case diff.SegIns: in.WriteString("" + esc(s.S) + "") default: in.WriteString(esc(s.S)) } } return wrap(in.String()) } return "" } func esc(s string) string { return html.EscapeString(s) } func metaText(v any) string { switch t := v.(type) { case nil: return "(none)" case []any: parts := make([]string, len(t)) for i, x := range t { parts[i] = fmt.Sprint(x) } return strings.Join(parts, ", ") } return fmt.Sprint(v) }