You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

167 lines
3.9 KiB

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