A clean, Markdown-based publishing platform made for writers. Write together, and build a community. https://writefreely.org
Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.
 
 
 
 
 

229 lignes
7.9 KiB

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