A clean, Markdown-based publishing platform made for writers. Write together, and build a community. https://writefreely.org
Nie możesz wybrać więcej, niż 25 tematów Tematy muszą się zaczynać od litery lub cyfry, mogą zawierać myślniki ('-') i mogą mieć do 35 znaków.
 
 
 
 
 

222 wiersze
7.7 KiB

  1. package writefreely
  2. import (
  3. "bytes"
  4. "fmt"
  5. "github.com/microcosm-cc/bluemonday"
  6. stripmd "github.com/writeas/go-strip-markdown"
  7. "github.com/writeas/saturday"
  8. "github.com/writeas/web-core/stringmanip"
  9. "github.com/writeas/writefreely/parse"
  10. "html"
  11. "html/template"
  12. "regexp"
  13. "strings"
  14. "unicode"
  15. "unicode/utf8"
  16. )
  17. var (
  18. blockReg = regexp.MustCompile("<(ul|ol|blockquote)>\n")
  19. endBlockReg = regexp.MustCompile("</([a-z]+)>\n</(ul|ol|blockquote)>")
  20. youtubeReg = regexp.MustCompile("(https?://www.youtube.com/embed/[a-zA-Z0-9\\-_]+)(\\?[^\t\n\f\r \"']+)?")
  21. titleElementReg = regexp.MustCompile("</?h[1-6]>")
  22. hashtagReg = regexp.MustCompile(`#([\p{L}\p{M}\d]+)`)
  23. markeddownReg = regexp.MustCompile("<p>(.+)</p>")
  24. )
  25. func (p *Post) formatContent(c *Collection, isOwner bool) {
  26. baseURL := c.CanonicalURL()
  27. if !isSingleUser {
  28. baseURL = "/" + c.Alias + "/"
  29. }
  30. newCon := hashtagReg.ReplaceAllFunc([]byte(p.Content), func(b []byte) []byte {
  31. // Ensure we only replace "hashtags" that have already been extracted.
  32. // `hashtagReg` catches everything, including any hash on the end of a
  33. // URL, so we rely on p.Tags as the final word on whether or not to link
  34. // a tag.
  35. for _, t := range p.Tags {
  36. if string(b) == "#"+t {
  37. return bytes.Replace(b, []byte("#"+t), []byte("<a href=\""+baseURL+"tag:"+t+"\" class=\"hashtag\"><span>#</span><span class=\"p-category\">"+t+"</span></a>"), -1)
  38. }
  39. }
  40. return b
  41. })
  42. p.HTMLTitle = template.HTML(applyBasicMarkdown([]byte(p.Title.String)))
  43. p.HTMLContent = template.HTML(applyMarkdown([]byte(newCon)))
  44. if exc := strings.Index(string(newCon), "<!--more-->"); exc > -1 {
  45. p.HTMLExcerpt = template.HTML(applyMarkdown([]byte(newCon[:exc])))
  46. }
  47. }
  48. func (p *PublicPost) formatContent(isOwner bool) {
  49. p.Post.formatContent(&p.Collection.Collection, isOwner)
  50. }
  51. func applyMarkdown(data []byte) string {
  52. return applyMarkdownSpecial(data, false)
  53. }
  54. func applyMarkdownSpecial(data []byte, skipNoFollow bool) string {
  55. mdExtensions := 0 |
  56. blackfriday.EXTENSION_TABLES |
  57. blackfriday.EXTENSION_FENCED_CODE |
  58. blackfriday.EXTENSION_AUTOLINK |
  59. blackfriday.EXTENSION_STRIKETHROUGH |
  60. blackfriday.EXTENSION_SPACE_HEADERS |
  61. blackfriday.EXTENSION_AUTO_HEADER_IDS
  62. htmlFlags := 0 |
  63. blackfriday.HTML_USE_SMARTYPANTS |
  64. blackfriday.HTML_SMARTYPANTS_DASHES
  65. // Generate Markdown
  66. md := blackfriday.Markdown([]byte(data), blackfriday.HtmlRenderer(htmlFlags, "", ""), mdExtensions)
  67. // Strip out bad HTML
  68. policy := getSanitizationPolicy()
  69. policy.RequireNoFollowOnLinks(!skipNoFollow)
  70. outHTML := string(policy.SanitizeBytes(md))
  71. // Strip newlines on certain block elements that render with them
  72. outHTML = blockReg.ReplaceAllString(outHTML, "<$1>")
  73. outHTML = endBlockReg.ReplaceAllString(outHTML, "</$1></$2>")
  74. // Remove all query parameters on YouTube embed links
  75. // TODO: make this more specific. Taking the nuclear approach here to strip ?autoplay=1
  76. outHTML = youtubeReg.ReplaceAllString(outHTML, "$1")
  77. return outHTML
  78. }
  79. func applyBasicMarkdown(data []byte) string {
  80. mdExtensions := 0 |
  81. blackfriday.EXTENSION_STRIKETHROUGH |
  82. blackfriday.EXTENSION_SPACE_HEADERS |
  83. blackfriday.EXTENSION_HEADER_IDS
  84. htmlFlags := 0 |
  85. blackfriday.HTML_SKIP_HTML |
  86. blackfriday.HTML_USE_SMARTYPANTS |
  87. blackfriday.HTML_SMARTYPANTS_DASHES
  88. // Generate Markdown
  89. md := blackfriday.Markdown([]byte(data), blackfriday.HtmlRenderer(htmlFlags, "", ""), mdExtensions)
  90. // Strip out bad HTML
  91. policy := bluemonday.UGCPolicy()
  92. policy.AllowAttrs("class", "id").Globally()
  93. outHTML := string(policy.SanitizeBytes(md))
  94. outHTML = markeddownReg.ReplaceAllString(outHTML, "$1")
  95. outHTML = strings.TrimRightFunc(outHTML, unicode.IsSpace)
  96. return outHTML
  97. }
  98. func postTitle(content, friendlyId string) string {
  99. const maxTitleLen = 80
  100. // Strip HTML tags with bluemonday's StrictPolicy, then unescape the HTML
  101. // entities added in by sanitizing the content.
  102. content = html.UnescapeString(bluemonday.StrictPolicy().Sanitize(content))
  103. content = strings.TrimLeftFunc(stripmd.Strip(content), unicode.IsSpace)
  104. eol := strings.IndexRune(content, '\n')
  105. blankLine := strings.Index(content, "\n\n")
  106. if blankLine != -1 && blankLine <= eol && blankLine <= assumedTitleLen {
  107. return strings.TrimSpace(content[:blankLine])
  108. } else if utf8.RuneCountInString(content) <= maxTitleLen {
  109. return content
  110. }
  111. return friendlyId
  112. }
  113. // TODO: fix duplicated code from postTitle. postTitle is a widely used func we
  114. // don't have time to investigate right now.
  115. func friendlyPostTitle(content, friendlyId string) string {
  116. const maxTitleLen = 80
  117. // Strip HTML tags with bluemonday's StrictPolicy, then unescape the HTML
  118. // entities added in by sanitizing the content.
  119. content = html.UnescapeString(bluemonday.StrictPolicy().Sanitize(content))
  120. content = strings.TrimLeftFunc(stripmd.Strip(content), unicode.IsSpace)
  121. eol := strings.IndexRune(content, '\n')
  122. blankLine := strings.Index(content, "\n\n")
  123. if blankLine != -1 && blankLine <= eol && blankLine <= assumedTitleLen {
  124. return strings.TrimSpace(content[:blankLine])
  125. } else if eol == -1 && utf8.RuneCountInString(content) <= maxTitleLen {
  126. return content
  127. }
  128. title, truncd := parse.TruncToWord(parse.PostLede(content, true), maxTitleLen)
  129. if truncd {
  130. title += "..."
  131. }
  132. return title
  133. }
  134. func getSanitizationPolicy() *bluemonday.Policy {
  135. policy := bluemonday.UGCPolicy()
  136. policy.AllowAttrs("src", "style").OnElements("iframe", "video")
  137. policy.AllowAttrs("frameborder", "width", "height").Matching(bluemonday.Integer).OnElements("iframe")
  138. policy.AllowAttrs("allowfullscreen").OnElements("iframe")
  139. policy.AllowAttrs("controls", "loop", "muted", "autoplay").OnElements("video")
  140. policy.AllowAttrs("target").OnElements("a")
  141. policy.AllowAttrs("style", "class", "id").Globally()
  142. policy.AllowURLSchemes("http", "https", "mailto", "xmpp")
  143. return policy
  144. }
  145. func sanitizePost(content string) string {
  146. return strings.Replace(content, "<", "&lt;", -1)
  147. }
  148. // postDescription generates a description based on the given post content,
  149. // title, and post ID. This doesn't consider a V2 post field, `title` when
  150. // choosing what to generate. In case a post has a title, this function will
  151. // fail, and logic should instead be implemented to skip this when there's no
  152. // title, like so:
  153. // var desc string
  154. // if title == "" {
  155. // desc = postDescription(content, title, friendlyId)
  156. // } else {
  157. // desc = shortPostDescription(content)
  158. // }
  159. func postDescription(content, title, friendlyId string) string {
  160. maxLen := 140
  161. if content == "" {
  162. content = "Write Freely is a painless, simple, federated blogging platform."
  163. } else {
  164. fmtStr := "%s"
  165. truncation := 0
  166. if utf8.RuneCountInString(content) > maxLen {
  167. // Post is longer than the max description, so let's show a better description
  168. fmtStr = "%s..."
  169. truncation = 3
  170. }
  171. if title == friendlyId {
  172. // No specific title was found; simply truncate the post, starting at the beginning
  173. content = fmt.Sprintf(fmtStr, strings.Replace(stringmanip.Substring(content, 0, maxLen-truncation), "\n", " ", -1))
  174. } else {
  175. // There was a title, so return a real description
  176. blankLine := strings.Index(content, "\n\n")
  177. if blankLine < 0 {
  178. blankLine = 0
  179. }
  180. truncd := stringmanip.Substring(content, blankLine, blankLine+maxLen-truncation)
  181. contentNoNL := strings.Replace(truncd, "\n", " ", -1)
  182. content = strings.TrimSpace(fmt.Sprintf(fmtStr, contentNoNL))
  183. }
  184. }
  185. return content
  186. }
  187. func shortPostDescription(content string) string {
  188. maxLen := 140
  189. fmtStr := "%s"
  190. truncation := 0
  191. if utf8.RuneCountInString(content) > maxLen {
  192. // Post is longer than the max description, so let's show a better description
  193. fmtStr = "%s..."
  194. truncation = 3
  195. }
  196. return strings.TrimSpace(fmt.Sprintf(fmtStr, strings.Replace(stringmanip.Substring(content, 0, maxLen-truncation), "\n", " ", -1)))
  197. }