package html import ( "bytes" "fmt" "io" "regexp" "sort" "strconv" "strings" "github.com/gomarkdown/markdown/ast" ) // Flags control optional behavior of HTML renderer. type Flags int // IDTag is the tag used for tag identification, it defaults to "id", some renderers // may wish to override this and use e.g. "anchor". var IDTag = "id" // HTML renderer configuration options. const ( FlagsNone Flags = 0 SkipHTML Flags = 1 << iota // Skip preformatted HTML blocks SkipImages // Skip embedded images SkipLinks // Skip all links Safelink // Only link to trusted protocols NofollowLinks // Only link with rel="nofollow" NoreferrerLinks // Only link with rel="noreferrer" HrefTargetBlank // Add a blank target CompletePage // Generate a complete HTML page UseXHTML // Generate XHTML output instead of HTML FootnoteReturnLinks // Generate a link at the end of a footnote to return to the source FootnoteNoHRTag // Do not output an HR after starting a footnote list. Smartypants // Enable smart punctuation substitutions SmartypantsFractions // Enable smart fractions (with Smartypants) SmartypantsDashes // Enable smart dashes (with Smartypants) SmartypantsLatexDashes // Enable LaTeX-style dashes (with Smartypants) SmartypantsAngledQuotes // Enable angled double quotes (with Smartypants) for double quotes rendering SmartypantsQuotesNBSP // Enable « French guillemets » (with Smartypants) TOC // Generate a table of contents CommonFlags Flags = Smartypants | SmartypantsFractions | SmartypantsDashes | SmartypantsLatexDashes ) var ( htmlTagRe = regexp.MustCompile("(?i)^" + htmlTag) ) const ( htmlTag = "(?:" + openTag + "|" + closeTag + "|" + htmlComment + "|" + processingInstruction + "|" + declaration + "|" + cdata + ")" closeTag = "]" openTag = "<" + tagName + attribute + "*" + "\\s*/?>" attribute = "(?:" + "\\s+" + attributeName + attributeValueSpec + "?)" attributeValue = "(?:" + unquotedValue + "|" + singleQuotedValue + "|" + doubleQuotedValue + ")" attributeValueSpec = "(?:" + "\\s*=" + "\\s*" + attributeValue + ")" attributeName = "[a-zA-Z_:][a-zA-Z0-9:._-]*" cdata = "" declaration = "]*>" doubleQuotedValue = "\"[^\"]*\"" htmlComment = "|" processingInstruction = "[<][?].*?[?][>]" singleQuotedValue = "'[^']*'" tagName = "[A-Za-z][A-Za-z0-9-]*" unquotedValue = "[^\"'=<>`\\x00-\\x20]+" ) // RenderNodeFunc allows reusing most of Renderer logic and replacing // rendering of some nodes. If it returns false, Renderer.RenderNode // will execute its logic. If it returns true, Renderer.RenderNode will // skip rendering this node and will return WalkStatus type RenderNodeFunc func(w io.Writer, node ast.Node, entering bool) (ast.WalkStatus, bool) // RendererOptions is a collection of supplementary parameters tweaking // the behavior of various parts of HTML renderer. type RendererOptions struct { // Prepend this text to each relative URL. AbsolutePrefix string // Add this text to each footnote anchor, to ensure uniqueness. FootnoteAnchorPrefix string // Show this text inside the tag for a footnote return link, if the // FootnoteReturnLinks flag is enabled. If blank, the string // [return] is used. FootnoteReturnLinkContents string // CitationFormatString defines how a citation is rendered. If blnck, the string // [%s] is used. Where %s will be substituted with the citation target. CitationFormatString string // If set, add this text to the front of each Heading ID, to ensure uniqueness. HeadingIDPrefix string // If set, add this text to the back of each Heading ID, to ensure uniqueness. HeadingIDSuffix string Title string // Document title (used if CompletePage is set) CSS string // Optional CSS file URL (used if CompletePage is set) Icon string // Optional icon file URL (used if CompletePage is set) Head []byte // Optional head data injected in the section (used if CompletePage is set) Flags Flags // Flags allow customizing this renderer's behavior // if set, called at the start of RenderNode(). Allows replacing // rendering of some nodes RenderNodeHook RenderNodeFunc // Comments is a list of comments the renderer should detect when // parsing code blocks and detecting callouts. Comments [][]byte // Generator is a meta tag that is inserted in the generated HTML so show what rendered it. It should not include the closing tag. // Defaults (note content quote is not closed) to ` " or ">" // Track heading IDs to prevent ID collision in a single generation. headingIDs map[string]int lastOutputLen int disableTags int sr *SPRenderer documentMatter ast.DocumentMatters // keep track of front/main/back matter. } // NewRenderer creates and configures an Renderer object, which // satisfies the Renderer interface. func NewRenderer(opts RendererOptions) *Renderer { // configure the rendering engine closeTag := ">" if opts.Flags&UseXHTML != 0 { closeTag = " />" } if opts.FootnoteReturnLinkContents == "" { opts.FootnoteReturnLinkContents = `[return]` } if opts.CitationFormatString == "" { opts.CitationFormatString = `[%s]` } if opts.Generator == "" { opts.Generator = ` = len(tagname) { break } if strings.ToLower(string(tag[i]))[0] != tagname[j] { return false, -1 } } if i == len(tag) { return false, -1 } rightAngle := skipUntilCharIgnoreQuotes(tag, i, '>') if rightAngle >= i { return true, rightAngle } return false, -1 } func isRelativeLink(link []byte) (yes bool) { // a tag begin with '#' if link[0] == '#' { return true } // link begin with '/' but not '//', the second maybe a protocol relative link if len(link) >= 2 && link[0] == '/' && link[1] != '/' { return true } // only the root '/' if len(link) == 1 && link[0] == '/' { return true } // current directory : begin with "./" if bytes.HasPrefix(link, []byte("./")) { return true } // parent directory : begin with "../" if bytes.HasPrefix(link, []byte("../")) { return true } return false } func (r *Renderer) ensureUniqueHeadingID(id string) string { for count, found := r.headingIDs[id]; found; count, found = r.headingIDs[id] { tmp := fmt.Sprintf("%s-%d", id, count+1) if _, tmpFound := r.headingIDs[tmp]; !tmpFound { r.headingIDs[id] = count + 1 id = tmp } else { id = id + "-1" } } if _, found := r.headingIDs[id]; !found { r.headingIDs[id] = 0 } return id } func (r *Renderer) addAbsPrefix(link []byte) []byte { if r.opts.AbsolutePrefix != "" && isRelativeLink(link) && link[0] != '.' { newDest := r.opts.AbsolutePrefix if link[0] != '/' { newDest += "/" } newDest += string(link) return []byte(newDest) } return link } func appendLinkAttrs(attrs []string, flags Flags, link []byte) []string { if isRelativeLink(link) { return attrs } var val []string if flags&NofollowLinks != 0 { val = append(val, "nofollow") } if flags&NoreferrerLinks != 0 { val = append(val, "noreferrer") } if flags&HrefTargetBlank != 0 { attrs = append(attrs, `target="_blank"`) } if len(val) == 0 { return attrs } attr := fmt.Sprintf("rel=%q", strings.Join(val, " ")) return append(attrs, attr) } func isMailto(link []byte) bool { return bytes.HasPrefix(link, []byte("mailto:")) } func needSkipLink(flags Flags, dest []byte) bool { if flags&SkipLinks != 0 { return true } return flags&Safelink != 0 && !isSafeLink(dest) && !isMailto(dest) } func isSmartypantable(node ast.Node) bool { switch node.GetParent().(type) { case *ast.Link, *ast.CodeBlock, *ast.Code: return false } return true } func appendLanguageAttr(attrs []string, info []byte) []string { if len(info) == 0 { return attrs } endOfLang := bytes.IndexAny(info, "\t ") if endOfLang < 0 { endOfLang = len(info) } s := `class="language-` + string(info[:endOfLang]) + `"` return append(attrs, s) } func (r *Renderer) outTag(w io.Writer, name string, attrs []string) { s := name if len(attrs) > 0 { s += " " + strings.Join(attrs, " ") } io.WriteString(w, s+">") r.lastOutputLen = 1 } func footnoteRef(prefix string, node *ast.Link) string { urlFrag := prefix + string(slugify(node.Destination)) nStr := strconv.Itoa(node.NoteID) anchor := `` + nStr + `` return `` + anchor + `` } func footnoteItem(prefix string, slug []byte) string { return `
  • ` } func footnoteReturnLink(prefix, returnLink string, slug []byte) string { return ` ` + returnLink + `` } func listItemOpenCR(listItem *ast.ListItem) bool { if ast.GetPrevNode(listItem) == nil { return false } ld := listItem.Parent.(*ast.List) return !ld.Tight && ld.ListFlags&ast.ListTypeDefinition == 0 } func skipParagraphTags(para *ast.Paragraph) bool { parent := para.Parent grandparent := parent.GetParent() if grandparent == nil || !isList(grandparent) { return false } isParentTerm := isListItemTerm(parent) grandparentListData := grandparent.(*ast.List) tightOrTerm := grandparentListData.Tight || isParentTerm return tightOrTerm } func (r *Renderer) out(w io.Writer, d []byte) { r.lastOutputLen = len(d) if r.disableTags > 0 { d = htmlTagRe.ReplaceAll(d, []byte{}) } w.Write(d) } func (r *Renderer) outs(w io.Writer, s string) { r.lastOutputLen = len(s) if r.disableTags > 0 { s = htmlTagRe.ReplaceAllString(s, "") } io.WriteString(w, s) } func (r *Renderer) cr(w io.Writer) { if r.lastOutputLen > 0 { r.outs(w, "\n") } } var ( openHTags = []string{"", "", "", "", ""} ) func headingOpenTagFromLevel(level int) string { if level < 1 || level > 5 { return " 5 { return "" } return closeHTags[level-1] } func (r *Renderer) outHRTag(w io.Writer, attrs []string) { hr := tagWithAttributes("") } func (r *Renderer) text(w io.Writer, text *ast.Text) { if r.opts.Flags&Smartypants != 0 { var tmp bytes.Buffer EscapeHTML(&tmp, text.Literal) r.sr.Process(w, tmp.Bytes()) } else { _, parentIsLink := text.Parent.(*ast.Link) if parentIsLink { escLink(w, text.Literal) } else { EscapeHTML(w, text.Literal) } } } func (r *Renderer) hardBreak(w io.Writer, node *ast.Hardbreak) { r.outOneOf(w, r.opts.Flags&UseXHTML == 0, "
    ", "
    ") r.cr(w) } func (r *Renderer) nonBlockingSpace(w io.Writer, node *ast.NonBlockingSpace) { r.outs(w, " ") } func (r *Renderer) outOneOf(w io.Writer, outFirst bool, first string, second string) { if outFirst { r.outs(w, first) } else { r.outs(w, second) } } func (r *Renderer) outOneOfCr(w io.Writer, outFirst bool, first string, second string) { if outFirst { r.cr(w) r.outs(w, first) } else { r.outs(w, second) r.cr(w) } } func (r *Renderer) htmlSpan(w io.Writer, span *ast.HTMLSpan) { if r.opts.Flags&SkipHTML == 0 { r.out(w, span.Literal) } } func (r *Renderer) linkEnter(w io.Writer, link *ast.Link) { var attrs []string dest := link.Destination dest = r.addAbsPrefix(dest) var hrefBuf bytes.Buffer hrefBuf.WriteString("href=\"") escLink(&hrefBuf, dest) hrefBuf.WriteByte('"') attrs = append(attrs, hrefBuf.String()) if link.NoteID != 0 { r.outs(w, footnoteRef(r.opts.FootnoteAnchorPrefix, link)) return } attrs = appendLinkAttrs(attrs, r.opts.Flags, dest) if len(link.Title) > 0 { var titleBuff bytes.Buffer titleBuff.WriteString("title=\"") EscapeHTML(&titleBuff, link.Title) titleBuff.WriteByte('"') attrs = append(attrs, titleBuff.String()) } r.outTag(w, "") } } func (r *Renderer) link(w io.Writer, link *ast.Link, entering bool) { // mark it but don't link it if it is not a safe link: no smartypants if needSkipLink(r.opts.Flags, link.Destination) { r.outOneOf(w, entering, "", "") return } if entering { r.linkEnter(w, link) } else { r.linkExit(w, link) } } func (r *Renderer) imageEnter(w io.Writer, image *ast.Image) { dest := image.Destination dest = r.addAbsPrefix(dest) if r.disableTags == 0 { //if options.safe && potentiallyUnsafe(dest) { //out(w, ``)
		//} else {
		r.outs(w, `<img src=`) } } func (r *Renderer) paragraphEnter(w io.Writer, para *ast.Paragraph) { // TODO: untangle this clusterfuck about when the newlines need // to be added and when not. prev := ast.GetPrevNode(para) if prev != nil { switch prev.(type) { case *ast.HTMLBlock, *ast.List, *ast.Paragraph, *ast.Heading, *ast.CaptionFigure, *ast.CodeBlock, *ast.BlockQuote, *ast.Aside, *ast.HorizontalRule: r.cr(w) } } if prev == nil { _, isParentBlockQuote := para.Parent.(*ast.BlockQuote) if isParentBlockQuote { r.cr(w) } _, isParentAside := para.Parent.(*ast.Aside) if isParentAside { r.cr(w) } } tag := tagWithAttributes("") if !(isListItem(para.Parent) && ast.GetNextNode(para) == nil) { r.cr(w) } } func (r *Renderer) paragraph(w io.Writer, para *ast.Paragraph, entering bool) { if skipParagraphTags(para) { return } if entering { r.paragraphEnter(w, para) } else { r.paragraphExit(w, para) } } func (r *Renderer) image(w io.Writer, node *ast.Image, entering bool) { if entering { r.imageEnter(w, node) } else { r.imageExit(w, node) } } func (r *Renderer) code(w io.Writer, node *ast.Code) { r.outs(w, "") EscapeHTML(w, node.Literal) r.outs(w, "") } func (r *Renderer) htmlBlock(w io.Writer, node *ast.HTMLBlock) { if r.opts.Flags&SkipHTML != 0 { return } r.cr(w) r.out(w, node.Literal) r.cr(w) } func (r *Renderer) headingEnter(w io.Writer, nodeData *ast.Heading) { var attrs []string var class string // TODO(miek): add helper functions for coalescing these classes. if nodeData.IsTitleblock { class = "title" } if nodeData.IsSpecial { if class != "" { class += " special" } else { class = "special" } } if class != "" { attrs = []string{`class="` + class + `"`} } if nodeData.HeadingID != "" { id := r.ensureUniqueHeadingID(nodeData.HeadingID) if r.opts.HeadingIDPrefix != "" { id = r.opts.HeadingIDPrefix + id } if r.opts.HeadingIDSuffix != "" { id = id + r.opts.HeadingIDSuffix } attrID := `id="` + id + `"` attrs = append(attrs, attrID) } attrs = append(attrs, BlockAttrs(nodeData)...) r.cr(w) r.outTag(w, headingOpenTagFromLevel(nodeData.Level), attrs) } func (r *Renderer) headingExit(w io.Writer, heading *ast.Heading) { r.outs(w, headingCloseTagFromLevel(heading.Level)) if !(isListItem(heading.Parent) && ast.GetNextNode(heading) == nil) { r.cr(w) } } func (r *Renderer) heading(w io.Writer, node *ast.Heading, entering bool) { if entering { r.headingEnter(w, node) } else { r.headingExit(w, node) } } func (r *Renderer) horizontalRule(w io.Writer, node *ast.HorizontalRule) { r.cr(w) r.outHRTag(w, BlockAttrs(node)) r.cr(w) } func (r *Renderer) listEnter(w io.Writer, nodeData *ast.List) { // TODO: attrs don't seem to be set var attrs []string if nodeData.IsFootnotesList { r.outs(w, "\n
    \n\n") if r.opts.Flags&FootnoteNoHRTag == 0 { r.outHRTag(w, nil) r.cr(w) } } r.cr(w) if isListItem(nodeData.Parent) { grand := nodeData.Parent.GetParent() if isListTight(grand) { r.cr(w) } } openTag := " 0 { attrs = append(attrs, fmt.Sprintf(`start="%d"`, nodeData.Start)) } openTag = "\n") } } func (r *Renderer) list(w io.Writer, list *ast.List, entering bool) { if entering { r.listEnter(w, list) } else { r.listExit(w, list) } } func (r *Renderer) listItemEnter(w io.Writer, listItem *ast.ListItem) { if listItemOpenCR(listItem) { r.cr(w) } if listItem.RefLink != nil { slug := slugify(listItem.RefLink) r.outs(w, footnoteItem(r.opts.FootnoteAnchorPrefix, slug)) return } openTag := "
  • " if listItem.ListFlags&ast.ListTypeDefinition != 0 { openTag = "
    " } if listItem.ListFlags&ast.ListTypeTerm != 0 { openTag = "
    " } r.outs(w, openTag) } func (r *Renderer) listItemExit(w io.Writer, listItem *ast.ListItem) { if listItem.RefLink != nil && r.opts.Flags&FootnoteReturnLinks != 0 { slug := slugify(listItem.RefLink) prefix := r.opts.FootnoteAnchorPrefix link := r.opts.FootnoteReturnLinkContents s := footnoteReturnLink(prefix, link, slug) r.outs(w, s) } closeTag := "
  • " if listItem.ListFlags&ast.ListTypeDefinition != 0 { closeTag = "" } if listItem.ListFlags&ast.ListTypeTerm != 0 { closeTag = "" } r.outs(w, closeTag) r.cr(w) } func (r *Renderer) listItem(w io.Writer, listItem *ast.ListItem, entering bool) { if entering { r.listItemEnter(w, listItem) } else { r.listItemExit(w, listItem) } } func (r *Renderer) codeBlock(w io.Writer, codeBlock *ast.CodeBlock) { var attrs []string // TODO(miek): this can add multiple class= attribute, they should be coalesced into one. // This is probably true for some other elements as well attrs = appendLanguageAttr(attrs, codeBlock.Info) attrs = append(attrs, BlockAttrs(codeBlock)...) r.cr(w) r.outs(w, "
    ")
    	code := tagWithAttributes("")
    	r.outs(w, "
    ") if !isListItem(codeBlock.Parent) { r.cr(w) } } func (r *Renderer) caption(w io.Writer, caption *ast.Caption, entering bool) { if entering { r.outs(w, "
    ") return } r.outs(w, "
    ") } func (r *Renderer) captionFigure(w io.Writer, figure *ast.CaptionFigure, entering bool) { // TODO(miek): copy more generic ways of mmark over to here. fig := "` } else { fig += ">" } r.outOneOf(w, entering, fig, "\n\n") } func (r *Renderer) tableCell(w io.Writer, tableCell *ast.TableCell, entering bool) { if !entering { r.outOneOf(w, tableCell.IsHeader, "", "") r.cr(w) return } // entering var attrs []string openTag := "") // XXX: this is to adhere to a rather silly test. Should fix test. if ast.GetFirstChild(node) == nil { r.cr(w) } } else { r.outs(w, "") r.cr(w) } } func (r *Renderer) matter(w io.Writer, node *ast.DocumentMatter, entering bool) { if !entering { return } if r.documentMatter != ast.DocumentMatterNone { r.outs(w, "\n") } switch node.Matter { case ast.DocumentMatterFront: r.outs(w, `
    `) case ast.DocumentMatterMain: r.outs(w, `
    `) case ast.DocumentMatterBack: r.outs(w, `
    `) } r.documentMatter = node.Matter } func (r *Renderer) citation(w io.Writer, node *ast.Citation) { for i, c := range node.Destination { attr := []string{`class="none"`} switch node.Type[i] { case ast.CitationTypeNormative: attr[0] = `class="normative"` case ast.CitationTypeInformative: attr[0] = `class="informative"` case ast.CitationTypeSuppressed: attr[0] = `class="suppressed"` } r.outTag(w, "`+r.opts.CitationFormatString+``, c, c)) r.outs(w, "") } } func (r *Renderer) callout(w io.Writer, node *ast.Callout) { attr := []string{`class="callout"`} r.outTag(w, "") } func (r *Renderer) index(w io.Writer, node *ast.Index) { // there is no in-text representation. attr := []string{`class="index"`, fmt.Sprintf(`id="%s"`, node.ID)} r.outTag(w, "") } // RenderNode renders a markdown node to HTML func (r *Renderer) RenderNode(w io.Writer, node ast.Node, entering bool) ast.WalkStatus { if r.opts.RenderNodeHook != nil { status, didHandle := r.opts.RenderNodeHook(w, node, entering) if didHandle { return status } } switch node := node.(type) { case *ast.Text: r.text(w, node) case *ast.Softbreak: r.cr(w) // TODO: make it configurable via out(renderer.softbreak) case *ast.Hardbreak: r.hardBreak(w, node) case *ast.NonBlockingSpace: r.nonBlockingSpace(w, node) case *ast.Emph: r.outOneOf(w, entering, "", "") case *ast.Strong: r.outOneOf(w, entering, "", "") case *ast.Del: r.outOneOf(w, entering, "", "") case *ast.BlockQuote: tag := tagWithAttributes("") case *ast.Aside: tag := tagWithAttributes("") case *ast.Link: r.link(w, node, entering) case *ast.CrossReference: link := &ast.Link{Destination: append([]byte("#"), node.Destination...)} r.link(w, link, entering) case *ast.Citation: r.citation(w, node) case *ast.Image: if r.opts.Flags&SkipImages != 0 { return ast.SkipChildren } r.image(w, node, entering) case *ast.Code: r.code(w, node) case *ast.CodeBlock: r.codeBlock(w, node) case *ast.Caption: r.caption(w, node, entering) case *ast.CaptionFigure: r.captionFigure(w, node, entering) case *ast.Document: // do nothing case *ast.Paragraph: r.paragraph(w, node, entering) case *ast.HTMLSpan: r.htmlSpan(w, node) case *ast.HTMLBlock: r.htmlBlock(w, node) case *ast.Heading: r.heading(w, node, entering) case *ast.HorizontalRule: r.horizontalRule(w, node) case *ast.List: r.list(w, node, entering) case *ast.ListItem: r.listItem(w, node, entering) case *ast.Table: tag := tagWithAttributes("") case *ast.TableCell: r.tableCell(w, node, entering) case *ast.TableHeader: r.outOneOfCr(w, entering, "", "") case *ast.TableBody: r.tableBody(w, node, entering) case *ast.TableRow: r.outOneOfCr(w, entering, "", "") case *ast.TableFooter: r.outOneOfCr(w, entering, "", "") case *ast.Math: r.outOneOf(w, true, `\(`, `\)`) EscapeHTML(w, node.Literal) r.outOneOf(w, false, `\(`, `\)`) case *ast.MathBlock: r.outOneOf(w, entering, `

    \[`, `\]

    `) if entering { EscapeHTML(w, node.Literal) } case *ast.DocumentMatter: r.matter(w, node, entering) case *ast.Callout: r.callout(w, node) case *ast.Index: r.index(w, node) case *ast.Subscript: r.outOneOf(w, true, "", "") if entering { Escape(w, node.Literal) } r.outOneOf(w, false, "", "") case *ast.Superscript: r.outOneOf(w, true, "", "") if entering { Escape(w, node.Literal) } r.outOneOf(w, false, "", "") case *ast.Footnotes: // nothing by default; just output the list. default: panic(fmt.Sprintf("Unknown node %T", node)) } return ast.GoToNext } // RenderHeader writes HTML document preamble and TOC if requested. func (r *Renderer) RenderHeader(w io.Writer, ast ast.Node) { r.writeDocumentHeader(w) if r.opts.Flags&TOC != 0 { r.writeTOC(w, ast) } } // RenderFooter writes HTML document footer. func (r *Renderer) RenderFooter(w io.Writer, _ ast.Node) { if r.documentMatter != ast.DocumentMatterNone { r.outs(w, "
    \n") } if r.opts.Flags&CompletePage == 0 { return } io.WriteString(w, "\n\n\n") } func (r *Renderer) writeDocumentHeader(w io.Writer) { if r.opts.Flags&CompletePage == 0 { return } ending := "" if r.opts.Flags&UseXHTML != 0 { io.WriteString(w, "\n") io.WriteString(w, "\n") ending = " /" } else { io.WriteString(w, "\n") io.WriteString(w, "\n") } io.WriteString(w, "\n") io.WriteString(w, " ") if r.opts.Flags&Smartypants != 0 { r.sr.Process(w, []byte(r.opts.Title)) } else { EscapeHTML(w, []byte(r.opts.Title)) } io.WriteString(w, "\n") io.WriteString(w, r.opts.Generator) io.WriteString(w, "\"") io.WriteString(w, ending) io.WriteString(w, ">\n") io.WriteString(w, " \n") if r.opts.CSS != "" { io.WriteString(w, " \n") } if r.opts.Icon != "" { io.WriteString(w, " \n") } if r.opts.Head != nil { w.Write(r.opts.Head) } io.WriteString(w, "\n") io.WriteString(w, "\n\n") } func (r *Renderer) writeTOC(w io.Writer, doc ast.Node) { buf := bytes.Buffer{} inHeading := false tocLevel := 0 headingCount := 0 ast.WalkFunc(doc, func(node ast.Node, entering bool) ast.WalkStatus { if nodeData, ok := node.(*ast.Heading); ok && !nodeData.IsTitleblock { inHeading = entering if !entering { buf.WriteString("") return ast.GoToNext } nodeData.HeadingID = fmt.Sprintf("toc_%d", headingCount) if nodeData.Level == tocLevel { buf.WriteString("\n\n
  • ") } else if nodeData.Level < tocLevel { for nodeData.Level < tocLevel { tocLevel-- buf.WriteString("
  • \n") } buf.WriteString("\n\n
  • ") } else { for nodeData.Level > tocLevel { tocLevel++ buf.WriteString("\n") } if buf.Len() > 0 { io.WriteString(w, "\n") } r.lastOutputLen = buf.Len() } func isList(node ast.Node) bool { _, ok := node.(*ast.List) return ok } func isListTight(node ast.Node) bool { if list, ok := node.(*ast.List); ok { return list.Tight } return false } func isListItem(node ast.Node) bool { _, ok := node.(*ast.ListItem) return ok } func isListItemTerm(node ast.Node) bool { data, ok := node.(*ast.ListItem) return ok && data.ListFlags&ast.ListTypeTerm != 0 } // TODO: move to internal package func skipSpace(data []byte, i int) int { n := len(data) for i < n && isSpace(data[i]) { i++ } return i } // TODO: move to internal package var validUris = [][]byte{[]byte("http://"), []byte("https://"), []byte("ftp://"), []byte("mailto://")} var validPaths = [][]byte{[]byte("/"), []byte("./"), []byte("../")} func isSafeLink(link []byte) bool { for _, path := range validPaths { if len(link) >= len(path) && bytes.Equal(link[:len(path)], path) { if len(link) == len(path) { return true } else if isAlnum(link[len(path)]) { return true } } } for _, prefix := range validUris { // TODO: handle unicode here // case-insensitive prefix test if len(link) > len(prefix) && bytes.Equal(bytes.ToLower(link[:len(prefix)]), prefix) && isAlnum(link[len(prefix)]) { return true } } return false } // TODO: move to internal package // Create a url-safe slug for fragments func slugify(in []byte) []byte { if len(in) == 0 { return in } out := make([]byte, 0, len(in)) sym := false for _, ch := range in { if isAlnum(ch) { sym = false out = append(out, ch) } else if sym { continue } else { out = append(out, '-') sym = true } } var a, b int var ch byte for a, ch = range out { if ch != '-' { break } } for b = len(out) - 1; b > 0; b-- { if out[b] != '-' { break } } return out[a : b+1] } // TODO: move to internal package // isAlnum returns true if c is a digit or letter // TODO: check when this is looking for ASCII alnum and when it should use unicode func isAlnum(c byte) bool { return (c >= '0' && c <= '9') || isLetter(c) } // isSpace returns true if c is a white-space charactr func isSpace(c byte) bool { return c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\f' || c == '\v' } // isLetter returns true if c is ascii letter func isLetter(c byte) bool { return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') } // isPunctuation returns true if c is a punctuation symbol. func isPunctuation(c byte) bool { for _, r := range []byte("!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~") { if c == r { return true } } return false } // BlockAttrs takes a node and checks if it has block level attributes set. If so it // will return a slice each containing a "key=value(s)" string. func BlockAttrs(node ast.Node) []string { var attr *ast.Attribute if c := node.AsContainer(); c != nil && c.Attribute != nil { attr = c.Attribute } if l := node.AsLeaf(); l != nil && l.Attribute != nil { attr = l.Attribute } if attr == nil { return nil } var s []string if attr.ID != nil { s = append(s, fmt.Sprintf(`%s="%s"`, IDTag, attr.ID)) } classes := "" for _, c := range attr.Classes { classes += " " + string(c) } if classes != "" { s = append(s, fmt.Sprintf(`class="%s"`, classes[1:])) // skip space we added. } // sort the attributes so it remain stable between runs var keys = []string{} for k := range attr.Attrs { keys = append(keys, k) } sort.Strings(keys) for _, k := range keys { s = append(s, fmt.Sprintf(`%s="%s"`, k, attr.Attrs[k])) } return s } func tagWithAttributes(name string, attrs []string) string { s := name if len(attrs) > 0 { s += " " + strings.Join(attrs, " ") } return s + ">" }