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
5package present
6
7import (
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
29func init() {
30	funcs["style"] = Style
31}
32
33// Style returns s with HTML entities escaped and font indicators turned into
34// HTML font tags.
35func 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.
40func font(s string) string {
41	if !strings.ContainsAny(s, "[`_*") {
42		return s
43	}
44	words := split(s)
45	var b bytes.Buffer
46Word:
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.
126func 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