1 | // Copyright 2012 The Go Authors. All rights reserved. |
---|---|
2 | // Use of this source code is governed by a BSD-style |
3 | // license that can be found in the LICENSE file. |
4 | |
5 | package present |
6 | |
7 | import ( |
8 | "bytes" |
9 | "html" |
10 | "html/template" |
11 | "strings" |
12 | "unicode" |
13 | "unicode/utf8" |
14 | ) |
15 | |
16 | /* |
17 | Fonts are demarcated by an initial and final char bracketing a |
18 | space-delimited word, plus possibly some terminal punctuation. |
19 | The chars are |
20 | _ for italic |
21 | * for bold |
22 | ` (back quote) for fixed width. |
23 | Inner appearances of the char become spaces. For instance, |
24 | _this_is_italic_! |
25 | becomes |
26 | <i>this is italic</i>! |
27 | */ |
28 | |
29 | func init() { |
30 | funcs["style"] = Style |
31 | } |
32 | |
33 | // Style returns s with HTML entities escaped and font indicators turned into |
34 | // HTML font tags. |
35 | func Style(s string) template.HTML { |
36 | return template.HTML(font(html.EscapeString(s))) |
37 | } |
38 | |
39 | // font returns s with font indicators turned into HTML font tags. |
40 | func font(s string) string { |
41 | if !strings.ContainsAny(s, "[`_*") { |
42 | return s |
43 | } |
44 | words := split(s) |
45 | var b bytes.Buffer |
46 | Word: |
47 | for w, word := range words { |
48 | if len(word) < 2 { |
49 | continue Word |
50 | } |
51 | if link, _ := parseInlineLink(word); link != "" { |
52 | words[w] = link |
53 | continue Word |
54 | } |
55 | const marker = "_*`" |
56 | // Initial punctuation is OK but must be peeled off. |
57 | first := strings.IndexAny(word, marker) |
58 | if first == -1 { |
59 | continue Word |
60 | } |
61 | // Opening marker must be at the beginning of the token or else preceded by punctuation. |
62 | if first != 0 { |
63 | r, _ := utf8.DecodeLastRuneInString(word[:first]) |
64 | if !unicode.IsPunct(r) { |
65 | continue Word |
66 | } |
67 | } |
68 | open, word := word[:first], word[first:] |
69 | char := word[0] // ASCII is OK. |
70 | close := "" |
71 | switch char { |
72 | default: |
73 | continue Word |
74 | case '_': |
75 | open += "<i>" |
76 | close = "</i>" |
77 | case '*': |
78 | open += "<b>" |
79 | close = "</b>" |
80 | case '`': |
81 | open += "<code>" |
82 | close = "</code>" |
83 | } |
84 | // Closing marker must be at the end of the token or else followed by punctuation. |
85 | last := strings.LastIndex(word, word[:1]) |
86 | if last == 0 { |
87 | continue Word |
88 | } |
89 | if last+1 != len(word) { |
90 | r, _ := utf8.DecodeRuneInString(word[last+1:]) |
91 | if !unicode.IsPunct(r) { |
92 | continue Word |
93 | } |
94 | } |
95 | head, tail := word[:last+1], word[last+1:] |
96 | b.Reset() |
97 | b.WriteString(open) |
98 | var wid int |
99 | for i := 1; i < len(head)-1; i += wid { |
100 | var r rune |
101 | r, wid = utf8.DecodeRuneInString(head[i:]) |
102 | if r != rune(char) { |
103 | // Ordinary character. |
104 | b.WriteRune(r) |
105 | continue |
106 | } |
107 | if head[i+1] != char { |
108 | // Inner char becomes space. |
109 | b.WriteRune(' ') |
110 | continue |
111 | } |
112 | // Doubled char becomes real char. |
113 | // Not worth worrying about "_x__". |
114 | b.WriteByte(char) |
115 | wid++ // Consumed two chars, both ASCII. |
116 | } |
117 | b.WriteString(close) // Write closing tag. |
118 | b.WriteString(tail) // Restore trailing punctuation. |
119 | words[w] = b.String() |
120 | } |
121 | return strings.Join(words, "") |
122 | } |
123 | |
124 | // split is like strings.Fields but also returns the runs of spaces |
125 | // and treats inline links as distinct words. |
126 | func split(s string) []string { |
127 | var ( |
128 | words = make([]string, 0, 10) |
129 | start = 0 |
130 | ) |
131 | |
132 | // appendWord appends the string s[start:end] to the words slice. |
133 | // If the word contains the beginning of a link, the non-link portion |
134 | // of the word and the entire link are appended as separate words, |
135 | // and the start index is advanced to the end of the link. |
136 | appendWord := func(end int) { |
137 | if j := strings.Index(s[start:end], "[["); j > -1 { |
138 | if _, l := parseInlineLink(s[start+j:]); l > 0 { |
139 | // Append portion before link, if any. |
140 | if j > 0 { |
141 | words = append(words, s[start:start+j]) |
142 | } |
143 | // Append link itself. |
144 | words = append(words, s[start+j:start+j+l]) |
145 | // Advance start index to end of link. |
146 | start = start + j + l |
147 | return |
148 | } |
149 | } |
150 | // No link; just add the word. |
151 | words = append(words, s[start:end]) |
152 | start = end |
153 | } |
154 | |
155 | wasSpace := false |
156 | for i, r := range s { |
157 | isSpace := unicode.IsSpace(r) |
158 | if i > start && isSpace != wasSpace { |
159 | appendWord(i) |
160 | } |
161 | wasSpace = isSpace |
162 | } |
163 | for start < len(s) { |
164 | appendWord(len(s)) |
165 | } |
166 | return words |
167 | } |
168 |
Members