| 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