Add responsive images to markdown
This commit is contained in:
parent
c341321145
commit
ff90aa1f5f
18
main.go
18
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")
|
||||
|
|
|
@ -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, §ionSegments, [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 """, true
|
||||
}
|
||||
if char == '&' {
|
||||
return "&", true
|
||||
}
|
||||
if char == '<' {
|
||||
return "<", true
|
||||
}
|
||||
if char == '>' {
|
||||
return ">", 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:])
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue