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 = ` + %s | %s @@ -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(``, level, anchorName, anchorName)) + out.WriteString(textHTML) + out.WriteString(fmt.Sprintf("\n", level)) +} + +func (r *renderer) Image(out *bytes.Buffer, link []byte, title []byte, alt []byte) { + writeImg := func() { + out.WriteString("\"") 0 { + attrEscape(out, alt) + } + if len(title) > 0 { + out.WriteString("\" title=\"") + attrEscape(out, title) + } + + out.WriteString("\" />") + } + + writeSource := func() { + out.WriteString("") + } + // 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("") + writeSource() + writeImg() + out.WriteString("") + } +} + +// 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(`
`)
+		count++
+		break
+	}
+
+	if count == 0 {
+		out.WriteString("
")
+	}
+
+	if highlightedCode, ok := highlightCode(text, lang); ok {
+		out.Write(highlightedCode)
+	} else {
+		attrEscape(out, text)
+	}
+
+	if count == 0 {
+		out.WriteString("
\n") + } else { + out.WriteString("
\n") + } +} + +// Task List support. +func (r *renderer) ListItem(out *bytes.Buffer, text []byte, flags int) { + switch { + case bytes.HasPrefix(text, []byte("[ ] ")): + text = append([]byte(``), text[3:]...) + case bytes.HasPrefix(text, []byte("[x] ")) || bytes.HasPrefix(text, []byte("[X] ")): + text = append([]byte(``), 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(``), Right: []byte(``), WantInner: 0}) + anns = append(anns, &annotate.Annotation{Start: beginOffsetRight, End: endOffsetRight, Left: []byte(``), Right: []byte(``), 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:]) + } +}