// 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" "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 { 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:]) } }