168 lines
3.9 KiB
Go
168 lines
3.9 KiB
Go
|
// Copyright 2012 The Go Authors. All rights reserved.
|
||
|
// Use of this source code is governed by a BSD-style
|
||
|
// license that can be found in the LICENSE file.
|
||
|
|
||
|
package present
|
||
|
|
||
|
import (
|
||
|
"bytes"
|
||
|
"html"
|
||
|
"html/template"
|
||
|
"strings"
|
||
|
"unicode"
|
||
|
"unicode/utf8"
|
||
|
)
|
||
|
|
||
|
/*
|
||
|
Fonts are demarcated by an initial and final char bracketing a
|
||
|
space-delimited word, plus possibly some terminal punctuation.
|
||
|
The chars are
|
||
|
_ for italic
|
||
|
* for bold
|
||
|
` (back quote) for fixed width.
|
||
|
Inner appearances of the char become spaces. For instance,
|
||
|
_this_is_italic_!
|
||
|
becomes
|
||
|
<i>this is italic</i>!
|
||
|
*/
|
||
|
|
||
|
func init() {
|
||
|
funcs["style"] = Style
|
||
|
}
|
||
|
|
||
|
// Style returns s with HTML entities escaped and font indicators turned into
|
||
|
// HTML font tags.
|
||
|
func Style(s string) template.HTML {
|
||
|
return template.HTML(font(html.EscapeString(s)))
|
||
|
}
|
||
|
|
||
|
// font returns s with font indicators turned into HTML font tags.
|
||
|
func font(s string) string {
|
||
|
if !strings.ContainsAny(s, "[`_*") {
|
||
|
return s
|
||
|
}
|
||
|
words := split(s)
|
||
|
var b bytes.Buffer
|
||
|
Word:
|
||
|
for w, word := range words {
|
||
|
if len(word) < 2 {
|
||
|
continue Word
|
||
|
}
|
||
|
if link, _ := parseInlineLink(word); link != "" {
|
||
|
words[w] = link
|
||
|
continue Word
|
||
|
}
|
||
|
const marker = "_*`"
|
||
|
// Initial punctuation is OK but must be peeled off.
|
||
|
first := strings.IndexAny(word, marker)
|
||
|
if first == -1 {
|
||
|
continue Word
|
||
|
}
|
||
|
// Opening marker must be at the beginning of the token or else preceded by punctuation.
|
||
|
if first != 0 {
|
||
|
r, _ := utf8.DecodeLastRuneInString(word[:first])
|
||
|
if !unicode.IsPunct(r) {
|
||
|
continue Word
|
||
|
}
|
||
|
}
|
||
|
open, word := word[:first], word[first:]
|
||
|
char := word[0] // ASCII is OK.
|
||
|
close := ""
|
||
|
switch char {
|
||
|
default:
|
||
|
continue Word
|
||
|
case '_':
|
||
|
open += "<i>"
|
||
|
close = "</i>"
|
||
|
case '*':
|
||
|
open += "<b>"
|
||
|
close = "</b>"
|
||
|
case '`':
|
||
|
open += "<code>"
|
||
|
close = "</code>"
|
||
|
}
|
||
|
// Closing marker must be at the end of the token or else followed by punctuation.
|
||
|
last := strings.LastIndex(word, word[:1])
|
||
|
if last == 0 {
|
||
|
continue Word
|
||
|
}
|
||
|
if last+1 != len(word) {
|
||
|
r, _ := utf8.DecodeRuneInString(word[last+1:])
|
||
|
if !unicode.IsPunct(r) {
|
||
|
continue Word
|
||
|
}
|
||
|
}
|
||
|
head, tail := word[:last+1], word[last+1:]
|
||
|
b.Reset()
|
||
|
b.WriteString(open)
|
||
|
var wid int
|
||
|
for i := 1; i < len(head)-1; i += wid {
|
||
|
var r rune
|
||
|
r, wid = utf8.DecodeRuneInString(head[i:])
|
||
|
if r != rune(char) {
|
||
|
// Ordinary character.
|
||
|
b.WriteRune(r)
|
||
|
continue
|
||
|
}
|
||
|
if head[i+1] != char {
|
||
|
// Inner char becomes space.
|
||
|
b.WriteRune(' ')
|
||
|
continue
|
||
|
}
|
||
|
// Doubled char becomes real char.
|
||
|
// Not worth worrying about "_x__".
|
||
|
b.WriteByte(char)
|
||
|
wid++ // Consumed two chars, both ASCII.
|
||
|
}
|
||
|
b.WriteString(close) // Write closing tag.
|
||
|
b.WriteString(tail) // Restore trailing punctuation.
|
||
|
words[w] = b.String()
|
||
|
}
|
||
|
return strings.Join(words, "")
|
||
|
}
|
||
|
|
||
|
// split is like strings.Fields but also returns the runs of spaces
|
||
|
// and treats inline links as distinct words.
|
||
|
func split(s string) []string {
|
||
|
var (
|
||
|
words = make([]string, 0, 10)
|
||
|
start = 0
|
||
|
)
|
||
|
|
||
|
// appendWord appends the string s[start:end] to the words slice.
|
||
|
// If the word contains the beginning of a link, the non-link portion
|
||
|
// of the word and the entire link are appended as separate words,
|
||
|
// and the start index is advanced to the end of the link.
|
||
|
appendWord := func(end int) {
|
||
|
if j := strings.Index(s[start:end], "[["); j > -1 {
|
||
|
if _, l := parseInlineLink(s[start+j:]); l > 0 {
|
||
|
// Append portion before link, if any.
|
||
|
if j > 0 {
|
||
|
words = append(words, s[start:start+j])
|
||
|
}
|
||
|
// Append link itself.
|
||
|
words = append(words, s[start+j:start+j+l])
|
||
|
// Advance start index to end of link.
|
||
|
start = start + j + l
|
||
|
return
|
||
|
}
|
||
|
}
|
||
|
// No link; just add the word.
|
||
|
words = append(words, s[start:end])
|
||
|
start = end
|
||
|
}
|
||
|
|
||
|
wasSpace := false
|
||
|
for i, r := range s {
|
||
|
isSpace := unicode.IsSpace(r)
|
||
|
if i > start && isSpace != wasSpace {
|
||
|
appendWord(i)
|
||
|
}
|
||
|
wasSpace = isSpace
|
||
|
}
|
||
|
for start < len(s) {
|
||
|
appendWord(len(s))
|
||
|
}
|
||
|
return words
|
||
|
}
|