carlos / movement Public

Clone
git clone https://amadan.net/carlos/movement

Plain git — no account needed to clone.

Download

Download this file

1package movement
2
3import (
4 "html/template"
5 "strings"
6)
7
8// Links inside copy, without putting markup inside copy.
9//
10// The alternative was to store the lede with anchors in it and render it
11// through template.HTML, which turns the copy table into a place where
12// escaping is off and one careless edit is an injection. Instead the copy
13// stays plain text — reviewable as words, diffable as words — and the links
14// are structured data beside it: a phrase, and where it points.
15//
16// The phrase has to appear in the copy verbatim. When copy is rewritten and a
17// phrase stops matching, the link silently stops appearing, so a test asserts
18// every phrase still resolves rather than trusting that nobody reworded
19// around it.
20
21type copyLink struct {
22 Phrase string
23 URL string
24}
25
26var links = map[string][]copyLink{
27 "index.lede": {
28 {"philosophy", "https://11factor.org"},
29 {"software ideals", "https://carlosframework.com/"},
30
31 // Three consecutive words, three destinations. Paul's call, and it
32 // only works because the words are adjacent: "projects we’re working
33 // on" reads as one phrase and turns out to be three doors. The space
34 // between them is not underlined, so they stay visibly separate
35 // links rather than one long one.
36 {"projects", "https://elevenmessenger.com/"},
37 {"we’re", "https://rastrillo.org/"},
38 {"working on", "https://carloku.com/"},
39 },
40}
41
42// Linked renders one copy string as HTML: escaped first, then anchors spliced
43// in for each phrase. Escaping happens before any markup is added, so nothing
44// in the copy itself can become markup.
45func Linked(id string) []template.HTML {
46 ls := links[id]
47
48 out := make([]template.HTML, 0, 4)
49 for _, para := range Paragraphs(id) {
50 safe := escape(para)
51 for _, l := range ls {
52 needle := escape(l.Phrase)
53 if !strings.Contains(safe, needle) {
54 continue
55 }
56 anchor := `<a class="lede-link" href="` + escape(l.URL) +
57 `" target="_blank" rel="noopener noreferrer">` + needle + `</a>`
58 // Once only: a phrase that recurs should not become a row of
59 // identical links down the paragraph.
60 safe = strings.Replace(safe, needle, anchor, 1)
61 }
62 out = append(out, template.HTML(safe))
63 }
64 return out
65}
66
67// LinkPhrases lists the phrases a copy id expects to find, so a test can
68// check they are all still present after a rewrite.
69func LinkPhrases(id string) []string {
70 var out []string
71 for _, l := range links[id] {
72 out = append(out, l.Phrase)
73 }
74 return out
75}
76
77func escape(s string) string {
78 var b strings.Builder
79 template.HTMLEscape(&b, []byte(s))
80 return b.String()
81}
82