A clean, Markdown-based publishing platform made for writers. Write together, and build a community. https://writefreely.org
選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。
 
 
 
 
 

136 行
4.7 KiB

  1. package writefreely
  2. import (
  3. "bytes"
  4. "github.com/microcosm-cc/bluemonday"
  5. stripmd "github.com/writeas/go-strip-markdown"
  6. "github.com/writeas/saturday"
  7. "html"
  8. "html/template"
  9. "regexp"
  10. "strings"
  11. "unicode"
  12. "unicode/utf8"
  13. )
  14. var (
  15. blockReg = regexp.MustCompile("<(ul|ol|blockquote)>\n")
  16. endBlockReg = regexp.MustCompile("</([a-z]+)>\n</(ul|ol|blockquote)>")
  17. youtubeReg = regexp.MustCompile("(https?://www.youtube.com/embed/[a-zA-Z0-9\\-_]+)(\\?[^\t\n\f\r \"']+)?")
  18. titleElementReg = regexp.MustCompile("</?h[1-6]>")
  19. hashtagReg = regexp.MustCompile(`#([\p{L}\p{M}\d]+)`)
  20. markeddownReg = regexp.MustCompile("<p>(.+)</p>")
  21. )
  22. func (p *Post) formatContent(c *Collection, isOwner bool) {
  23. baseURL := c.CanonicalURL()
  24. if isOwner {
  25. baseURL = "/" + c.Alias + "/"
  26. }
  27. newCon := hashtagReg.ReplaceAllFunc([]byte(p.Content), func(b []byte) []byte {
  28. // Ensure we only replace "hashtags" that have already been extracted.
  29. // `hashtagReg` catches everything, including any hash on the end of a
  30. // URL, so we rely on p.Tags as the final word on whether or not to link
  31. // a tag.
  32. for _, t := range p.Tags {
  33. if string(b) == "#"+t {
  34. return bytes.Replace(b, []byte("#"+t), []byte("<a href=\""+baseURL+"tag:"+t+"\" class=\"hashtag\"><span>#</span><span class=\"p-category\">"+t+"</span></a>"), -1)
  35. }
  36. }
  37. return b
  38. })
  39. p.HTMLTitle = template.HTML(applyBasicMarkdown([]byte(p.Title.String)))
  40. p.HTMLContent = template.HTML(applyMarkdown([]byte(newCon)))
  41. if exc := strings.Index(string(newCon), "<!--more-->"); exc > -1 {
  42. p.HTMLExcerpt = template.HTML(applyMarkdown([]byte(newCon[:exc])))
  43. }
  44. }
  45. func (p *PublicPost) formatContent(isOwner bool) {
  46. p.Post.formatContent(&p.Collection.Collection, isOwner)
  47. }
  48. func applyMarkdown(data []byte) string {
  49. return applyMarkdownSpecial(data, false)
  50. }
  51. func applyMarkdownSpecial(data []byte, skipNoFollow bool) string {
  52. mdExtensions := 0 |
  53. blackfriday.EXTENSION_TABLES |
  54. blackfriday.EXTENSION_FENCED_CODE |
  55. blackfriday.EXTENSION_AUTOLINK |
  56. blackfriday.EXTENSION_STRIKETHROUGH |
  57. blackfriday.EXTENSION_SPACE_HEADERS |
  58. blackfriday.EXTENSION_AUTO_HEADER_IDS
  59. htmlFlags := 0 |
  60. blackfriday.HTML_USE_SMARTYPANTS |
  61. blackfriday.HTML_SMARTYPANTS_DASHES
  62. // Generate Markdown
  63. md := blackfriday.Markdown([]byte(data), blackfriday.HtmlRenderer(htmlFlags, "", ""), mdExtensions)
  64. // Strip out bad HTML
  65. policy := getSanitizationPolicy()
  66. policy.RequireNoFollowOnLinks(!skipNoFollow)
  67. outHTML := string(policy.SanitizeBytes(md))
  68. // Strip newlines on certain block elements that render with them
  69. outHTML = blockReg.ReplaceAllString(outHTML, "<$1>")
  70. outHTML = endBlockReg.ReplaceAllString(outHTML, "</$1></$2>")
  71. // Remove all query parameters on YouTube embed links
  72. // TODO: make this more specific. Taking the nuclear approach here to strip ?autoplay=1
  73. outHTML = youtubeReg.ReplaceAllString(outHTML, "$1")
  74. return outHTML
  75. }
  76. func applyBasicMarkdown(data []byte) string {
  77. mdExtensions := 0 |
  78. blackfriday.EXTENSION_STRIKETHROUGH |
  79. blackfriday.EXTENSION_SPACE_HEADERS |
  80. blackfriday.EXTENSION_HEADER_IDS
  81. htmlFlags := 0 |
  82. blackfriday.HTML_SKIP_HTML |
  83. blackfriday.HTML_USE_SMARTYPANTS |
  84. blackfriday.HTML_SMARTYPANTS_DASHES
  85. // Generate Markdown
  86. md := blackfriday.Markdown([]byte(data), blackfriday.HtmlRenderer(htmlFlags, "", ""), mdExtensions)
  87. // Strip out bad HTML
  88. policy := bluemonday.UGCPolicy()
  89. policy.AllowAttrs("class", "id").Globally()
  90. outHTML := string(policy.SanitizeBytes(md))
  91. outHTML = markeddownReg.ReplaceAllString(outHTML, "$1")
  92. outHTML = strings.TrimRightFunc(outHTML, unicode.IsSpace)
  93. return outHTML
  94. }
  95. func postTitle(content, friendlyId string) string {
  96. const maxTitleLen = 80
  97. // Strip HTML tags with bluemonday's StrictPolicy, then unescape the HTML
  98. // entities added in by sanitizing the content.
  99. content = html.UnescapeString(bluemonday.StrictPolicy().Sanitize(content))
  100. content = strings.TrimLeftFunc(stripmd.Strip(content), unicode.IsSpace)
  101. eol := strings.IndexRune(content, '\n')
  102. blankLine := strings.Index(content, "\n\n")
  103. if blankLine != -1 && blankLine <= eol && blankLine <= assumedTitleLen {
  104. return strings.TrimSpace(content[:blankLine])
  105. } else if utf8.RuneCountInString(content) <= maxTitleLen {
  106. return content
  107. }
  108. return friendlyId
  109. }
  110. func getSanitizationPolicy() *bluemonday.Policy {
  111. policy := bluemonday.UGCPolicy()
  112. policy.AllowAttrs("src", "style").OnElements("iframe", "video")
  113. policy.AllowAttrs("frameborder", "width", "height").Matching(bluemonday.Integer).OnElements("iframe")
  114. policy.AllowAttrs("allowfullscreen").OnElements("iframe")
  115. policy.AllowAttrs("controls", "loop", "muted", "autoplay").OnElements("video")
  116. policy.AllowAttrs("target").OnElements("a")
  117. policy.AllowAttrs("style", "class", "id").Globally()
  118. policy.AllowURLSchemes("http", "https", "mailto", "xmpp")
  119. return policy
  120. }