From ff90aa1f5f3637edad6f7d626061eddc60364528 Mon Sep 17 00:00:00 2001
From: Kelvin Ly <kelvin.ly1618@gmail.com>
Date: Wed, 20 Nov 2019 22:53:50 -0500
Subject: [PATCH] Add responsive images to markdown

---
 main.go     |  18 ++-
 markdown.go | 345 ++++++++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 357 insertions(+), 6 deletions(-)
 create mode 100644 markdown.go

diff --git a/main.go b/main.go
index d6c89ef..5853e3b 100644
--- a/main.go
+++ b/main.go
@@ -21,7 +21,6 @@ import (
 	"io/ioutil"
 
 	"github.com/sevlyar/go-daemon"
-	gfm "github.com/shurcooL/github_flavored_markdown"
 	"github.com/shurcooL/github_flavored_markdown/gfmstyle"
 	//blackfriday "gopkg.in/russross/blackfriday.v2"
 )
@@ -34,7 +33,7 @@ var (
 	devmode = flag.Bool("dev_mode", false, "whether this server should run in developer mode or not")
 )
 
-const DEBUG = true
+const DEBUG = false
 
 const DOMAIN_NAME = "threefortiethofonehamster.com"
 
@@ -42,6 +41,7 @@ const HTML_HEADER = `<!doctype html5>
 <html>
 <head>
   <meta charset=utf-8>
+	<meta name="viewport" content="width=device-width, initial-scale=1">
 	<title>%s | %s</title>
 	<link href=/gfm/gfm.css media=all rel=stylesheet type=text/css></link>
 	<link href=/main.css media=all rel=stylesheet type=text/css></link>
@@ -89,9 +89,15 @@ func serveMarkdown(w http.ResponseWriter, r *http.Request, paths ...string) {
 		}
 	}
 	w.Write([]byte(fmt.Sprintf(HTML_HEADER, string(title), r.Host)))
-	for _, b := range bs {
-		html := gfm.Markdown(b)
-		// find images in markdown, replace with device-responsive images
+	for i, b := range bs {
+		pathDir := paths[i][len("static/"):]
+		lastSlash := strings.LastIndex(pathDir, "/")
+		if lastSlash != -1 {
+			pathDir = pathDir[:lastSlash]
+		}
+		// Markdown uses the path to generate the correct paths for resized images
+		log.Print(paths[i], "->", pathDir)
+		html := Markdown(b, pathDir)
 		w.Write(html)
 	}
 	w.Write([]byte(HTML_FOOTER))
@@ -240,7 +246,7 @@ func startServer(srv *http.Server) {
 	//serveMux.Handle("/certbot/", http.StripPrefix("/certbot/", http.FileServer(http.Dir("./certbot-tmp"))))
 	serveMux.Handle("/gfm/", http.StripPrefix("/gfm", http.FileServer(gfmstyle.Assets)))
 	serveMux.Handle("/resume/", http.StripPrefix("/resume", http.FileServer(http.Dir("resume/"))))
-	serveMux.Handle("/thumbnail/", Cache(Resize(1024, http.StripPrefix("/thumbnail", http.FileServer(http.Dir("static/"))))))
+	serveMux.Handle("/resize/", Cache(Resize(640, http.StripPrefix("/resize", http.FileServer(http.Dir("static/"))))))
 	serveMux.HandleFunc("/main.css", func(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, "main.css") })
 	if webhookKey != nil {
 		log.Print("web hook found")
diff --git a/markdown.go b/markdown.go
new file mode 100644
index 0000000..48ab165
--- /dev/null
+++ b/markdown.go
@@ -0,0 +1,345 @@
+// this code is a fork of GitHub Flavored Markdown (https://github.com/shurcooL/github_flavored_markdown)
+// with some minor alterations to support media-responsive images
+/*
+Package github_flavored_markdown provides a GitHub Flavored Markdown renderer
+with fenced code block highlighting, clickable heading anchor links.
+
+The functionality should be equivalent to the GitHub Markdown API endpoint specified at
+https://developer.github.com/v3/markdown/#render-a-markdown-document-in-raw-mode, except
+the rendering is performed locally.
+
+See examples for how to generate a complete HTML page, including CSS styles.
+*/
+package main
+
+import (
+	"bytes"
+	"fmt"
+	"log"
+	"regexp"
+	"sort"
+	"strings"
+	"text/template"
+
+	"github.com/microcosm-cc/bluemonday"
+	"github.com/russross/blackfriday"
+	"github.com/shurcooL/highlight_diff"
+	"github.com/shurcooL/highlight_go"
+	"github.com/shurcooL/sanitized_anchor_name"
+	"github.com/sourcegraph/annotate"
+	"github.com/sourcegraph/syntaxhighlight"
+	"golang.org/x/net/html"
+)
+
+// Markdown renders GitHub Flavored Markdown text.
+func Markdown(text []byte, path string) []byte {
+	log.Print("markdown " + path)
+	const htmlFlags = 0
+	renderer := &renderer{
+		Html: blackfriday.HtmlRenderer(htmlFlags, "", "").(*blackfriday.Html), path: path}
+	unsanitized := blackfriday.Markdown(text, renderer, extensions)
+	return unsanitized
+}
+
+// extensions for GitHub Flavored Markdown-like parsing.
+const extensions = blackfriday.EXTENSION_NO_INTRA_EMPHASIS |
+	blackfriday.EXTENSION_TABLES |
+	blackfriday.EXTENSION_FENCED_CODE |
+	blackfriday.EXTENSION_AUTOLINK |
+	blackfriday.EXTENSION_STRIKETHROUGH |
+	blackfriday.EXTENSION_SPACE_HEADERS |
+	blackfriday.EXTENSION_NO_EMPTY_LINE_BEFORE_BLOCK
+
+// policy for GitHub Flavored Markdown-like sanitization.
+var policy = func() *bluemonday.Policy {
+	p := bluemonday.UGCPolicy()
+	p.AllowAttrs("class").Matching(bluemonday.SpaceSeparatedTokens).OnElements("div", "span")
+	p.AllowAttrs("class", "name").Matching(bluemonday.SpaceSeparatedTokens).OnElements("a")
+	p.AllowAttrs("rel").Matching(regexp.MustCompile(`^nofollow$`)).OnElements("a")
+	p.AllowAttrs("aria-hidden").Matching(regexp.MustCompile(`^true$`)).OnElements("a")
+	p.AllowAttrs("type").Matching(regexp.MustCompile(`^checkbox$`)).OnElements("input")
+	p.AllowAttrs("checked", "disabled").Matching(regexp.MustCompile(`^$`)).OnElements("input")
+	p.AllowDataURIImages()
+	return p
+}()
+
+type renderer struct {
+	*blackfriday.Html
+	path string
+}
+
+// GitHub Flavored Markdown heading with clickable and hidden anchor.
+func (*renderer) Header(out *bytes.Buffer, text func() bool, level int, _ string) {
+	marker := out.Len()
+	doubleSpace(out)
+
+	if !text() {
+		out.Truncate(marker)
+		return
+	}
+
+	textHTML := out.String()[marker:]
+	out.Truncate(marker)
+
+	// Extract text content of the heading.
+	var textContent string
+	if node, err := html.Parse(strings.NewReader(textHTML)); err == nil {
+		textContent = extractText(node)
+	} else {
+		// Failed to parse HTML (probably can never happen), so just use the whole thing.
+		textContent = html.UnescapeString(textHTML)
+	}
+	anchorName := sanitized_anchor_name.Create(textContent)
+
+	out.WriteString(fmt.Sprintf(`<h%d><a name="%s" class="anchor" href="#%s" rel="nofollow" aria-hidden="true"><span class="octicon octicon-link"></span></a>`, level, anchorName, anchorName))
+	out.WriteString(textHTML)
+	out.WriteString(fmt.Sprintf("</h%d>\n", level))
+}
+
+func (r *renderer) Image(out *bytes.Buffer, link []byte, title []byte, alt []byte) {
+	writeImg := func() {
+		out.WriteString("<img src=\"/resize/")
+		leadingSlash := link[0] == '\\'
+		out.WriteString(r.path)
+		if !leadingSlash {
+			out.WriteByte('/')
+		}
+		attrEscape(out, link)
+		out.WriteString("\" alt=\"")
+		if len(alt) > 0 {
+			attrEscape(out, alt)
+		}
+		if len(title) > 0 {
+			out.WriteString("\" title=\"")
+			attrEscape(out, title)
+		}
+
+		out.WriteString("\" />")
+	}
+
+	writeSource := func() {
+		out.WriteString("<source srcset=\"")
+		attrEscape(out, link)
+		out.WriteString("\" media=\"(min-width: 1024px)\">")
+	}
+	// link to outside of this website
+	if bytes.HasPrefix(link, []byte("http")) {
+		writeImg()
+	} else {
+		// local link; we can use the resized images to support phones
+		out.WriteString("<picture>")
+		writeSource()
+		writeImg()
+		out.WriteString("</picture>")
+	}
+}
+
+// extractText returns the recursive concatenation of the text content of an html node.
+func extractText(n *html.Node) string {
+	var out string
+	for c := n.FirstChild; c != nil; c = c.NextSibling {
+		if c.Type == html.TextNode {
+			out += c.Data
+		} else {
+			out += extractText(c)
+		}
+	}
+	return out
+}
+
+// TODO: Clean up and improve this code.
+// GitHub Flavored Markdown fenced code block with highlighting.
+func (*renderer) BlockCode(out *bytes.Buffer, text []byte, lang string) {
+	doubleSpace(out)
+
+	// parse out the language name
+	count := 0
+	for _, elt := range strings.Fields(lang) {
+		if elt[0] == '.' {
+			elt = elt[1:]
+		}
+		if len(elt) == 0 {
+			continue
+		}
+		out.WriteString(`<div class="highlight highlight-`)
+		attrEscape(out, []byte(elt))
+		lang = elt
+		out.WriteString(`"><pre>`)
+		count++
+		break
+	}
+
+	if count == 0 {
+		out.WriteString("<pre><code>")
+	}
+
+	if highlightedCode, ok := highlightCode(text, lang); ok {
+		out.Write(highlightedCode)
+	} else {
+		attrEscape(out, text)
+	}
+
+	if count == 0 {
+		out.WriteString("</code></pre>\n")
+	} else {
+		out.WriteString("</pre></div>\n")
+	}
+}
+
+// Task List support.
+func (r *renderer) ListItem(out *bytes.Buffer, text []byte, flags int) {
+	switch {
+	case bytes.HasPrefix(text, []byte("[ ] ")):
+		text = append([]byte(`<input type="checkbox" disabled="">`), text[3:]...)
+	case bytes.HasPrefix(text, []byte("[x] ")) || bytes.HasPrefix(text, []byte("[X] ")):
+		text = append([]byte(`<input type="checkbox" checked="" disabled="">`), text[3:]...)
+	}
+	r.Html.ListItem(out, text, flags)
+}
+
+var gfmHTMLConfig = syntaxhighlight.HTMLConfig{
+	String:        "s",
+	Keyword:       "k",
+	Comment:       "c",
+	Type:          "n",
+	Literal:       "o",
+	Punctuation:   "p",
+	Plaintext:     "n",
+	Tag:           "tag",
+	HTMLTag:       "htm",
+	HTMLAttrName:  "atn",
+	HTMLAttrValue: "atv",
+	Decimal:       "m",
+}
+
+func highlightCode(src []byte, lang string) (highlightedCode []byte, ok bool) {
+	switch lang {
+	case "Go", "Go-unformatted":
+		var buf bytes.Buffer
+		err := highlight_go.Print(src, &buf, syntaxhighlight.HTMLPrinter(gfmHTMLConfig))
+		if err != nil {
+			return nil, false
+		}
+		return buf.Bytes(), true
+	case "diff":
+		anns, err := highlight_diff.Annotate(src)
+		if err != nil {
+			return nil, false
+		}
+
+		lines := bytes.Split(src, []byte("\n"))
+		lineStarts := make([]int, len(lines))
+		var offset int
+		for lineIndex := 0; lineIndex < len(lines); lineIndex++ {
+			lineStarts[lineIndex] = offset
+			offset += len(lines[lineIndex]) + 1
+		}
+
+		lastDel, lastIns := -1, -1
+		for lineIndex := 0; lineIndex < len(lines); lineIndex++ {
+			var lineFirstChar byte
+			if len(lines[lineIndex]) > 0 {
+				lineFirstChar = lines[lineIndex][0]
+			}
+			switch lineFirstChar {
+			case '+':
+				if lastIns == -1 {
+					lastIns = lineIndex
+				}
+			case '-':
+				if lastDel == -1 {
+					lastDel = lineIndex
+				}
+			default:
+				if lastDel != -1 || lastIns != -1 {
+					if lastDel == -1 {
+						lastDel = lastIns
+					} else if lastIns == -1 {
+						lastIns = lineIndex
+					}
+
+					beginOffsetLeft := lineStarts[lastDel]
+					endOffsetLeft := lineStarts[lastIns]
+					beginOffsetRight := lineStarts[lastIns]
+					endOffsetRight := lineStarts[lineIndex]
+
+					anns = append(anns, &annotate.Annotation{Start: beginOffsetLeft, End: endOffsetLeft, Left: []byte(`<span class="gd input-block">`), Right: []byte(`</span>`), WantInner: 0})
+					anns = append(anns, &annotate.Annotation{Start: beginOffsetRight, End: endOffsetRight, Left: []byte(`<span class="gi input-block">`), Right: []byte(`</span>`), WantInner: 0})
+
+					if '@' != lineFirstChar {
+						//leftContent := string(src[beginOffsetLeft:endOffsetLeft])
+						//rightContent := string(src[beginOffsetRight:endOffsetRight])
+						// This is needed to filter out the "-" and "+" at the beginning of each line from being highlighted.
+						// TODO: Still not completely filtered out.
+						leftContent := ""
+						for line := lastDel; line < lastIns; line++ {
+							leftContent += "\x00" + string(lines[line][1:]) + "\n"
+						}
+						rightContent := ""
+						for line := lastIns; line < lineIndex; line++ {
+							rightContent += "\x00" + string(lines[line][1:]) + "\n"
+						}
+
+						var sectionSegments [2][]*annotate.Annotation
+						highlight_diff.HighlightedDiffFunc(leftContent, rightContent, &sectionSegments, [2]int{beginOffsetLeft, beginOffsetRight})
+
+						anns = append(anns, sectionSegments[0]...)
+						anns = append(anns, sectionSegments[1]...)
+					}
+				}
+				lastDel, lastIns = -1, -1
+			}
+		}
+
+		sort.Sort(anns)
+
+		out, err := annotate.Annotate(src, anns, template.HTMLEscape)
+		if err != nil {
+			return nil, false
+		}
+		return out, true
+	default:
+		return nil, false
+	}
+}
+
+// Unexported blackfriday helpers.
+
+func doubleSpace(out *bytes.Buffer) {
+	if out.Len() > 0 {
+		out.WriteByte('\n')
+	}
+}
+
+func escapeSingleChar(char byte) (string, bool) {
+	if char == '"' {
+		return "&quot;", true
+	}
+	if char == '&' {
+		return "&amp;", true
+	}
+	if char == '<' {
+		return "&lt;", true
+	}
+	if char == '>' {
+		return "&gt;", true
+	}
+	return "", false
+}
+
+func attrEscape(out *bytes.Buffer, src []byte) {
+	org := 0
+	for i, ch := range src {
+		if entity, ok := escapeSingleChar(ch); ok {
+			if i > org {
+				// copy all the normal characters since the last escape
+				out.Write(src[org:i])
+			}
+			org = i + 1
+			out.WriteString(entity)
+		}
+	}
+	if org < len(src) {
+		out.Write(src[org:])
+	}
+}