diff --git a/postrender.go b/postrender.go index d5988e8..60a8a9b 100644 --- a/postrender.go +++ b/postrender.go @@ -1,363 +1,363 @@ /* * Copyright © 2018-2021 Musing Studio LLC. * * This file is part of WriteFreely. * * WriteFreely is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License, included * in the LICENSE file in this source code package. */ package writefreely import ( "bytes" "encoding/json" "fmt" "html" "html/template" "net/http" "net/url" "regexp" "strings" "unicode" "unicode/utf8" "github.com/microcosm-cc/bluemonday" stripmd "github.com/writeas/go-strip-markdown/v2" "github.com/writeas/impart" blackfriday "github.com/writeas/saturday" "github.com/writeas/web-core/log" "github.com/writeas/web-core/stringmanip" "github.com/writefreely/writefreely/config" "github.com/writefreely/writefreely/parse" ) var ( blockReg = regexp.MustCompile("<(ul|ol|blockquote)>\n") endBlockReg = regexp.MustCompile("([a-z]+)>\n(ul|ol|blockquote)>") youtubeReg = regexp.MustCompile("(https?://www.youtube.com/embed/[a-zA-Z0-9\\-_]+)(\\?[^\t\n\f\r \"']+)?") titleElementReg = regexp.MustCompile("?h[1-6]>") hashtagReg = regexp.MustCompile(`{{\[\[\|\|([^|]+)\|\|\]\]}}`) markeddownReg = regexp.MustCompile("
(.+)
") mentionReg = regexp.MustCompile(`@([A-Za-z0-9._%+-]+)(@[A-Za-z0-9.-]+\.[A-Za-z]+)\b`) ) func (p *Post) handlePremiumContent(c *Collection, isOwner, postPage bool, cfg *config.Config) { if c.Monetization != "" { // User has Web Monetization enabled, so split content if it exists spl := strings.Index(p.Content, shortCodePaid) p.IsPaid = spl > -1 if postPage { // We're viewing the individual post if isOwner { p.Content = strings.Replace(p.Content, shortCodePaid, "\n\n"+`Your subscriber content begins here.
`+"\n\n", 1) } else { if spl > -1 { p.Content = p.Content[:spl+len(shortCodePaid)] - p.Content = strings.Replace(p.Content, shortCodePaid, "\n\n"+`Continue reading with a Coil membership.
`+"\n\n", 1) + p.Content = strings.Replace(p.Content, shortCodePaid, "\n\n"+`Continue reading with Web Monetization.
`+"\n\n", 1) } } } else { // We've viewing the post on the collection landing if spl > -1 { baseURL := c.CanonicalURL() if isOwner { baseURL = "/" + c.Alias + "/" } p.Content = p.Content[:spl+len(shortCodePaid)] p.HTMLExcerpt = template.HTML(applyMarkdown([]byte(p.Content[:spl]), baseURL, cfg)) } } } } func (p *Post) formatContent(cfg *config.Config, c *Collection, isOwner bool, isPostPage bool) { baseURL := c.CanonicalURL() // TODO: redundant if !isSingleUser { baseURL = "/" + c.Alias + "/" } p.handlePremiumContent(c, isOwner, isPostPage, cfg) p.Content = strings.Replace(p.Content, "<!--paid-->", "", 1) p.HTMLTitle = template.HTML(applyBasicMarkdown([]byte(p.Title.String))) p.HTMLContent = template.HTML(applyMarkdown([]byte(p.Content), baseURL, cfg)) if exc := strings.Index(string(p.Content), ""); exc > -1 { p.HTMLExcerpt = template.HTML(applyMarkdown([]byte(p.Content[:exc]), baseURL, cfg)) } } func (p *PublicPost) formatContent(cfg *config.Config, isOwner bool, isPostPage bool) { p.Post.formatContent(cfg, &p.Collection.Collection, isOwner, isPostPage) } func (p *Post) augmentContent(c *Collection) { if p.PinnedPosition.Valid { // Don't augment posts that are pinned return } if strings.Index(p.Content, shortCodeNoSig) > -1 { // Don't augment posts with the special "nosig" shortcode return } // Add post signatures if c.Signature != "" { p.Content += "\n\n" + c.Signature } } func (p *PublicPost) augmentContent() { p.Post.augmentContent(&p.Collection.Collection) } func (p *PublicPost) augmentReadingDestination() { if p.IsPaid { p.HTMLContent += template.HTML("\n\n" + `` + localStr("Read more...", p.Language.String) + ` ($)
`) } } func applyMarkdown(data []byte, baseURL string, cfg *config.Config) string { return applyMarkdownSpecial(data, baseURL, cfg, cfg.App.SingleUser) } func disableYoutubeAutoplay(outHTML string) string { for _, match := range youtubeReg.FindAllString(outHTML, -1) { u, err := url.Parse(match) if err != nil { continue } u.RawQuery = html.UnescapeString(u.RawQuery) q := u.Query() // Set Youtube autoplay url parameter, if any, to 0 if len(q["autoplay"]) == 1 { q.Set("autoplay", "0") } u.RawQuery = q.Encode() cleanURL := u.String() outHTML = strings.Replace(outHTML, match, cleanURL, 1) } return outHTML } func applyMarkdownSpecial(data []byte, baseURL string, cfg *config.Config, skipNoFollow bool) string { mdExtensions := 0 | blackfriday.EXTENSION_TABLES | blackfriday.EXTENSION_FENCED_CODE | blackfriday.EXTENSION_AUTOLINK | blackfriday.EXTENSION_STRIKETHROUGH | blackfriday.EXTENSION_SPACE_HEADERS | blackfriday.EXTENSION_AUTO_HEADER_IDS htmlFlags := 0 | blackfriday.HTML_USE_SMARTYPANTS | blackfriday.HTML_SMARTYPANTS_DASHES if baseURL != "" { htmlFlags |= blackfriday.HTML_HASHTAGS } // Generate Markdown md := blackfriday.Markdown([]byte(data), blackfriday.HtmlRenderer(htmlFlags, "", ""), mdExtensions) if baseURL != "" { // Replace special text generated by Markdown parser tagPrefix := baseURL + "tag:" if cfg.App.Chorus { tagPrefix = "/read/t/" } md = []byte(hashtagReg.ReplaceAll(md, []byte("#$1"))) handlePrefix := cfg.App.Host + "/@/" md = []byte(mentionReg.ReplaceAll(md, []byte("@$1$2"))) } // Strip out bad HTML policy := getSanitizationPolicy() policy.RequireNoFollowOnLinks(!skipNoFollow) outHTML := string(policy.SanitizeBytes(md)) // Strip newlines on certain block elements that render with them outHTML = blockReg.ReplaceAllString(outHTML, "<$1>") outHTML = endBlockReg.ReplaceAllString(outHTML, "$1>$2>") outHTML = disableYoutubeAutoplay(outHTML) return outHTML } func applyBasicMarkdown(data []byte) string { if len(bytes.TrimSpace(data)) == 0 { return "" } mdExtensions := 0 | blackfriday.EXTENSION_STRIKETHROUGH | blackfriday.EXTENSION_SPACE_HEADERS | blackfriday.EXTENSION_HEADER_IDS htmlFlags := 0 | blackfriday.HTML_SKIP_HTML | blackfriday.HTML_USE_SMARTYPANTS | blackfriday.HTML_SMARTYPANTS_DASHES // Generate Markdown // This passes the supplied title into blackfriday.Markdown() as an H1 header, so we only render HTML that // belongs in an H1. md := blackfriday.Markdown(append([]byte("# "), data...), blackfriday.HtmlRenderer(htmlFlags, "", ""), mdExtensions) // Remove H1 markup md = bytes.TrimSpace(md) // blackfriday.Markdown adds a newline at the end of theFor $5 per month, you can read this and other great writing across our site and other websites that support Web Monetization.
') - $readmoreSell.insertAdjacentHTML("beforeend", '\n\n') + $readmoreSell.insertAdjacentHTML("beforeend", '\n\nWith a Web Monetization wallet, you can read this and other great writing across our site and other websites that support Web Monetization.
') + $readmoreSell.insertAdjacentHTML("beforeend", '\n\n') } function initMonetization() { let $content = document.querySelector('.e-content') let $post = document.getElementById('post-body') let $split = $post.querySelector('.split') if (document.monetization === undefined || $split == null) { if ($split) { showWMPaywall($content, $split) } return } document.monetization.addEventListener('monetizationstop', function(event) { if (pendingSplitContent) { // We've seen the 'pending' activity, so we can assume things will work document.monetization.removeEventListener('monetizationstop', progressHandler) return } // We're getting 'stop' without ever starting, so display the paywall. showWMPaywall($content, $split) }); document.monetization.addEventListener('monetizationpending', function (event) { pendingSplitContent = true }) let progressHandler = function(event) { if (unlockedSplitContent) { document.monetization.removeEventListener('monetizationprogress', progressHandler) return } if (!unlockingSplitContent && !unlockedSplitContent) { unlockingSplitContent = true getSplitContent(event.detail.receipt, function (status, data) { unlockingSplitContent = false if (status == 200) { $split.textContent = "Your subscriber perks start here." $split.insertAdjacentHTML("afterend", "\n\n"+data.data.html_body) } else { $split.textContent = "Something went wrong while unlocking subscriber content." } unlockedSplitContent = true }) } } function getSplitContent(receipt, callback) { let params = "receipt="+encodeURIComponent(receipt) let http = new XMLHttpRequest(); http.open("POST", "/api/collections/" + window.collAlias + "/posts/" + window.postSlug + "/splitcontent", true); // Send the proper header information along with the request http.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); http.onreadystatechange = function () { if (http.readyState == 4) { callback(http.status, JSON.parse(http.responseText)); } } http.send(params); } document.monetization.addEventListener('monetizationstart', function() { if (!unlockedSplitContent) { $split.textContent = "Unlocking subscriber content..." } document.monetization.removeEventListener('monetizationstart', progressHandler) }); document.monetization.addEventListener('monetizationprogress', progressHandler); } \ No newline at end of file