diff options
author | Wim <wim@42.be> | 2019-02-24 15:13:56 +0100 |
---|---|---|
committer | Wim <wim@42.be> | 2019-02-24 15:13:56 +0100 |
commit | 96841c70c7d59d1c80f98db7dcdfc03620829758 (patch) | |
tree | df43223b443ab43949c68b265dee518ff3455c47 /vendor/github.com/russross | |
parent | f92735d35d01388ca790c43ef54548ecafae6f92 (diff) | |
download | matterbridge-msglm-96841c70c7d59d1c80f98db7dcdfc03620829758.tar.gz matterbridge-msglm-96841c70c7d59d1c80f98db7dcdfc03620829758.tar.bz2 matterbridge-msglm-96841c70c7d59d1c80f98db7dcdfc03620829758.zip |
Fix regression in HTML handling (telegram). Closes #734
* Revert back to blackfriday v1
* Add testing
Diffstat (limited to 'vendor/github.com/russross')
-rw-r--r-- | vendor/github.com/russross/blackfriday/.travis.yml | 27 | ||||
-rw-r--r-- | vendor/github.com/russross/blackfriday/README.md | 130 | ||||
-rw-r--r-- | vendor/github.com/russross/blackfriday/block.go | 843 | ||||
-rw-r--r-- | vendor/github.com/russross/blackfriday/doc.go | 40 | ||||
-rw-r--r-- | vendor/github.com/russross/blackfriday/esc.go | 34 | ||||
-rw-r--r-- | vendor/github.com/russross/blackfriday/go.mod | 1 | ||||
-rw-r--r-- | vendor/github.com/russross/blackfriday/html.go | 1510 | ||||
-rw-r--r-- | vendor/github.com/russross/blackfriday/inline.go | 552 | ||||
-rw-r--r-- | vendor/github.com/russross/blackfriday/latex.go | 334 | ||||
-rw-r--r-- | vendor/github.com/russross/blackfriday/markdown.go | 773 | ||||
-rw-r--r-- | vendor/github.com/russross/blackfriday/node.go | 354 | ||||
-rw-r--r-- | vendor/github.com/russross/blackfriday/smartypants.go | 139 |
12 files changed, 2310 insertions, 2427 deletions
diff --git a/vendor/github.com/russross/blackfriday/.travis.yml b/vendor/github.com/russross/blackfriday/.travis.yml index a4eb2577..2f3351d7 100644 --- a/vendor/github.com/russross/blackfriday/.travis.yml +++ b/vendor/github.com/russross/blackfriday/.travis.yml @@ -1,18 +1,17 @@ -# Travis CI (http://travis-ci.org/) is a continuous integration service for -# open source projects. This file configures it to run unit tests for -# blackfriday. - +sudo: false language: go - go: - - 1.5 - - 1.6 - - 1.7 - + - "1.9.x" + - "1.10.x" + - tip +matrix: + fast_finish: true + allow_failures: + - go: tip install: - - go get -d -t -v ./... - - go build -v ./... - + - # Do nothing. This is needed to prevent default install action "go get -t -v ./..." from happening here (we want it to happen inside script step). script: - - go test -v ./... - - go test -run=^$ -bench=BenchmarkReference -benchmem + - go get -t -v ./... + - diff -u <(echo -n) <(gofmt -d -s .) + - go tool vet . + - go test -v -race ./... diff --git a/vendor/github.com/russross/blackfriday/README.md b/vendor/github.com/russross/blackfriday/README.md index 2e0db355..3c62e137 100644 --- a/vendor/github.com/russross/blackfriday/README.md +++ b/vendor/github.com/russross/blackfriday/README.md @@ -1,4 +1,6 @@ -Blackfriday [![Build Status](https://travis-ci.org/russross/blackfriday.svg?branch=master)](https://travis-ci.org/russross/blackfriday) +Blackfriday +[![Build Status][BuildSVG]][BuildURL] +[![Godoc][GodocV2SVG]][GodocV2URL] =========== Blackfriday is a [Markdown][1] processor implemented in [Go][2]. It @@ -16,27 +18,27 @@ It started as a translation from C of [Sundown][3]. Installation ------------ -Blackfriday is compatible with any modern Go release. With Go 1.7 and git -installed: +Blackfriday is compatible with any modern Go release. With Go and git installed: - go get gopkg.in/russross/blackfriday.v2 + go get -u gopkg.in/russross/blackfriday.v2 -will download, compile, and install the package into your `$GOPATH` -directory hierarchy. Alternatively, you can achieve the same if you -import it into a project: - - import "gopkg.in/russross/blackfriday.v2" - -and `go get` without parameters. +will download, compile, and install the package into your `$GOPATH` directory +hierarchy. Versions -------- Currently maintained and recommended version of Blackfriday is `v2`. It's being -developed on its own branch: https://github.com/russross/blackfriday/v2. You -should install and import it via [gopkg.in][6] at -`gopkg.in/russross/blackfriday.v2`. +developed on its own branch: https://github.com/russross/blackfriday/tree/v2 and the +documentation is available at +https://godoc.org/gopkg.in/russross/blackfriday.v2. + +It is `go get`-able via [gopkg.in][6] at `gopkg.in/russross/blackfriday.v2`, +but we highly recommend using package management tool like [dep][7] or +[Glide][8] and make use of semantic versioning. With package management you +should import `github.com/russross/blackfriday` and specify that you're using +version 2.0.0. Version 2 offers a number of improvements over v1: @@ -56,9 +58,43 @@ Potential drawbacks: v2. See issue [#348](https://github.com/russross/blackfriday/issues/348) for tracking. +If you are still interested in the legacy `v1`, you can import it from +`github.com/russross/blackfriday`. Documentation for the legacy v1 can be found +here: https://godoc.org/github.com/russross/blackfriday + +### Known issue with `dep` + +There is a known problem with using Blackfriday v1 _transitively_ and `dep`. +Currently `dep` prioritizes semver versions over anything else, and picks the +latest one, plus it does not apply a `[[constraint]]` specifier to transitively +pulled in packages. So if you're using something that uses Blackfriday v1, but +that something does not use `dep` yet, you will get Blackfriday v2 pulled in and +your first dependency will fail to build. + +There are couple of fixes for it, documented here: +https://github.com/golang/dep/blob/master/docs/FAQ.md#how-do-i-constrain-a-transitive-dependencys-version + +Meanwhile, `dep` team is working on a more general solution to the constraints +on transitive dependencies problem: https://github.com/golang/dep/issues/1124. + + Usage ----- +### v1 + +For basic usage, it is as simple as getting your input into a byte +slice and calling: + + output := blackfriday.MarkdownBasic(input) + +This renders it with no extensions enabled. To get a more useful +feature set, use this instead: + + output := blackfriday.MarkdownCommon(input) + +### v2 + For the most sensible markdown processing, it is as simple as getting your input into a byte slice and calling: @@ -85,7 +121,7 @@ Here's an example of simple usage of Blackfriday together with Bluemonday: ```go import ( "github.com/microcosm-cc/bluemonday" - "github.com/russross/blackfriday" + "gopkg.in/russross/blackfriday.v2" ) // ... @@ -93,11 +129,21 @@ unsafe := blackfriday.Run(input) html := bluemonday.UGCPolicy().SanitizeBytes(unsafe) ``` -### Custom options +### Custom options, v1 + +If you want to customize the set of options, first get a renderer +(currently only the HTML output engine), then use it to +call the more general `Markdown` function. For examples, see the +implementations of `MarkdownBasic` and `MarkdownCommon` in +`markdown.go`. + +### Custom options, v2 If you want to customize the set of options, use `blackfriday.WithExtensions`, `blackfriday.WithRenderer` and `blackfriday.WithRefOverride`. +### `blackfriday-tool` + You can also check out `blackfriday-tool` for a more complete example of how to use it. Download and install it using: @@ -117,6 +163,22 @@ installed in `$GOPATH/bin`. This is a statically-linked binary that can be copied to wherever you need it without worrying about dependencies and library versions. +### Sanitized anchor names + +Blackfriday includes an algorithm for creating sanitized anchor names +corresponding to a given input text. This algorithm is used to create +anchors for headings when `EXTENSION_AUTO_HEADER_IDS` is enabled. The +algorithm has a specification, so that other packages can create +compatible anchor names and links to those anchors. + +The specification is located at https://godoc.org/github.com/russross/blackfriday#hdr-Sanitized_Anchor_Names. + +[`SanitizedAnchorName`](https://godoc.org/github.com/russross/blackfriday#SanitizedAnchorName) exposes this functionality, and can be used to +create compatible links to the anchor names generated by blackfriday. +This algorithm is also implemented in a small standalone package at +[`github.com/shurcooL/sanitized_anchor_name`](https://godoc.org/github.com/shurcooL/sanitized_anchor_name). It can be useful for clients +that want a small package and don't need full functionality of blackfriday. + Features -------- @@ -184,7 +246,7 @@ implements the following extensions: and supply a language (to make syntax highlighting simple). Just mark it like this: - ```go + ``` go func getTrue() bool { return true } @@ -193,6 +255,15 @@ implements the following extensions: You can use 3 or more backticks to mark the beginning of the block, and the same number to mark the end of the block. + To preserve classes of fenced code blocks while using the bluemonday + HTML sanitizer, use the following policy: + + ``` go + p := bluemonday.UGCPolicy() + p.AllowAttrs("class").Matching(regexp.MustCompile("^language-[a-zA-Z0-9]+$")).OnElements("code") + html := p.SanitizeBytes(unsafe) + ``` + * **Definition lists**. A simple definition list is made of a single-line term followed by a colon and the definition for that term. @@ -218,8 +289,10 @@ implements the following extensions: * **Strikethrough**. Use two tildes (`~~`) to mark text that should be crossed out. -* **Hard line breaks**. With this extension enabled newlines in the input - translate into line breaks in the output. This extension is off by default. +* **Hard line breaks**. With this extension enabled (it is off by + default in the `MarkdownBasic` and `MarkdownCommon` convenience + functions), newlines in the input translate into line breaks in + the output. * **Smart quotes**. Smartypants-style punctuation substitution is supported, turning normal double- and single-quote marks into @@ -258,15 +331,21 @@ are a few of note: * [LaTeX output](https://bitbucket.org/ambrevar/blackfriday-latex): renders output as LaTeX. +* [bfchroma](https://github.com/Depado/bfchroma/): provides convenience + integration with the [Chroma](https://github.com/alecthomas/chroma) code + highlighting library. bfchroma is only compatible with v2 of Blackfriday and + provides a drop-in renderer ready to use with Blackfriday, as well as + options and means for further customization. + -Todo +TODO ---- * More unit testing -* Improve unicode support. It does not understand all unicode +* Improve Unicode support. It does not understand all Unicode rules (about what constitutes a letter, a punctuation symbol, etc.), so it may fail to detect word boundaries correctly in - some instances. It is safe on all utf-8 input. + some instances. It is safe on all UTF-8 input. License @@ -281,3 +360,10 @@ License [4]: https://godoc.org/gopkg.in/russross/blackfriday.v2#Parse "Parse func" [5]: https://github.com/microcosm-cc/bluemonday "Bluemonday" [6]: https://labix.org/gopkg.in "gopkg.in" + [7]: https://github.com/golang/dep/ "dep" + [8]: https://github.com/Masterminds/glide "Glide" + + [BuildSVG]: https://travis-ci.org/russross/blackfriday.svg?branch=master + [BuildURL]: https://travis-ci.org/russross/blackfriday + [GodocV2SVG]: https://godoc.org/gopkg.in/russross/blackfriday.v2?status.svg + [GodocV2URL]: https://godoc.org/gopkg.in/russross/blackfriday.v2 diff --git a/vendor/github.com/russross/blackfriday/block.go b/vendor/github.com/russross/blackfriday/block.go index d7da33f2..45c21a6c 100644 --- a/vendor/github.com/russross/blackfriday/block.go +++ b/vendor/github.com/russross/blackfriday/block.go @@ -15,26 +15,18 @@ package blackfriday import ( "bytes" - "html" - "regexp" - - "github.com/shurcooL/sanitized_anchor_name" -) - -const ( - charEntity = "&(?:#x[a-f0-9]{1,8}|#[0-9]{1,8}|[a-z][a-z0-9]{1,31});" - escapable = "[!\"#$%&'()*+,./:;<=>?@[\\\\\\]^_`{|}~-]" -) - -var ( - reBackslashOrAmp = regexp.MustCompile("[\\&]") - reEntityOrEscapedChar = regexp.MustCompile("(?i)\\\\" + escapable + "|" + charEntity) + "strings" + "unicode" ) // Parse block-level data. // Note: this function and many that it calls assume that // the input buffer ends with a newline. -func (p *Markdown) block(data []byte) { +func (p *parser) block(out *bytes.Buffer, data []byte) { + if len(data) == 0 || data[len(data)-1] != '\n' { + panic("block input is missing terminating newline") + } + // this is called recursively: enforce a maximum depth if p.nesting >= p.maxNesting { return @@ -43,14 +35,14 @@ func (p *Markdown) block(data []byte) { // parse out one block-level construct at a time for len(data) > 0 { - // prefixed heading: + // prefixed header: // - // # Heading 1 - // ## Heading 2 + // # Header 1 + // ## Header 2 // ... - // ###### Heading 6 - if p.isPrefixHeading(data) { - data = data[p.prefixHeading(data):] + // ###### Header 6 + if p.isPrefixHeader(data) { + data = data[p.prefixHeader(out, data):] continue } @@ -60,7 +52,7 @@ func (p *Markdown) block(data []byte) { // ... // </div> if data[0] == '<' { - if i := p.html(data, true); i > 0 { + if i := p.html(out, data, true); i > 0 { data = data[i:] continue } @@ -71,9 +63,9 @@ func (p *Markdown) block(data []byte) { // % stuff // % more stuff // % even more stuff - if p.extensions&Titleblock != 0 { + if p.flags&EXTENSION_TITLEBLOCK != 0 { if data[0] == '%' { - if i := p.titleBlock(data, true); i > 0 { + if i := p.titleBlock(out, data, true); i > 0 { data = data[i:] continue } @@ -95,13 +87,13 @@ func (p *Markdown) block(data []byte) { // return b // } if p.codePrefix(data) > 0 { - data = data[p.code(data):] + data = data[p.code(out, data):] continue } // fenced code block: // - // ``` go + // ``` go info string here // func fact(n int) int { // if n <= 1 { // return n @@ -109,8 +101,8 @@ func (p *Markdown) block(data []byte) { // return n * fact(n-1) // } // ``` - if p.extensions&FencedCode != 0 { - if i := p.fencedCodeBlock(data, true); i > 0 { + if p.flags&EXTENSION_FENCED_CODE != 0 { + if i := p.fencedCodeBlock(out, data, true); i > 0 { data = data[i:] continue } @@ -124,9 +116,9 @@ func (p *Markdown) block(data []byte) { // or // ______ if p.isHRule(data) { - p.addBlock(HorizontalRule, nil) + p.r.HRule(out) var i int - for i = 0; i < len(data) && data[i] != '\n'; i++ { + for i = 0; data[i] != '\n'; i++ { } data = data[i:] continue @@ -137,7 +129,7 @@ func (p *Markdown) block(data []byte) { // > A big quote I found somewhere // > on the web if p.quotePrefix(data) > 0 { - data = data[p.quote(data):] + data = data[p.quote(out, data):] continue } @@ -147,8 +139,8 @@ func (p *Markdown) block(data []byte) { // ------|-----|--------- // Bob | 31 | 555-1234 // Alice | 27 | 555-4321 - if p.extensions&Tables != 0 { - if i := p.table(data); i > 0 { + if p.flags&EXTENSION_TABLES != 0 { + if i := p.table(out, data); i > 0 { data = data[i:] continue } @@ -161,7 +153,7 @@ func (p *Markdown) block(data []byte) { // // also works with + or - if p.uliPrefix(data) > 0 { - data = data[p.list(data, 0):] + data = data[p.list(out, data, 0):] continue } @@ -170,7 +162,7 @@ func (p *Markdown) block(data []byte) { // 1. Item 1 // 2. Item 2 if p.oliPrefix(data) > 0 { - data = data[p.list(data, ListTypeOrdered):] + data = data[p.list(out, data, LIST_TYPE_ORDERED):] continue } @@ -182,62 +174,55 @@ func (p *Markdown) block(data []byte) { // // Term 2 // : Definition c - if p.extensions&DefinitionLists != 0 { + if p.flags&EXTENSION_DEFINITION_LISTS != 0 { if p.dliPrefix(data) > 0 { - data = data[p.list(data, ListTypeDefinition):] + data = data[p.list(out, data, LIST_TYPE_DEFINITION):] continue } } // anything else must look like a normal paragraph - // note: this finds underlined headings, too - data = data[p.paragraph(data):] + // note: this finds underlined headers, too + data = data[p.paragraph(out, data):] } p.nesting-- } -func (p *Markdown) addBlock(typ NodeType, content []byte) *Node { - p.closeUnmatchedBlocks() - container := p.addChild(typ, 0) - container.content = content - return container -} - -func (p *Markdown) isPrefixHeading(data []byte) bool { +func (p *parser) isPrefixHeader(data []byte) bool { if data[0] != '#' { return false } - if p.extensions&SpaceHeadings != 0 { + if p.flags&EXTENSION_SPACE_HEADERS != 0 { level := 0 - for level < 6 && level < len(data) && data[level] == '#' { + for level < 6 && data[level] == '#' { level++ } - if level == len(data) || data[level] != ' ' { + if data[level] != ' ' { return false } } return true } -func (p *Markdown) prefixHeading(data []byte) int { +func (p *parser) prefixHeader(out *bytes.Buffer, data []byte) int { level := 0 - for level < 6 && level < len(data) && data[level] == '#' { + for level < 6 && data[level] == '#' { level++ } i := skipChar(data, level, ' ') end := skipUntilChar(data, i, '\n') skip := end id := "" - if p.extensions&HeadingIDs != 0 { + if p.flags&EXTENSION_HEADER_IDS != 0 { j, k := 0, 0 - // find start/end of heading id + // find start/end of header id for j = i; j < end-1 && (data[j] != '{' || data[j+1] != '#'); j++ { } for k = j + 1; k < end && data[k] != '}'; k++ { } - // extract heading id iff found + // extract header id iff found if j < end && k < end { id = string(data[j+2 : k]) end = j @@ -257,41 +242,45 @@ func (p *Markdown) prefixHeading(data []byte) int { end-- } if end > i { - if id == "" && p.extensions&AutoHeadingIDs != 0 { - id = sanitized_anchor_name.Create(string(data[i:end])) + if id == "" && p.flags&EXTENSION_AUTO_HEADER_IDS != 0 { + id = SanitizedAnchorName(string(data[i:end])) } - block := p.addBlock(Heading, data[i:end]) - block.HeadingID = id - block.Level = level + work := func() bool { + p.inline(out, data[i:end]) + return true + } + p.r.Header(out, work, level, id) } return skip } -func (p *Markdown) isUnderlinedHeading(data []byte) int { - // test of level 1 heading +func (p *parser) isUnderlinedHeader(data []byte) int { + // test of level 1 header if data[0] == '=' { i := skipChar(data, 1, '=') i = skipChar(data, i, ' ') - if i < len(data) && data[i] == '\n' { + if data[i] == '\n' { return 1 + } else { + return 0 } - return 0 } - // test of level 2 heading + // test of level 2 header if data[0] == '-' { i := skipChar(data, 1, '-') i = skipChar(data, i, ' ') - if i < len(data) && data[i] == '\n' { + if data[i] == '\n' { return 2 + } else { + return 0 } - return 0 } return 0 } -func (p *Markdown) titleBlock(data []byte, doRender bool) int { +func (p *parser) titleBlock(out *bytes.Buffer, data []byte, doRender bool) int { if data[0] != '%' { return 0 } @@ -305,17 +294,12 @@ func (p *Markdown) titleBlock(data []byte, doRender bool) int { } data = bytes.Join(splitData[0:i], []byte("\n")) - consumed := len(data) - data = bytes.TrimPrefix(data, []byte("% ")) - data = bytes.Replace(data, []byte("\n% "), []byte("\n"), -1) - block := p.addBlock(Heading, data) - block.Level = 1 - block.IsTitleblock = true - - return consumed + p.r.TitleBlock(out, data) + + return len(data) } -func (p *Markdown) html(data []byte, doRender bool) int { +func (p *parser) html(out *bytes.Buffer, data []byte, doRender bool) int { var i, j int // identify the opening tag @@ -327,12 +311,17 @@ func (p *Markdown) html(data []byte, doRender bool) int { // handle special cases if !tagfound { // check for an HTML comment - if size := p.htmlComment(data, doRender); size > 0 { + if size := p.htmlComment(out, data, doRender); size > 0 { return size } // check for an <hr> tag - if size := p.htmlHr(data, doRender); size > 0 { + if size := p.htmlHr(out, data, doRender); size > 0 { + return size + } + + // check for HTML CDATA + if size := p.htmlCDATA(out, data, doRender); size > 0 { return size } @@ -407,42 +396,60 @@ func (p *Markdown) html(data []byte, doRender bool) int { for end > 0 && data[end-1] == '\n' { end-- } - finalizeHTMLBlock(p.addBlock(HTMLBlock, data[:end])) + p.r.BlockHtml(out, data[:end]) } return i } -func finalizeHTMLBlock(block *Node) { - block.Literal = block.content - block.content = nil -} - -// HTML comment, lax form -func (p *Markdown) htmlComment(data []byte, doRender bool) int { - i := p.inlineHTMLComment(data) - // needs to end with a blank line - if j := p.isEmpty(data[i:]); j > 0 { - size := i + j +func (p *parser) renderHTMLBlock(out *bytes.Buffer, data []byte, start int, doRender bool) int { + // html block needs to end with a blank line + if i := p.isEmpty(data[start:]); i > 0 { + size := start + i if doRender { // trim trailing newlines end := size for end > 0 && data[end-1] == '\n' { end-- } - block := p.addBlock(HTMLBlock, data[:end]) - finalizeHTMLBlock(block) + p.r.BlockHtml(out, data[:end]) } return size } return 0 } -// HR, which is the only self-closing block tag considered -func (p *Markdown) htmlHr(data []byte, doRender bool) int { - if len(data) < 4 { +// HTML comment, lax form +func (p *parser) htmlComment(out *bytes.Buffer, data []byte, doRender bool) int { + i := p.inlineHTMLComment(out, data) + return p.renderHTMLBlock(out, data, i, doRender) +} + +// HTML CDATA section +func (p *parser) htmlCDATA(out *bytes.Buffer, data []byte, doRender bool) int { + const cdataTag = "<![cdata[" + const cdataTagLen = len(cdataTag) + if len(data) < cdataTagLen+1 { return 0 } + if !bytes.Equal(bytes.ToLower(data[:cdataTagLen]), []byte(cdataTag)) { + return 0 + } + i := cdataTagLen + // scan for an end-of-comment marker, across lines if necessary + for i < len(data) && !(data[i-2] == ']' && data[i-1] == ']' && data[i] == '>') { + i++ + } + i++ + // no end-of-comment marker + if i >= len(data) { + return 0 + } + return p.renderHTMLBlock(out, data, i, doRender) +} + +// HR, which is the only self-closing block tag considered +func (p *parser) htmlHr(out *bytes.Buffer, data []byte, doRender bool) int { if data[0] != '<' || (data[1] != 'h' && data[1] != 'H') || (data[2] != 'r' && data[2] != 'R') { return 0 } @@ -450,31 +457,22 @@ func (p *Markdown) htmlHr(data []byte, doRender bool) int { // not an <hr> tag after all; at least not a valid one return 0 } + i := 3 - for i < len(data) && data[i] != '>' && data[i] != '\n' { + for data[i] != '>' && data[i] != '\n' { i++ } - if i < len(data) && data[i] == '>' { - i++ - if j := p.isEmpty(data[i:]); j > 0 { - size := i + j - if doRender { - // trim newlines - end := size - for end > 0 && data[end-1] == '\n' { - end-- - } - finalizeHTMLBlock(p.addBlock(HTMLBlock, data[:end])) - } - return size - } + + if data[i] == '>' { + return p.renderHTMLBlock(out, data, i+1, doRender) } + return 0 } -func (p *Markdown) htmlFindTag(data []byte) (string, bool) { +func (p *parser) htmlFindTag(data []byte) (string, bool) { i := 0 - for i < len(data) && isalnum(data[i]) { + for isalnum(data[i]) { i++ } key := string(data[:i]) @@ -484,11 +482,9 @@ func (p *Markdown) htmlFindTag(data []byte) (string, bool) { return "", false } -func (p *Markdown) htmlFindEnd(tag string, data []byte) int { +func (p *parser) htmlFindEnd(tag string, data []byte) int { // assume data[0] == '<' && data[1] == '/' already tested - if tag == "hr" { - return 2 - } + // check if tag is a match closetag := []byte("</" + tag + ">") if !bytes.HasPrefix(data, closetag) { @@ -508,7 +504,7 @@ func (p *Markdown) htmlFindEnd(tag string, data []byte) int { return i } - if p.extensions&LaxHTMLBlocks != 0 { + if p.flags&EXTENSION_LAX_HTML_BLOCKS != 0 { return i } if skip = p.isEmpty(data[i:]); skip == 0 { @@ -519,7 +515,7 @@ func (p *Markdown) htmlFindEnd(tag string, data []byte) int { return i + skip } -func (*Markdown) isEmpty(data []byte) int { +func (*parser) isEmpty(data []byte) int { // it is okay to call isEmpty on an empty buffer if len(data) == 0 { return 0 @@ -531,13 +527,10 @@ func (*Markdown) isEmpty(data []byte) int { return 0 } } - if i < len(data) && data[i] == '\n' { - i++ - } - return i + return i + 1 } -func (*Markdown) isHRule(data []byte) bool { +func (*parser) isHRule(data []byte) bool { i := 0 // skip up to three spaces @@ -553,7 +546,7 @@ func (*Markdown) isHRule(data []byte) bool { // the whole line must be the char or whitespace n := 0 - for i < len(data) && data[i] != '\n' { + for data[i] != '\n' { switch { case data[i] == c: n++ @@ -569,7 +562,8 @@ func (*Markdown) isHRule(data []byte) bool { // isFenceLine checks if there's a fence line (e.g., ``` or ``` go) at the beginning of data, // and returns the end index if so, or 0 otherwise. It also returns the marker found. // If syntax is not nil, it gets set to the syntax specified in the fence line. -func isFenceLine(data []byte, syntax *string, oldmarker string) (end int, marker string) { +// A final newline is mandatory to recognize the fence line, unless newlineOptional is true. +func isFenceLine(data []byte, info *string, oldmarker string, newlineOptional bool) (end int, marker string) { i, size := 0, 0 // skip up to three spaces @@ -605,26 +599,26 @@ func isFenceLine(data []byte, syntax *string, oldmarker string) (end int, marker } // TODO(shurcooL): It's probably a good idea to simplify the 2 code paths here - // into one, always get the syntax, and discard it if the caller doesn't care. - if syntax != nil { - syn := 0 + // into one, always get the info string, and discard it if the caller doesn't care. + if info != nil { + infoLength := 0 i = skipChar(data, i, ' ') if i >= len(data) { - if i == len(data) { + if newlineOptional && i == len(data) { return i, marker } return 0, "" } - syntaxStart := i + infoStart := i if data[i] == '{' { i++ - syntaxStart++ + infoStart++ for i < len(data) && data[i] != '}' && data[i] != '\n' { - syn++ + infoLength++ i++ } @@ -634,55 +628,55 @@ func isFenceLine(data []byte, syntax *string, oldmarker string) (end int, marker // strip all whitespace at the beginning and the end // of the {} block - for syn > 0 && isspace(data[syntaxStart]) { - syntaxStart++ - syn-- + for infoLength > 0 && isspace(data[infoStart]) { + infoStart++ + infoLength-- } - for syn > 0 && isspace(data[syntaxStart+syn-1]) { - syn-- + for infoLength > 0 && isspace(data[infoStart+infoLength-1]) { + infoLength-- } i++ } else { - for i < len(data) && !isspace(data[i]) { - syn++ + for i < len(data) && !isverticalspace(data[i]) { + infoLength++ i++ } } - *syntax = string(data[syntaxStart : syntaxStart+syn]) + *info = strings.TrimSpace(string(data[infoStart : infoStart+infoLength])) } i = skipChar(data, i, ' ') if i >= len(data) || data[i] != '\n' { - if i == len(data) { + if newlineOptional && i == len(data) { return i, marker } return 0, "" } + return i + 1, marker // Take newline into account. } // fencedCodeBlock returns the end index if data contains a fenced code block at the beginning, // or 0 otherwise. It writes to out if doRender is true, otherwise it has no side effects. // If doRender is true, a final newline is mandatory to recognize the fenced code block. -func (p *Markdown) fencedCodeBlock(data []byte, doRender bool) int { - var syntax string - beg, marker := isFenceLine(data, &syntax, "") +func (p *parser) fencedCodeBlock(out *bytes.Buffer, data []byte, doRender bool) int { + var infoString string + beg, marker := isFenceLine(data, &infoString, "", false) if beg == 0 || beg >= len(data) { return 0 } var work bytes.Buffer - work.Write([]byte(syntax)) - work.WriteByte('\n') for { // safe to assume beg < len(data) // check for the end of the code block - fenceEnd, _ := isFenceLine(data[beg:], nil, marker) + newlineOptional := !doRender + fenceEnd, _ := isFenceLine(data[beg:], nil, marker, newlineOptional) if fenceEnd != 0 { beg += fenceEnd break @@ -704,55 +698,24 @@ func (p *Markdown) fencedCodeBlock(data []byte, doRender bool) int { } if doRender { - block := p.addBlock(CodeBlock, work.Bytes()) // TODO: get rid of temp buffer - block.IsFenced = true - finalizeCodeBlock(block) + p.r.BlockCode(out, work.Bytes(), infoString) } return beg } -func unescapeChar(str []byte) []byte { - if str[0] == '\\' { - return []byte{str[1]} - } - return []byte(html.UnescapeString(string(str))) -} - -func unescapeString(str []byte) []byte { - if reBackslashOrAmp.Match(str) { - return reEntityOrEscapedChar.ReplaceAllFunc(str, unescapeChar) - } - return str -} - -func finalizeCodeBlock(block *Node) { - if block.IsFenced { - newlinePos := bytes.IndexByte(block.content, '\n') - firstLine := block.content[:newlinePos] - rest := block.content[newlinePos+1:] - block.Info = unescapeString(bytes.Trim(firstLine, "\n")) - block.Literal = rest - } else { - block.Literal = block.content - } - block.content = nil -} - -func (p *Markdown) table(data []byte) int { - table := p.addBlock(Table, nil) - i, columns := p.tableHeader(data) +func (p *parser) table(out *bytes.Buffer, data []byte) int { + var header bytes.Buffer + i, columns := p.tableHeader(&header, data) if i == 0 { - p.tip = table.Parent - table.Unlink() return 0 } - p.addBlock(TableBody, nil) + var body bytes.Buffer for i < len(data) { pipes, rowStart := 0, i - for ; i < len(data) && data[i] != '\n'; i++ { + for ; data[i] != '\n'; i++ { if data[i] == '|' { pipes++ } @@ -764,12 +727,12 @@ func (p *Markdown) table(data []byte) int { } // include the newline in data sent to tableRow - if i < len(data) && data[i] == '\n' { - i++ - } - p.tableRow(data[rowStart:i], columns, false) + i++ + p.tableRow(&body, data[rowStart:i], columns, false) } + p.r.Table(out, header.Bytes(), body.Bytes(), columns) + return i } @@ -782,10 +745,10 @@ func isBackslashEscaped(data []byte, i int) bool { return backslashes&1 == 1 } -func (p *Markdown) tableHeader(data []byte) (size int, columns []CellAlignFlags) { +func (p *parser) tableHeader(out *bytes.Buffer, data []byte) (size int, columns []int) { i := 0 colCount := 1 - for i = 0; i < len(data) && data[i] != '\n'; i++ { + for i = 0; data[i] != '\n'; i++ { if data[i] == '|' && !isBackslashEscaped(data, i) { colCount++ } @@ -797,11 +760,7 @@ func (p *Markdown) tableHeader(data []byte) (size int, columns []CellAlignFlags) } // include the newline in the data sent to tableRow - j := i - if j < len(data) && data[j] == '\n' { - j++ - } - header := data[:j] + header := data[:i+1] // column count ignores pipes at beginning or end of line if data[0] == '|' { @@ -811,7 +770,7 @@ func (p *Markdown) tableHeader(data []byte) (size int, columns []CellAlignFlags) colCount-- } - columns = make([]CellAlignFlags, colCount) + columns = make([]int, colCount) // move on to the header underline i++ @@ -827,29 +786,27 @@ func (p *Markdown) tableHeader(data []byte) (size int, columns []CellAlignFlags) // each column header is of form: / *:?-+:? *|/ with # dashes + # colons >= 3 // and trailing | optional on last column col := 0 - for i < len(data) && data[i] != '\n' { + for data[i] != '\n' { dashes := 0 if data[i] == ':' { i++ - columns[col] |= TableAlignmentLeft + columns[col] |= TABLE_ALIGNMENT_LEFT dashes++ } - for i < len(data) && data[i] == '-' { + for data[i] == '-' { i++ dashes++ } - if i < len(data) && data[i] == ':' { + if data[i] == ':' { i++ - columns[col] |= TableAlignmentRight + columns[col] |= TABLE_ALIGNMENT_RIGHT dashes++ } - for i < len(data) && data[i] == ' ' { + for data[i] == ' ' { i++ } - if i == len(data) { - return - } + // end of column test is messy switch { case dashes < 3: @@ -860,12 +817,12 @@ func (p *Markdown) tableHeader(data []byte) (size int, columns []CellAlignFlags) // marker found, now skip past trailing whitespace col++ i++ - for i < len(data) && data[i] == ' ' { + for data[i] == ' ' { i++ } // trailing junk found after last column - if col >= colCount && i < len(data) && data[i] != '\n' { + if col >= colCount && data[i] != '\n' { return } @@ -886,31 +843,27 @@ func (p *Markdown) tableHeader(data []byte) (size int, columns []CellAlignFlags) return } - p.addBlock(TableHead, nil) - p.tableRow(header, columns, true) - size = i - if size < len(data) && data[size] == '\n' { - size++ - } + p.tableRow(out, header, columns, true) + size = i + 1 return } -func (p *Markdown) tableRow(data []byte, columns []CellAlignFlags, header bool) { - p.addBlock(TableRow, nil) +func (p *parser) tableRow(out *bytes.Buffer, data []byte, columns []int, header bool) { i, col := 0, 0 + var rowWork bytes.Buffer if data[i] == '|' && !isBackslashEscaped(data, i) { i++ } for col = 0; col < len(columns) && i < len(data); col++ { - for i < len(data) && data[i] == ' ' { + for data[i] == ' ' { i++ } cellStart := i - for i < len(data) && (data[i] != '|' || isBackslashEscaped(data, i)) && data[i] != '\n' { + for (data[i] != '|' || isBackslashEscaped(data, i)) && data[i] != '\n' { i++ } @@ -919,33 +872,42 @@ func (p *Markdown) tableRow(data []byte, columns []CellAlignFlags, header bool) // skip the end-of-cell marker, possibly taking us past end of buffer i++ - for cellEnd > cellStart && cellEnd-1 < len(data) && data[cellEnd-1] == ' ' { + for cellEnd > cellStart && data[cellEnd-1] == ' ' { cellEnd-- } - cell := p.addBlock(TableCell, data[cellStart:cellEnd]) - cell.IsHeader = header - cell.Align = columns[col] + var cellWork bytes.Buffer + p.inline(&cellWork, data[cellStart:cellEnd]) + + if header { + p.r.TableHeaderCell(&rowWork, cellWork.Bytes(), columns[col]) + } else { + p.r.TableCell(&rowWork, cellWork.Bytes(), columns[col]) + } } // pad it out with empty columns to get the right number for ; col < len(columns); col++ { - cell := p.addBlock(TableCell, nil) - cell.IsHeader = header - cell.Align = columns[col] + if header { + p.r.TableHeaderCell(&rowWork, nil, columns[col]) + } else { + p.r.TableCell(&rowWork, nil, columns[col]) + } } // silently ignore rows with too many cells + + p.r.TableRow(out, rowWork.Bytes()) } // returns blockquote prefix length -func (p *Markdown) quotePrefix(data []byte) int { +func (p *parser) quotePrefix(data []byte) int { i := 0 - for i < 3 && i < len(data) && data[i] == ' ' { + for i < 3 && data[i] == ' ' { i++ } - if i < len(data) && data[i] == '>' { - if i+1 < len(data) && data[i+1] == ' ' { + if data[i] == '>' { + if data[i+1] == ' ' { return i + 2 } return i + 1 @@ -955,7 +917,7 @@ func (p *Markdown) quotePrefix(data []byte) int { // blockquote ends with at least one blank line // followed by something without a blockquote prefix -func (p *Markdown) terminateBlockquote(data []byte, beg, end int) bool { +func (p *parser) terminateBlockquote(data []byte, beg, end int) bool { if p.isEmpty(data[beg:]) <= 0 { return false } @@ -966,8 +928,7 @@ func (p *Markdown) terminateBlockquote(data []byte, beg, end int) bool { } // parse a blockquote fragment -func (p *Markdown) quote(data []byte) int { - block := p.addBlock(BlockQuote, nil) +func (p *parser) quote(out *bytes.Buffer, data []byte) int { var raw bytes.Buffer beg, end := 0, 0 for beg < len(data) { @@ -975,9 +936,9 @@ func (p *Markdown) quote(data []byte) int { // Step over whole lines, collecting them. While doing that, check for // fenced code and if one's found, incorporate it altogether, // irregardless of any contents inside it - for end < len(data) && data[end] != '\n' { - if p.extensions&FencedCode != 0 { - if i := p.fencedCodeBlock(data[end:], false); i > 0 { + for data[end] != '\n' { + if p.flags&EXTENSION_FENCED_CODE != 0 { + if i := p.fencedCodeBlock(out, data[end:], false); i > 0 { // -1 to compensate for the extra end++ after the loop: end += i - 1 break @@ -985,47 +946,44 @@ func (p *Markdown) quote(data []byte) int { } end++ } - if end < len(data) && data[end] == '\n' { - end++ - } + end++ + if pre := p.quotePrefix(data[beg:]); pre > 0 { // skip the prefix beg += pre } else if p.terminateBlockquote(data, beg, end) { break } + // this line is part of the blockquote raw.Write(data[beg:end]) beg = end } - p.block(raw.Bytes()) - p.finalize(block) + + var cooked bytes.Buffer + p.block(&cooked, raw.Bytes()) + p.r.BlockQuote(out, cooked.Bytes()) return end } // returns prefix length for block code -func (p *Markdown) codePrefix(data []byte) int { - if len(data) >= 1 && data[0] == '\t' { - return 1 - } - if len(data) >= 4 && data[0] == ' ' && data[1] == ' ' && data[2] == ' ' && data[3] == ' ' { +func (p *parser) codePrefix(data []byte) int { + if data[0] == ' ' && data[1] == ' ' && data[2] == ' ' && data[3] == ' ' { return 4 } return 0 } -func (p *Markdown) code(data []byte) int { +func (p *parser) code(out *bytes.Buffer, data []byte) int { var work bytes.Buffer i := 0 for i < len(data) { beg := i - for i < len(data) && data[i] != '\n' { - i++ - } - if i < len(data) && data[i] == '\n' { + for data[i] != '\n' { i++ } + i++ blankline := p.isEmpty(data[beg:i]) > 0 if pre := p.codePrefix(data[beg:i]); pre > 0 { @@ -1036,7 +994,7 @@ func (p *Markdown) code(data []byte) int { break } - // verbatim copy to the working buffer + // verbatim copy to the working buffeu if blankline { work.WriteByte('\n') } else { @@ -1056,183 +1014,122 @@ func (p *Markdown) code(data []byte) int { work.WriteByte('\n') - block := p.addBlock(CodeBlock, work.Bytes()) // TODO: get rid of temp buffer - block.IsFenced = false - finalizeCodeBlock(block) + p.r.BlockCode(out, work.Bytes(), "") return i } // returns unordered list item prefix -func (p *Markdown) uliPrefix(data []byte) int { +func (p *parser) uliPrefix(data []byte) int { i := 0 + // start with up to 3 spaces - for i < len(data) && i < 3 && data[i] == ' ' { + for i < 3 && data[i] == ' ' { i++ } - if i >= len(data)-1 { - return 0 - } - // need one of {'*', '+', '-'} followed by a space or a tab + + // need a *, +, or - followed by a space if (data[i] != '*' && data[i] != '+' && data[i] != '-') || - (data[i+1] != ' ' && data[i+1] != '\t') { + data[i+1] != ' ' { return 0 } return i + 2 } // returns ordered list item prefix -func (p *Markdown) oliPrefix(data []byte) int { +func (p *parser) oliPrefix(data []byte) int { i := 0 // start with up to 3 spaces - for i < 3 && i < len(data) && data[i] == ' ' { + for i < 3 && data[i] == ' ' { i++ } // count the digits start := i - for i < len(data) && data[i] >= '0' && data[i] <= '9' { + for data[i] >= '0' && data[i] <= '9' { i++ } - if start == i || i >= len(data)-1 { - return 0 - } - // we need >= 1 digits followed by a dot and a space or a tab - if data[i] != '.' || !(data[i+1] == ' ' || data[i+1] == '\t') { + // we need >= 1 digits followed by a dot and a space + if start == i || data[i] != '.' || data[i+1] != ' ' { return 0 } return i + 2 } // returns definition list item prefix -func (p *Markdown) dliPrefix(data []byte) int { - if len(data) < 2 { - return 0 - } +func (p *parser) dliPrefix(data []byte) int { i := 0 - // need a ':' followed by a space or a tab - if data[i] != ':' || !(data[i+1] == ' ' || data[i+1] == '\t') { + + // need a : followed by a spaces + if data[i] != ':' || data[i+1] != ' ' { return 0 } - for i < len(data) && data[i] == ' ' { + for data[i] == ' ' { i++ } return i + 2 } // parse ordered or unordered list block -func (p *Markdown) list(data []byte, flags ListType) int { +func (p *parser) list(out *bytes.Buffer, data []byte, flags int) int { i := 0 - flags |= ListItemBeginningOfList - block := p.addBlock(List, nil) - block.ListFlags = flags - block.Tight = true - - for i < len(data) { - skip := p.listItem(data[i:], &flags) - if flags&ListItemContainsBlock != 0 { - block.ListData.Tight = false - } - i += skip - if skip == 0 || flags&ListItemEndOfList != 0 { - break - } - flags &= ^ListItemBeginningOfList - } - - above := block.Parent - finalizeList(block) - p.tip = above - return i -} - -// Returns true if block ends with a blank line, descending if needed -// into lists and sublists. -func endsWithBlankLine(block *Node) bool { - // TODO: figure this out. Always false now. - for block != nil { - //if block.lastLineBlank { - //return true - //} - t := block.Type - if t == List || t == Item { - block = block.LastChild - } else { - break - } - } - return false -} + flags |= LIST_ITEM_BEGINNING_OF_LIST + work := func() bool { + for i < len(data) { + skip := p.listItem(out, data[i:], &flags) + i += skip -func finalizeList(block *Node) { - block.open = false - item := block.FirstChild - for item != nil { - // check for non-final list item ending with blank line: - if endsWithBlankLine(item) && item.Next != nil { - block.ListData.Tight = false - break - } - // recurse into children of list item, to see if there are spaces - // between any of them: - subItem := item.FirstChild - for subItem != nil { - if endsWithBlankLine(subItem) && (item.Next != nil || subItem.Next != nil) { - block.ListData.Tight = false + if skip == 0 || flags&LIST_ITEM_END_OF_LIST != 0 { break } - subItem = subItem.Next + flags &= ^LIST_ITEM_BEGINNING_OF_LIST } - item = item.Next + return true } + + p.r.List(out, work, flags) + return i } // Parse a single list item. // Assumes initial prefix is already removed if this is a sublist. -func (p *Markdown) listItem(data []byte, flags *ListType) int { +func (p *parser) listItem(out *bytes.Buffer, data []byte, flags *int) int { // keep track of the indentation of the first line itemIndent := 0 - if data[0] == '\t' { - itemIndent += 4 - } else { - for itemIndent < 3 && data[itemIndent] == ' ' { - itemIndent++ - } + for itemIndent < 3 && data[itemIndent] == ' ' { + itemIndent++ } - var bulletChar byte = '*' i := p.uliPrefix(data) if i == 0 { i = p.oliPrefix(data) - } else { - bulletChar = data[i-2] } if i == 0 { i = p.dliPrefix(data) // reset definition term flag if i > 0 { - *flags &= ^ListTypeTerm + *flags &= ^LIST_TYPE_TERM } } if i == 0 { - // if in definition list, set term flag and continue - if *flags&ListTypeDefinition != 0 { - *flags |= ListTypeTerm + // if in defnition list, set term flag and continue + if *flags&LIST_TYPE_DEFINITION != 0 { + *flags |= LIST_TYPE_TERM } else { return 0 } } // skip leading whitespace on first line - for i < len(data) && data[i] == ' ' { + for data[i] == ' ' { i++ } // find the end of the line line := i - for i > 0 && i < len(data) && data[i-1] != '\n' { + for i > 0 && data[i-1] != '\n' { i++ } @@ -1246,13 +1143,14 @@ func (p *Markdown) listItem(data []byte, flags *ListType) int { // process the following lines containsBlankLine := false sublist := 0 + codeBlockMarker := "" gatherlines: for line < len(data) { i++ // find the end of this line - for i < len(data) && data[i-1] != '\n' { + for data[i-1] != '\n' { i++ } @@ -1260,24 +1158,40 @@ gatherlines: // and move on to the next line if p.isEmpty(data[line:i]) > 0 { containsBlankLine = true + raw.Write(data[line:i]) line = i continue } // calculate the indentation indent := 0 - indentIndex := 0 - if data[line] == '\t' { - indentIndex++ - indent += 4 - } else { - for indent < 4 && line+indent < i && data[line+indent] == ' ' { - indent++ - indentIndex++ - } + for indent < 4 && line+indent < i && data[line+indent] == ' ' { + indent++ } - chunk := data[line+indentIndex : i] + chunk := data[line+indent : i] + + if p.flags&EXTENSION_FENCED_CODE != 0 { + // determine if in or out of codeblock + // if in codeblock, ignore normal list processing + _, marker := isFenceLine(chunk, nil, codeBlockMarker, false) + if marker != "" { + if codeBlockMarker == "" { + // start of codeblock + codeBlockMarker = marker + } else { + // end of codeblock. + *flags |= LIST_ITEM_CONTAINS_BLOCK + codeBlockMarker = "" + } + } + // we are in a codeblock, write line, and continue + if codeBlockMarker != "" || marker != "" { + raw.Write(data[line+indent : i]) + line = i + continue gatherlines + } + } // evaluate how this line fits in switch { @@ -1287,7 +1201,15 @@ gatherlines: p.dliPrefix(chunk) > 0: if containsBlankLine { - *flags |= ListItemContainsBlock + // end the list if the type changed after a blank line + if indent <= itemIndent && + ((*flags&LIST_TYPE_ORDERED != 0 && p.uliPrefix(chunk) > 0) || + (*flags&LIST_TYPE_ORDERED == 0 && p.oliPrefix(chunk) > 0)) { + + *flags |= LIST_ITEM_END_OF_LIST + break gatherlines + } + *flags |= LIST_ITEM_CONTAINS_BLOCK } // to be a nested list, it must be indented more @@ -1301,89 +1223,93 @@ gatherlines: sublist = raw.Len() } - // is this a nested prefix heading? - case p.isPrefixHeading(chunk): - // if the heading is not indented, it is not nested in the list + // is this a nested prefix header? + case p.isPrefixHeader(chunk): + // if the header is not indented, it is not nested in the list // and thus ends the list if containsBlankLine && indent < 4 { - *flags |= ListItemEndOfList + *flags |= LIST_ITEM_END_OF_LIST break gatherlines } - *flags |= ListItemContainsBlock + *flags |= LIST_ITEM_CONTAINS_BLOCK // anything following an empty line is only part // of this item if it is indented 4 spaces // (regardless of the indentation of the beginning of the item) case containsBlankLine && indent < 4: - if *flags&ListTypeDefinition != 0 && i < len(data)-1 { + if *flags&LIST_TYPE_DEFINITION != 0 && i < len(data)-1 { // is the next item still a part of this list? next := i - for next < len(data) && data[next] != '\n' { + for data[next] != '\n' { next++ } for next < len(data)-1 && data[next] == '\n' { next++ } if i < len(data)-1 && data[i] != ':' && data[next] != ':' { - *flags |= ListItemEndOfList + *flags |= LIST_ITEM_END_OF_LIST } } else { - *flags |= ListItemEndOfList + *flags |= LIST_ITEM_END_OF_LIST } break gatherlines // a blank line means this should be parsed as a block case containsBlankLine: - raw.WriteByte('\n') - *flags |= ListItemContainsBlock + *flags |= LIST_ITEM_CONTAINS_BLOCK } - // if this line was preceded by one or more blanks, - // re-introduce the blank into the buffer - if containsBlankLine { - containsBlankLine = false - raw.WriteByte('\n') - } + containsBlankLine = false // add the line into the working buffer without prefix - raw.Write(data[line+indentIndex : i]) + raw.Write(data[line+indent : i]) line = i } - rawBytes := raw.Bytes() + // If reached end of data, the Renderer.ListItem call we're going to make below + // is definitely the last in the list. + if line >= len(data) { + *flags |= LIST_ITEM_END_OF_LIST + } - block := p.addBlock(Item, nil) - block.ListFlags = *flags - block.Tight = false - block.BulletChar = bulletChar - block.Delimiter = '.' // Only '.' is possible in Markdown, but ')' will also be possible in CommonMark + rawBytes := raw.Bytes() // render the contents of the list item - if *flags&ListItemContainsBlock != 0 && *flags&ListTypeTerm == 0 { + var cooked bytes.Buffer + if *flags&LIST_ITEM_CONTAINS_BLOCK != 0 && *flags&LIST_TYPE_TERM == 0 { // intermediate render of block item, except for definition term if sublist > 0 { - p.block(rawBytes[:sublist]) - p.block(rawBytes[sublist:]) + p.block(&cooked, rawBytes[:sublist]) + p.block(&cooked, rawBytes[sublist:]) } else { - p.block(rawBytes) + p.block(&cooked, rawBytes) } } else { // intermediate render of inline item if sublist > 0 { - child := p.addChild(Paragraph, 0) - child.content = rawBytes[:sublist] - p.block(rawBytes[sublist:]) + p.inline(&cooked, rawBytes[:sublist]) + p.block(&cooked, rawBytes[sublist:]) } else { - child := p.addChild(Paragraph, 0) - child.content = rawBytes + p.inline(&cooked, rawBytes) } } + + // render the actual list item + cookedBytes := cooked.Bytes() + parsedEnd := len(cookedBytes) + + // strip trailing newlines + for parsedEnd > 0 && cookedBytes[parsedEnd-1] == '\n' { + parsedEnd-- + } + p.r.ListItem(out, cookedBytes[:parsedEnd], *flags) + return line } // render a single paragraph that has already been parsed out -func (p *Markdown) renderParagraph(data []byte) { +func (p *parser) renderParagraph(out *bytes.Buffer, data []byte) { if len(data) == 0 { return } @@ -1394,29 +1320,27 @@ func (p *Markdown) renderParagraph(data []byte) { beg++ } - end := len(data) // trim trailing newline - if data[len(data)-1] == '\n' { - end-- - } + end := len(data) - 1 // trim trailing spaces for end > beg && data[end-1] == ' ' { end-- } - p.addBlock(Paragraph, data[beg:end]) + work := func() bool { + p.inline(out, data[beg:end]) + return true + } + p.r.Paragraph(out, work) } -func (p *Markdown) paragraph(data []byte) int { +func (p *parser) paragraph(out *bytes.Buffer, data []byte) int { // prev: index of 1st char of previous line // line: index of 1st char of current line // i: index of cursor/end of current line var prev, line, i int - tabSize := TabSizeDefault - if p.extensions&TabSizeEight != 0 { - tabSize = TabSizeDouble - } + // keep going until we find something to mark the end of the paragraph for i < len(data) { // mark the beginning of the current line @@ -1424,32 +1348,24 @@ func (p *Markdown) paragraph(data []byte) int { current := data[i:] line = i - // did we find a reference or a footnote? If so, end a paragraph - // preceding it and report that we have consumed up to the end of that - // reference: - if refEnd := isReference(p, current, tabSize); refEnd > 0 { - p.renderParagraph(data[:i]) - return i + refEnd - } - // did we find a blank line marking the end of the paragraph? if n := p.isEmpty(current); n > 0 { // did this blank line followed by a definition list item? - if p.extensions&DefinitionLists != 0 { + if p.flags&EXTENSION_DEFINITION_LISTS != 0 { if i < len(data)-1 && data[i+1] == ':' { - return p.list(data[prev:], ListTypeDefinition) + return p.list(out, data[prev:], LIST_TYPE_DEFINITION) } } - p.renderParagraph(data[:i]) + p.renderParagraph(out, data[:i]) return i + n } - // an underline under some text marks a heading, so our paragraph ended on prev line + // an underline under some text marks a header, so our paragraph ended on prev line if i > 0 { - if level := p.isUnderlinedHeading(current); level > 0 { + if level := p.isUnderlinedHeader(current); level > 0 { // render the paragraph - p.renderParagraph(data[:prev]) + p.renderParagraph(out, data[:prev]) // ignore leading and trailing whitespace eol := i - 1 @@ -1460,17 +1376,24 @@ func (p *Markdown) paragraph(data []byte) int { eol-- } + // render the header + // this ugly double closure avoids forcing variables onto the heap + work := func(o *bytes.Buffer, pp *parser, d []byte) func() bool { + return func() bool { + pp.inline(o, d) + return true + } + }(out, p, data[prev:eol]) + id := "" - if p.extensions&AutoHeadingIDs != 0 { - id = sanitized_anchor_name.Create(string(data[prev:eol])) + if p.flags&EXTENSION_AUTO_HEADER_IDS != 0 { + id = SanitizedAnchorName(string(data[prev:eol])) } - block := p.addBlock(Heading, data[prev:eol]) - block.Level = level - block.HeadingID = id + p.r.Header(out, work, level, id) // find the end of the underline - for i < len(data) && data[i] != '\n' { + for data[i] != '\n' { i++ } return i @@ -1478,72 +1401,74 @@ func (p *Markdown) paragraph(data []byte) int { } // if the next line starts a block of HTML, then the paragraph ends here - if p.extensions&LaxHTMLBlocks != 0 { - if data[i] == '<' && p.html(current, false) > 0 { + if p.flags&EXTENSION_LAX_HTML_BLOCKS != 0 { + if data[i] == '<' && p.html(out, current, false) > 0 { // rewind to before the HTML block - p.renderParagraph(data[:i]) + p.renderParagraph(out, data[:i]) return i } } - // if there's a prefixed heading or a horizontal rule after this, paragraph is over - if p.isPrefixHeading(current) || p.isHRule(current) { - p.renderParagraph(data[:i]) + // if there's a prefixed header or a horizontal rule after this, paragraph is over + if p.isPrefixHeader(current) || p.isHRule(current) { + p.renderParagraph(out, data[:i]) return i } // if there's a fenced code block, paragraph is over - if p.extensions&FencedCode != 0 { - if p.fencedCodeBlock(current, false) > 0 { - p.renderParagraph(data[:i]) + if p.flags&EXTENSION_FENCED_CODE != 0 { + if p.fencedCodeBlock(out, current, false) > 0 { + p.renderParagraph(out, data[:i]) return i } } // if there's a definition list item, prev line is a definition term - if p.extensions&DefinitionLists != 0 { + if p.flags&EXTENSION_DEFINITION_LISTS != 0 { if p.dliPrefix(current) != 0 { - ret := p.list(data[prev:], ListTypeDefinition) - return ret + return p.list(out, data[prev:], LIST_TYPE_DEFINITION) } } // if there's a list after this, paragraph is over - if p.extensions&NoEmptyLineBeforeBlock != 0 { + if p.flags&EXTENSION_NO_EMPTY_LINE_BEFORE_BLOCK != 0 { if p.uliPrefix(current) != 0 || p.oliPrefix(current) != 0 || p.quotePrefix(current) != 0 || p.codePrefix(current) != 0 { - p.renderParagraph(data[:i]) + p.renderParagraph(out, data[:i]) return i } } // otherwise, scan to the beginning of the next line - nl := bytes.IndexByte(data[i:], '\n') - if nl >= 0 { - i += nl + 1 - } else { - i += len(data[i:]) + for data[i] != '\n' { + i++ } - } - - p.renderParagraph(data[:i]) - return i -} - -func skipChar(data []byte, start int, char byte) int { - i := start - for i < len(data) && data[i] == char { i++ } + + p.renderParagraph(out, data[:i]) return i } -func skipUntilChar(text []byte, start int, char byte) int { - i := start - for i < len(text) && text[i] != char { - i++ +// SanitizedAnchorName returns a sanitized anchor name for the given text. +// +// It implements the algorithm specified in the package comment. +func SanitizedAnchorName(text string) string { + var anchorName []rune + futureDash := false + for _, r := range text { + switch { + case unicode.IsLetter(r) || unicode.IsNumber(r): + if futureDash && len(anchorName) > 0 { + anchorName = append(anchorName, '-') + } + futureDash = false + anchorName = append(anchorName, unicode.ToLower(r)) + default: + futureDash = true + } } - return i + return string(anchorName) } diff --git a/vendor/github.com/russross/blackfriday/doc.go b/vendor/github.com/russross/blackfriday/doc.go index 5b3fa987..9656c42a 100644 --- a/vendor/github.com/russross/blackfriday/doc.go +++ b/vendor/github.com/russross/blackfriday/doc.go @@ -1,18 +1,32 @@ -// Package blackfriday is a markdown processor. +// Package blackfriday is a Markdown processor. // -// It translates plain text with simple formatting rules into an AST, which can -// then be further processed to HTML (provided by Blackfriday itself) or other -// formats (provided by the community). +// It translates plain text with simple formatting rules into HTML or LaTeX. // -// The simplest way to invoke Blackfriday is to call the Run function. It will -// take a text input and produce a text output in HTML (or other format). +// Sanitized Anchor Names // -// A slightly more sophisticated way to use Blackfriday is to create a Markdown -// processor and to call Parse, which returns a syntax tree for the input -// document. You can leverage Blackfriday's parsing for content extraction from -// markdown documents. You can assign a custom renderer and set various options -// to the Markdown processor. +// Blackfriday includes an algorithm for creating sanitized anchor names +// corresponding to a given input text. This algorithm is used to create +// anchors for headings when EXTENSION_AUTO_HEADER_IDS is enabled. The +// algorithm is specified below, so that other packages can create +// compatible anchor names and links to those anchors. // -// If you're interested in calling Blackfriday from command line, see -// https://github.com/russross/blackfriday-tool. +// The algorithm iterates over the input text, interpreted as UTF-8, +// one Unicode code point (rune) at a time. All runes that are letters (category L) +// or numbers (category N) are considered valid characters. They are mapped to +// lower case, and included in the output. All other runes are considered +// invalid characters. Invalid characters that preceed the first valid character, +// as well as invalid character that follow the last valid character +// are dropped completely. All other sequences of invalid characters +// between two valid characters are replaced with a single dash character '-'. +// +// SanitizedAnchorName exposes this functionality, and can be used to +// create compatible links to the anchor names generated by blackfriday. +// This algorithm is also implemented in a small standalone package at +// github.com/shurcooL/sanitized_anchor_name. It can be useful for clients +// that want a small package and don't need full functionality of blackfriday. package blackfriday + +// NOTE: Keep Sanitized Anchor Name algorithm in sync with package +// github.com/shurcooL/sanitized_anchor_name. +// Otherwise, users of sanitized_anchor_name will get anchor names +// that are incompatible with those generated by blackfriday. diff --git a/vendor/github.com/russross/blackfriday/esc.go b/vendor/github.com/russross/blackfriday/esc.go deleted file mode 100644 index 6385f27c..00000000 --- a/vendor/github.com/russross/blackfriday/esc.go +++ /dev/null @@ -1,34 +0,0 @@ -package blackfriday - -import ( - "html" - "io" -) - -var htmlEscaper = [256][]byte{ - '&': []byte("&"), - '<': []byte("<"), - '>': []byte(">"), - '"': []byte("""), -} - -func escapeHTML(w io.Writer, s []byte) { - var start, end int - for end < len(s) { - escSeq := htmlEscaper[s[end]] - if escSeq != nil { - w.Write(s[start:end]) - w.Write(escSeq) - start = end + 1 - } - end++ - } - if start < len(s) && end <= len(s) { - w.Write(s[start:end]) - } -} - -func escLink(w io.Writer, text []byte) { - unesc := html.UnescapeString(string(text)) - escapeHTML(w, []byte(unesc)) -} diff --git a/vendor/github.com/russross/blackfriday/go.mod b/vendor/github.com/russross/blackfriday/go.mod new file mode 100644 index 00000000..b05561a0 --- /dev/null +++ b/vendor/github.com/russross/blackfriday/go.mod @@ -0,0 +1 @@ +module github.com/russross/blackfriday diff --git a/vendor/github.com/russross/blackfriday/html.go b/vendor/github.com/russross/blackfriday/html.go index 25fb185e..e0a6c69c 100644 --- a/vendor/github.com/russross/blackfriday/html.go +++ b/vendor/github.com/russross/blackfriday/html.go @@ -18,62 +18,46 @@ package blackfriday import ( "bytes" "fmt" - "io" "regexp" + "strconv" "strings" ) -// HTMLFlags control optional behavior of HTML renderer. -type HTMLFlags int - -// HTML renderer configuration options. +// Html renderer configuration options. const ( - HTMLFlagsNone HTMLFlags = 0 - SkipHTML HTMLFlags = 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 - 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 + HTML_SKIP_HTML = 1 << iota // skip preformatted HTML blocks + HTML_SKIP_STYLE // skip embedded <style> elements + HTML_SKIP_IMAGES // skip embedded images + HTML_SKIP_LINKS // skip all links + HTML_SAFELINK // only link to trusted protocols + HTML_NOFOLLOW_LINKS // only link with rel="nofollow" + HTML_NOREFERRER_LINKS // only link with rel="noreferrer" + HTML_HREF_TARGET_BLANK // add a blank target + HTML_TOC // generate a table of contents + HTML_OMIT_CONTENTS // skip the main contents (for a standalone table of contents) + HTML_COMPLETE_PAGE // generate a complete HTML page + HTML_USE_XHTML // generate XHTML output instead of HTML + HTML_USE_SMARTYPANTS // enable smart punctuation substitutions + HTML_SMARTYPANTS_FRACTIONS // enable smart fractions (with HTML_USE_SMARTYPANTS) + HTML_SMARTYPANTS_DASHES // enable smart dashes (with HTML_USE_SMARTYPANTS) + HTML_SMARTYPANTS_LATEX_DASHES // enable LaTeX-style dashes (with HTML_USE_SMARTYPANTS and HTML_SMARTYPANTS_DASHES) + HTML_SMARTYPANTS_ANGLED_QUOTES // enable angled double quotes (with HTML_USE_SMARTYPANTS) for double quotes rendering + HTML_SMARTYPANTS_QUOTES_NBSP // enable "French guillemets" (with HTML_USE_SMARTYPANTS) + HTML_FOOTNOTE_RETURN_LINKS // generate a link at the end of a footnote to return to the source ) var ( - htmlTagRe = regexp.MustCompile("(?i)^" + htmlTag) -) + alignments = []string{ + "left", + "right", + "center", + } -const ( - htmlTag = "(?:" + openTag + "|" + closeTag + "|" + htmlComment + "|" + - processingInstruction + "|" + declaration + "|" + cdata + ")" - closeTag = "</" + tagName + "\\s*[>]" - 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 = "<!\\[CDATA\\[[\\s\\S]*?\\]\\]>" - declaration = "<![A-Z]+" + "\\s+[^>]*>" - doubleQuotedValue = "\"[^\"]*\"" - htmlComment = "<!---->|<!--(?:-?[^>-])(?:-?[^-])*-->" - processingInstruction = "[<][?].*?[?][>]" - singleQuotedValue = "'[^']*'" - tagName = "[A-Za-z][A-Za-z0-9-]*" - unquotedValue = "[^\"'=<>`\\x00-\\x20]+" + // TODO: improve this regexp to catch all possible entities: + htmlEntity = regexp.MustCompile(`&[a-z]{2,5};`) ) -// HTMLRendererParameters is a collection of supplementary parameters tweaking -// the behavior of various parts of HTML renderer. -type HTMLRendererParameters struct { +type HtmlRendererParameters struct { // Prepend this text to each relative URL. AbsolutePrefix string // Add this text to each footnote anchor, to ensure uniqueness. @@ -82,34 +66,34 @@ type HTMLRendererParameters struct { // HTML_FOOTNOTE_RETURN_LINKS flag is enabled. If blank, the string // <sup>[return]</sup> is used. FootnoteReturnLinkContents string - // If set, add this text to the front of each Heading ID, to ensure + // If set, add this text to the front of each Header 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) - - Flags HTMLFlags // Flags allow customizing this renderer's behavior + HeaderIDPrefix string + // If set, add this text to the back of each Header ID, to ensure uniqueness. + HeaderIDSuffix string } -// HTMLRenderer is a type that implements the Renderer interface for HTML output. +// Html is a type that implements the Renderer interface for HTML output. // -// Do not create this directly, instead use the NewHTMLRenderer function. -type HTMLRenderer struct { - HTMLRendererParameters - +// Do not create this directly, instead use the HtmlRenderer function. +type Html struct { + flags int // HTML_* options closeTag string // how to end singleton tags: either " />" or ">" + title string // document title + css string // optional css file url (used with HTML_COMPLETE_PAGE) - // Track heading IDs to prevent ID collision in a single generation. - headingIDs map[string]int + parameters HtmlRendererParameters - lastOutputLen int - disableTags int + // table of contents data + tocMarker int + headerCount int + currentLevel int + toc *bytes.Buffer - sr *SPRenderer + // Track header IDs to prevent ID collision in a single generation. + headerIDs map[string]int + + smartypants *smartypantsRenderer } const ( @@ -117,824 +101,838 @@ const ( htmlClose = ">" ) -// NewHTMLRenderer creates and configures an HTMLRenderer object, which +// HtmlRenderer creates and configures an Html object, which // satisfies the Renderer interface. -func NewHTMLRenderer(params HTMLRendererParameters) *HTMLRenderer { +// +// flags is a set of HTML_* options ORed together. +// title is the title of the document, and css is a URL for the document's +// stylesheet. +// title and css are only used when HTML_COMPLETE_PAGE is selected. +func HtmlRenderer(flags int, title string, css string) Renderer { + return HtmlRendererWithParameters(flags, title, css, HtmlRendererParameters{}) +} + +func HtmlRendererWithParameters(flags int, title string, + css string, renderParameters HtmlRendererParameters) Renderer { // configure the rendering engine closeTag := htmlClose - if params.Flags&UseXHTML != 0 { + if flags&HTML_USE_XHTML != 0 { closeTag = xhtmlClose } - if params.FootnoteReturnLinkContents == "" { - params.FootnoteReturnLinkContents = `<sup>[return]</sup>` + if renderParameters.FootnoteReturnLinkContents == "" { + renderParameters.FootnoteReturnLinkContents = `<sup>[return]</sup>` } - return &HTMLRenderer{ - HTMLRendererParameters: params, - + return &Html{ + flags: flags, closeTag: closeTag, - headingIDs: make(map[string]int), + title: title, + css: css, + parameters: renderParameters, + + headerCount: 0, + currentLevel: 0, + toc: new(bytes.Buffer), - sr: NewSmartypantsRenderer(params.Flags), + headerIDs: make(map[string]int), + + smartypants: smartypants(flags), } } -func isHTMLTag(tag []byte, tagname string) bool { - found, _ := findHTMLTagPos(tag, tagname) - return found +// Using if statements is a bit faster than a switch statement. As the compiler +// improves, this should be unnecessary this is only worthwhile because +// attrEscape is the single largest CPU user in normal use. +// Also tried using map, but that gave a ~3x slowdown. +func escapeSingleChar(char byte) (string, bool) { + if char == '"' { + return """, true + } + if char == '&' { + return "&", true + } + if char == '<' { + return "<", true + } + if char == '>' { + return ">", true + } + return "", false } -// Look for a character, but ignore it when it's in any kind of quotes, it -// might be JavaScript -func skipUntilCharIgnoreQuotes(html []byte, start int, char byte) int { - inSingleQuote := false - inDoubleQuote := false - inGraveQuote := false - i := start - for i < len(html) { - switch { - case html[i] == char && !inSingleQuote && !inDoubleQuote && !inGraveQuote: - return i - case html[i] == '\'': - inSingleQuote = !inSingleQuote - case html[i] == '"': - inDoubleQuote = !inDoubleQuote - case html[i] == '`': - inGraveQuote = !inGraveQuote +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) } - i++ } - return start + if org < len(src) { + out.Write(src[org:]) + } } -func findHTMLTagPos(tag []byte, tagname string) (bool, int) { - i := 0 - if i < len(tag) && tag[0] != '<' { - return false, -1 +func entityEscapeWithSkip(out *bytes.Buffer, src []byte, skipRanges [][]int) { + end := 0 + for _, rang := range skipRanges { + attrEscape(out, src[end:rang[0]]) + out.Write(src[rang[0]:rang[1]]) + end = rang[1] } - i++ - i = skipSpace(tag, i) + attrEscape(out, src[end:]) +} - if i < len(tag) && tag[i] == '/' { - i++ +func (options *Html) GetFlags() int { + return options.flags +} + +func (options *Html) TitleBlock(out *bytes.Buffer, text []byte) { + text = bytes.TrimPrefix(text, []byte("% ")) + text = bytes.Replace(text, []byte("\n% "), []byte("\n"), -1) + out.WriteString("<h1 class=\"title\">") + out.Write(text) + out.WriteString("\n</h1>") +} + +func (options *Html) Header(out *bytes.Buffer, text func() bool, level int, id string) { + marker := out.Len() + doubleSpace(out) + + if id == "" && options.flags&HTML_TOC != 0 { + id = fmt.Sprintf("toc_%d", options.headerCount) } - i = skipSpace(tag, i) - j := 0 - for ; i < len(tag); i, j = i+1, j+1 { - if j >= len(tagname) { - break + if id != "" { + id = options.ensureUniqueHeaderID(id) + + if options.parameters.HeaderIDPrefix != "" { + id = options.parameters.HeaderIDPrefix + id } - if strings.ToLower(string(tag[i]))[0] != tagname[j] { - return false, -1 + if options.parameters.HeaderIDSuffix != "" { + id = id + options.parameters.HeaderIDSuffix } + + out.WriteString(fmt.Sprintf("<h%d id=\"%s\">", level, id)) + } else { + out.WriteString(fmt.Sprintf("<h%d>", level)) } - if i == len(tag) { - return false, -1 + tocMarker := out.Len() + if !text() { + out.Truncate(marker) + return } - rightAngle := skipUntilCharIgnoreQuotes(tag, i, '>') - if rightAngle >= i { - return true, rightAngle + // are we building a table of contents? + if options.flags&HTML_TOC != 0 { + options.TocHeaderWithAnchor(out.Bytes()[tocMarker:], level, id) } - return false, -1 + out.WriteString(fmt.Sprintf("</h%d>\n", level)) } -func skipSpace(tag []byte, i int) int { - for i < len(tag) && isspace(tag[i]) { - i++ +func (options *Html) BlockHtml(out *bytes.Buffer, text []byte) { + if options.flags&HTML_SKIP_HTML != 0 { + return } - return i + + doubleSpace(out) + out.Write(text) + out.WriteByte('\n') } -func isRelativeLink(link []byte) (yes bool) { - // a tag begin with '#' - if link[0] == '#' { - return true - } +func (options *Html) HRule(out *bytes.Buffer) { + doubleSpace(out) + out.WriteString("<hr") + out.WriteString(options.closeTag) + out.WriteByte('\n') +} - // link begin with '/' but not '//', the second maybe a protocol relative link - if len(link) >= 2 && link[0] == '/' && link[1] != '/' { - return true - } +func (options *Html) BlockCode(out *bytes.Buffer, text []byte, info string) { + doubleSpace(out) - // only the root '/' - if len(link) == 1 && link[0] == '/' { - return true + endOfLang := strings.IndexAny(info, "\t ") + if endOfLang < 0 { + endOfLang = len(info) } - - // current directory : begin with "./" - if bytes.HasPrefix(link, []byte("./")) { - return true + lang := info[:endOfLang] + if len(lang) == 0 || lang == "." { + out.WriteString("<pre><code>") + } else { + out.WriteString("<pre><code class=\"language-") + attrEscape(out, []byte(lang)) + out.WriteString("\">") } + attrEscape(out, text) + out.WriteString("</code></pre>\n") +} - // parent directory : begin with "../" - if bytes.HasPrefix(link, []byte("../")) { - return true - } +func (options *Html) BlockQuote(out *bytes.Buffer, text []byte) { + doubleSpace(out) + out.WriteString("<blockquote>\n") + out.Write(text) + out.WriteString("</blockquote>\n") +} - return false +func (options *Html) Table(out *bytes.Buffer, header []byte, body []byte, columnData []int) { + doubleSpace(out) + out.WriteString("<table>\n<thead>\n") + out.Write(header) + out.WriteString("</thead>\n\n<tbody>\n") + out.Write(body) + out.WriteString("</tbody>\n</table>\n") } -func (r *HTMLRenderer) ensureUniqueHeadingID(id string) string { - for count, found := r.headingIDs[id]; found; count, found = r.headingIDs[id] { - tmp := fmt.Sprintf("%s-%d", id, count+1) +func (options *Html) TableRow(out *bytes.Buffer, text []byte) { + doubleSpace(out) + out.WriteString("<tr>\n") + out.Write(text) + out.WriteString("\n</tr>\n") +} - if _, tmpFound := r.headingIDs[tmp]; !tmpFound { - r.headingIDs[id] = count + 1 - id = tmp - } else { - id = id + "-1" - } +func (options *Html) TableHeaderCell(out *bytes.Buffer, text []byte, align int) { + doubleSpace(out) + switch align { + case TABLE_ALIGNMENT_LEFT: + out.WriteString("<th align=\"left\">") + case TABLE_ALIGNMENT_RIGHT: + out.WriteString("<th align=\"right\">") + case TABLE_ALIGNMENT_CENTER: + out.WriteString("<th align=\"center\">") + default: + out.WriteString("<th>") } - if _, found := r.headingIDs[id]; !found { - r.headingIDs[id] = 0 + out.Write(text) + out.WriteString("</th>") +} + +func (options *Html) TableCell(out *bytes.Buffer, text []byte, align int) { + doubleSpace(out) + switch align { + case TABLE_ALIGNMENT_LEFT: + out.WriteString("<td align=\"left\">") + case TABLE_ALIGNMENT_RIGHT: + out.WriteString("<td align=\"right\">") + case TABLE_ALIGNMENT_CENTER: + out.WriteString("<td align=\"center\">") + default: + out.WriteString("<td>") } - return id + out.Write(text) + out.WriteString("</td>") } -func (r *HTMLRenderer) addAbsPrefix(link []byte) []byte { - if r.AbsolutePrefix != "" && isRelativeLink(link) && link[0] != '.' { - newDest := r.AbsolutePrefix - if link[0] != '/' { - newDest += "/" - } - newDest += string(link) - return []byte(newDest) - } - return link +func (options *Html) Footnotes(out *bytes.Buffer, text func() bool) { + out.WriteString("<div class=\"footnotes\">\n") + options.HRule(out) + options.List(out, text, LIST_TYPE_ORDERED) + out.WriteString("</div>\n") +} + +func (options *Html) FootnoteItem(out *bytes.Buffer, name, text []byte, flags int) { + if flags&LIST_ITEM_CONTAINS_BLOCK != 0 || flags&LIST_ITEM_BEGINNING_OF_LIST != 0 { + doubleSpace(out) + } + slug := slugify(name) + out.WriteString(`<li id="`) + out.WriteString(`fn:`) + out.WriteString(options.parameters.FootnoteAnchorPrefix) + out.Write(slug) + out.WriteString(`">`) + out.Write(text) + if options.flags&HTML_FOOTNOTE_RETURN_LINKS != 0 { + out.WriteString(` <a class="footnote-return" href="#`) + out.WriteString(`fnref:`) + out.WriteString(options.parameters.FootnoteAnchorPrefix) + out.Write(slug) + out.WriteString(`">`) + out.WriteString(options.parameters.FootnoteReturnLinkContents) + out.WriteString(`</a>`) + } + out.WriteString("</li>\n") } -func appendLinkAttrs(attrs []string, flags HTMLFlags, link []byte) []string { - if isRelativeLink(link) { - return attrs +func (options *Html) List(out *bytes.Buffer, text func() bool, flags int) { + marker := out.Len() + doubleSpace(out) + + if flags&LIST_TYPE_DEFINITION != 0 { + out.WriteString("<dl>") + } else if flags&LIST_TYPE_ORDERED != 0 { + out.WriteString("<ol>") + } else { + out.WriteString("<ul>") } - val := []string{} - if flags&NofollowLinks != 0 { - val = append(val, "nofollow") + if !text() { + out.Truncate(marker) + return } - if flags&NoreferrerLinks != 0 { - val = append(val, "noreferrer") + if flags&LIST_TYPE_DEFINITION != 0 { + out.WriteString("</dl>\n") + } else if flags&LIST_TYPE_ORDERED != 0 { + out.WriteString("</ol>\n") + } else { + out.WriteString("</ul>\n") + } +} + +func (options *Html) ListItem(out *bytes.Buffer, text []byte, flags int) { + if (flags&LIST_ITEM_CONTAINS_BLOCK != 0 && flags&LIST_TYPE_DEFINITION == 0) || + flags&LIST_ITEM_BEGINNING_OF_LIST != 0 { + doubleSpace(out) } - if flags&HrefTargetBlank != 0 { - attrs = append(attrs, "target=\"_blank\"") + if flags&LIST_TYPE_TERM != 0 { + out.WriteString("<dt>") + } else if flags&LIST_TYPE_DEFINITION != 0 { + out.WriteString("<dd>") + } else { + out.WriteString("<li>") } - if len(val) == 0 { - return attrs + out.Write(text) + if flags&LIST_TYPE_TERM != 0 { + out.WriteString("</dt>\n") + } else if flags&LIST_TYPE_DEFINITION != 0 { + out.WriteString("</dd>\n") + } else { + out.WriteString("</li>\n") } - attr := fmt.Sprintf("rel=%q", strings.Join(val, " ")) - return append(attrs, attr) } -func isMailto(link []byte) bool { - return bytes.HasPrefix(link, []byte("mailto:")) -} +func (options *Html) Paragraph(out *bytes.Buffer, text func() bool) { + marker := out.Len() + doubleSpace(out) -func needSkipLink(flags HTMLFlags, dest []byte) bool { - if flags&SkipLinks != 0 { - return true + out.WriteString("<p>") + if !text() { + out.Truncate(marker) + return } - return flags&Safelink != 0 && !isSafeLink(dest) && !isMailto(dest) + out.WriteString("</p>\n") } -func isSmartypantable(node *Node) bool { - pt := node.Parent.Type - return pt != Link && pt != CodeBlock && pt != Code -} +func (options *Html) AutoLink(out *bytes.Buffer, link []byte, kind int) { + skipRanges := htmlEntity.FindAllIndex(link, -1) + if options.flags&HTML_SAFELINK != 0 && !isSafeLink(link) && kind != LINK_TYPE_EMAIL { + // mark it but don't link it if it is not a safe link: no smartypants + out.WriteString("<tt>") + entityEscapeWithSkip(out, link, skipRanges) + out.WriteString("</tt>") + return + } -func appendLanguageAttr(attrs []string, info []byte) []string { - if len(info) == 0 { - return attrs + out.WriteString("<a href=\"") + if kind == LINK_TYPE_EMAIL { + out.WriteString("mailto:") + } else { + options.maybeWriteAbsolutePrefix(out, link) } - endOfLang := bytes.IndexAny(info, "\t ") - if endOfLang < 0 { - endOfLang = len(info) + + entityEscapeWithSkip(out, link, skipRanges) + + var relAttrs []string + if options.flags&HTML_NOFOLLOW_LINKS != 0 && !isRelativeLink(link) { + relAttrs = append(relAttrs, "nofollow") + } + if options.flags&HTML_NOREFERRER_LINKS != 0 && !isRelativeLink(link) { + relAttrs = append(relAttrs, "noreferrer") + } + if len(relAttrs) > 0 { + out.WriteString(fmt.Sprintf("\" rel=\"%s", strings.Join(relAttrs, " "))) } - return append(attrs, fmt.Sprintf("class=\"language-%s\"", info[:endOfLang])) -} -func (r *HTMLRenderer) tag(w io.Writer, name []byte, attrs []string) { - w.Write(name) - if len(attrs) > 0 { - w.Write(spaceBytes) - w.Write([]byte(strings.Join(attrs, " "))) + // blank target only add to external link + if options.flags&HTML_HREF_TARGET_BLANK != 0 && !isRelativeLink(link) { + out.WriteString("\" target=\"_blank") + } + + out.WriteString("\">") + + // Pretty print: if we get an email address as + // an actual URI, e.g. `mailto:foo@bar.com`, we don't + // want to print the `mailto:` prefix + switch { + case bytes.HasPrefix(link, []byte("mailto://")): + attrEscape(out, link[len("mailto://"):]) + case bytes.HasPrefix(link, []byte("mailto:")): + attrEscape(out, link[len("mailto:"):]) + default: + entityEscapeWithSkip(out, link, skipRanges) } - w.Write(gtBytes) - r.lastOutputLen = 1 -} -func footnoteRef(prefix string, node *Node) []byte { - urlFrag := prefix + string(slugify(node.Destination)) - anchor := fmt.Sprintf(`<a rel="footnote" href="#fn:%s">%d</a>`, urlFrag, node.NoteID) - return []byte(fmt.Sprintf(`<sup class="footnote-ref" id="fnref:%s">%s</sup>`, urlFrag, anchor)) + out.WriteString("</a>") } -func footnoteItem(prefix string, slug []byte) []byte { - return []byte(fmt.Sprintf(`<li id="fn:%s%s">`, prefix, slug)) +func (options *Html) CodeSpan(out *bytes.Buffer, text []byte) { + out.WriteString("<code>") + attrEscape(out, text) + out.WriteString("</code>") } -func footnoteReturnLink(prefix, returnLink string, slug []byte) []byte { - const format = ` <a class="footnote-return" href="#fnref:%s%s">%s</a>` - return []byte(fmt.Sprintf(format, prefix, slug, returnLink)) +func (options *Html) DoubleEmphasis(out *bytes.Buffer, text []byte) { + out.WriteString("<strong>") + out.Write(text) + out.WriteString("</strong>") } -func itemOpenCR(node *Node) bool { - if node.Prev == nil { - return false +func (options *Html) Emphasis(out *bytes.Buffer, text []byte) { + if len(text) == 0 { + return } - ld := node.Parent.ListData - return !ld.Tight && ld.ListFlags&ListTypeDefinition == 0 + out.WriteString("<em>") + out.Write(text) + out.WriteString("</em>") } -func skipParagraphTags(node *Node) bool { - grandparent := node.Parent.Parent - if grandparent == nil || grandparent.Type != List { - return false +func (options *Html) maybeWriteAbsolutePrefix(out *bytes.Buffer, link []byte) { + if options.parameters.AbsolutePrefix != "" && isRelativeLink(link) && link[0] != '.' { + out.WriteString(options.parameters.AbsolutePrefix) + if link[0] != '/' { + out.WriteByte('/') + } } - tightOrTerm := grandparent.Tight || node.Parent.ListFlags&ListTypeTerm != 0 - return grandparent.Type == List && tightOrTerm } -func cellAlignment(align CellAlignFlags) string { - switch align { - case TableAlignmentLeft: - return "left" - case TableAlignmentRight: - return "right" - case TableAlignmentCenter: - return "center" - default: - return "" +func (options *Html) Image(out *bytes.Buffer, link []byte, title []byte, alt []byte) { + if options.flags&HTML_SKIP_IMAGES != 0 { + return + } + + out.WriteString("<img src=\"") + options.maybeWriteAbsolutePrefix(out, link) + attrEscape(out, link) + out.WriteString("\" alt=\"") + if len(alt) > 0 { + attrEscape(out, alt) + } + if len(title) > 0 { + out.WriteString("\" title=\"") + attrEscape(out, title) } + + out.WriteByte('"') + out.WriteString(options.closeTag) } -func (r *HTMLRenderer) out(w io.Writer, text []byte) { - if r.disableTags > 0 { - w.Write(htmlTagRe.ReplaceAll(text, []byte{})) - } else { - w.Write(text) +func (options *Html) LineBreak(out *bytes.Buffer) { + out.WriteString("<br") + out.WriteString(options.closeTag) + out.WriteByte('\n') +} + +func (options *Html) Link(out *bytes.Buffer, link []byte, title []byte, content []byte) { + if options.flags&HTML_SKIP_LINKS != 0 { + // write the link text out but don't link it, just mark it with typewriter font + out.WriteString("<tt>") + attrEscape(out, content) + out.WriteString("</tt>") + return + } + + if options.flags&HTML_SAFELINK != 0 && !isSafeLink(link) { + // write the link text out but don't link it, just mark it with typewriter font + out.WriteString("<tt>") + attrEscape(out, content) + out.WriteString("</tt>") + return + } + + out.WriteString("<a href=\"") + options.maybeWriteAbsolutePrefix(out, link) + attrEscape(out, link) + if len(title) > 0 { + out.WriteString("\" title=\"") + attrEscape(out, title) + } + var relAttrs []string + if options.flags&HTML_NOFOLLOW_LINKS != 0 && !isRelativeLink(link) { + relAttrs = append(relAttrs, "nofollow") + } + if options.flags&HTML_NOREFERRER_LINKS != 0 && !isRelativeLink(link) { + relAttrs = append(relAttrs, "noreferrer") + } + if len(relAttrs) > 0 { + out.WriteString(fmt.Sprintf("\" rel=\"%s", strings.Join(relAttrs, " "))) } - r.lastOutputLen = len(text) + + // blank target only add to external link + if options.flags&HTML_HREF_TARGET_BLANK != 0 && !isRelativeLink(link) { + out.WriteString("\" target=\"_blank") + } + + out.WriteString("\">") + out.Write(content) + out.WriteString("</a>") + return } -func (r *HTMLRenderer) cr(w io.Writer) { - if r.lastOutputLen > 0 { - r.out(w, nlBytes) +func (options *Html) RawHtmlTag(out *bytes.Buffer, text []byte) { + if options.flags&HTML_SKIP_HTML != 0 { + return + } + if options.flags&HTML_SKIP_STYLE != 0 && isHtmlTag(text, "style") { + return } + if options.flags&HTML_SKIP_LINKS != 0 && isHtmlTag(text, "a") { + return + } + if options.flags&HTML_SKIP_IMAGES != 0 && isHtmlTag(text, "img") { + return + } + out.Write(text) } -var ( - nlBytes = []byte{'\n'} - gtBytes = []byte{'>'} - spaceBytes = []byte{' '} -) +func (options *Html) TripleEmphasis(out *bytes.Buffer, text []byte) { + out.WriteString("<strong><em>") + out.Write(text) + out.WriteString("</em></strong>") +} -var ( - brTag = []byte("<br>") - brXHTMLTag = []byte("<br />") - emTag = []byte("<em>") - emCloseTag = []byte("</em>") - strongTag = []byte("<strong>") - strongCloseTag = []byte("</strong>") - delTag = []byte("<del>") - delCloseTag = []byte("</del>") - ttTag = []byte("<tt>") - ttCloseTag = []byte("</tt>") - aTag = []byte("<a") - aCloseTag = []byte("</a>") - preTag = []byte("<pre>") - preCloseTag = []byte("</pre>") - codeTag = []byte("<code>") - codeCloseTag = []byte("</code>") - pTag = []byte("<p>") - pCloseTag = []byte("</p>") - blockquoteTag = []byte("<blockquote>") - blockquoteCloseTag = []byte("</blockquote>") - hrTag = []byte("<hr>") - hrXHTMLTag = []byte("<hr />") - ulTag = []byte("<ul>") - ulCloseTag = []byte("</ul>") - olTag = []byte("<ol>") - olCloseTag = []byte("</ol>") - dlTag = []byte("<dl>") - dlCloseTag = []byte("</dl>") - liTag = []byte("<li>") - liCloseTag = []byte("</li>") - ddTag = []byte("<dd>") - ddCloseTag = []byte("</dd>") - dtTag = []byte("<dt>") - dtCloseTag = []byte("</dt>") - tableTag = []byte("<table>") - tableCloseTag = []byte("</table>") - tdTag = []byte("<td") - tdCloseTag = []byte("</td>") - thTag = []byte("<th") - thCloseTag = []byte("</th>") - theadTag = []byte("<thead>") - theadCloseTag = []byte("</thead>") - tbodyTag = []byte("<tbody>") - tbodyCloseTag = []byte("</tbody>") - trTag = []byte("<tr>") - trCloseTag = []byte("</tr>") - h1Tag = []byte("<h1") - h1CloseTag = []byte("</h1>") - h2Tag = []byte("<h2") - h2CloseTag = []byte("</h2>") - h3Tag = []byte("<h3") - h3CloseTag = []byte("</h3>") - h4Tag = []byte("<h4") - h4CloseTag = []byte("</h4>") - h5Tag = []byte("<h5") - h5CloseTag = []byte("</h5>") - h6Tag = []byte("<h6") - h6CloseTag = []byte("</h6>") - - footnotesDivBytes = []byte("\n<div class=\"footnotes\">\n\n") - footnotesCloseDivBytes = []byte("\n</div>\n") -) +func (options *Html) StrikeThrough(out *bytes.Buffer, text []byte) { + out.WriteString("<del>") + out.Write(text) + out.WriteString("</del>") +} -func headingTagsFromLevel(level int) ([]byte, []byte) { - switch level { - case 1: - return h1Tag, h1CloseTag - case 2: - return h2Tag, h2CloseTag - case 3: - return h3Tag, h3CloseTag - case 4: - return h4Tag, h4CloseTag - case 5: - return h5Tag, h5CloseTag - default: - return h6Tag, h6CloseTag - } +func (options *Html) FootnoteRef(out *bytes.Buffer, ref []byte, id int) { + slug := slugify(ref) + out.WriteString(`<sup class="footnote-ref" id="`) + out.WriteString(`fnref:`) + out.WriteString(options.parameters.FootnoteAnchorPrefix) + out.Write(slug) + out.WriteString(`"><a href="#`) + out.WriteString(`fn:`) + out.WriteString(options.parameters.FootnoteAnchorPrefix) + out.Write(slug) + out.WriteString(`">`) + out.WriteString(strconv.Itoa(id)) + out.WriteString(`</a></sup>`) +} + +func (options *Html) Entity(out *bytes.Buffer, entity []byte) { + out.Write(entity) } -func (r *HTMLRenderer) outHRTag(w io.Writer) { - if r.Flags&UseXHTML == 0 { - r.out(w, hrTag) +func (options *Html) NormalText(out *bytes.Buffer, text []byte) { + if options.flags&HTML_USE_SMARTYPANTS != 0 { + options.Smartypants(out, text) } else { - r.out(w, hrXHTMLTag) + attrEscape(out, text) } } -// RenderNode is a default renderer of a single node of a syntax tree. For -// block nodes it will be called twice: first time with entering=true, second -// time with entering=false, so that it could know when it's working on an open -// tag and when on close. It writes the result to w. -// -// The return value is a way to tell the calling walker to adjust its walk -// pattern: e.g. it can terminate the traversal by returning Terminate. Or it -// can ask the walker to skip a subtree of this node by returning SkipChildren. -// The typical behavior is to return GoToNext, which asks for the usual -// traversal to the next node. -func (r *HTMLRenderer) RenderNode(w io.Writer, node *Node, entering bool) WalkStatus { - attrs := []string{} - switch node.Type { - case Text: - if r.Flags&Smartypants != 0 { - var tmp bytes.Buffer - escapeHTML(&tmp, node.Literal) - r.sr.Process(w, tmp.Bytes()) - } else { - if node.Parent.Type == Link { - escLink(w, node.Literal) - } else { - escapeHTML(w, node.Literal) - } - } - case Softbreak: - r.cr(w) - // TODO: make it configurable via out(renderer.softbreak) - case Hardbreak: - if r.Flags&UseXHTML == 0 { - r.out(w, brTag) - } else { - r.out(w, brXHTMLTag) - } - r.cr(w) - case Emph: - if entering { - r.out(w, emTag) - } else { - r.out(w, emCloseTag) - } - case Strong: - if entering { - r.out(w, strongTag) - } else { - r.out(w, strongCloseTag) - } - case Del: - if entering { - r.out(w, delTag) - } else { - r.out(w, delCloseTag) - } - case HTMLSpan: - if r.Flags&SkipHTML != 0 { - break - } - r.out(w, node.Literal) - case Link: - // mark it but don't link it if it is not a safe link: no smartypants - dest := node.LinkData.Destination - if needSkipLink(r.Flags, dest) { - if entering { - r.out(w, ttTag) - } else { - r.out(w, ttCloseTag) - } - } else { - if entering { - dest = r.addAbsPrefix(dest) - var hrefBuf bytes.Buffer - hrefBuf.WriteString("href=\"") - escLink(&hrefBuf, dest) - hrefBuf.WriteByte('"') - attrs = append(attrs, hrefBuf.String()) - if node.NoteID != 0 { - r.out(w, footnoteRef(r.FootnoteAnchorPrefix, node)) - break - } - attrs = appendLinkAttrs(attrs, r.Flags, dest) - if len(node.LinkData.Title) > 0 { - var titleBuff bytes.Buffer - titleBuff.WriteString("title=\"") - escapeHTML(&titleBuff, node.LinkData.Title) - titleBuff.WriteByte('"') - attrs = append(attrs, titleBuff.String()) - } - r.tag(w, aTag, attrs) - } else { - if node.NoteID != 0 { - break - } - r.out(w, aCloseTag) - } - } - case Image: - if r.Flags&SkipImages != 0 { - return SkipChildren - } - if entering { - dest := node.LinkData.Destination - dest = r.addAbsPrefix(dest) - if r.disableTags == 0 { - //if options.safe && potentiallyUnsafe(dest) { - //out(w, `<img src="" alt="`) - //} else { - r.out(w, []byte(`<img src="`)) - escLink(w, dest) - r.out(w, []byte(`" alt="`)) - //} - } - r.disableTags++ - } else { - r.disableTags-- - if r.disableTags == 0 { - if node.LinkData.Title != nil { - r.out(w, []byte(`" title="`)) - escapeHTML(w, node.LinkData.Title) - } - r.out(w, []byte(`" />`)) - } - } - case Code: - r.out(w, codeTag) - escapeHTML(w, node.Literal) - r.out(w, codeCloseTag) - case Document: - break - case Paragraph: - if skipParagraphTags(node) { - break - } - if entering { - // TODO: untangle this clusterfuck about when the newlines need - // to be added and when not. - if node.Prev != nil { - switch node.Prev.Type { - case HTMLBlock, List, Paragraph, Heading, CodeBlock, BlockQuote, HorizontalRule: - r.cr(w) - } - } - if node.Parent.Type == BlockQuote && node.Prev == nil { - r.cr(w) - } - r.out(w, pTag) - } else { - r.out(w, pCloseTag) - if !(node.Parent.Type == Item && node.Next == nil) { - r.cr(w) - } - } - case BlockQuote: - if entering { - r.cr(w) - r.out(w, blockquoteTag) - } else { - r.out(w, blockquoteCloseTag) - r.cr(w) - } - case HTMLBlock: - if r.Flags&SkipHTML != 0 { - break - } - r.cr(w) - r.out(w, node.Literal) - r.cr(w) - case Heading: - openTag, closeTag := headingTagsFromLevel(node.Level) - if entering { - if node.IsTitleblock { - attrs = append(attrs, `class="title"`) - } - if node.HeadingID != "" { - id := r.ensureUniqueHeadingID(node.HeadingID) - if r.HeadingIDPrefix != "" { - id = r.HeadingIDPrefix + id - } - if r.HeadingIDSuffix != "" { - id = id + r.HeadingIDSuffix - } - attrs = append(attrs, fmt.Sprintf(`id="%s"`, id)) - } - r.cr(w) - r.tag(w, openTag, attrs) - } else { - r.out(w, closeTag) - if !(node.Parent.Type == Item && node.Next == nil) { - r.cr(w) - } - } - case HorizontalRule: - r.cr(w) - r.outHRTag(w) - r.cr(w) - case List: - openTag := ulTag - closeTag := ulCloseTag - if node.ListFlags&ListTypeOrdered != 0 { - openTag = olTag - closeTag = olCloseTag - } - if node.ListFlags&ListTypeDefinition != 0 { - openTag = dlTag - closeTag = dlCloseTag - } - if entering { - if node.IsFootnotesList { - r.out(w, footnotesDivBytes) - r.outHRTag(w) - r.cr(w) - } - r.cr(w) - if node.Parent.Type == Item && node.Parent.Parent.Tight { - r.cr(w) - } - r.tag(w, openTag[:len(openTag)-1], attrs) - r.cr(w) - } else { - r.out(w, closeTag) - //cr(w) - //if node.parent.Type != Item { - // cr(w) - //} - if node.Parent.Type == Item && node.Next != nil { - r.cr(w) - } - if node.Parent.Type == Document || node.Parent.Type == BlockQuote { - r.cr(w) +func (options *Html) Smartypants(out *bytes.Buffer, text []byte) { + smrt := smartypantsData{false, false} + + // first do normal entity escaping + var escaped bytes.Buffer + attrEscape(&escaped, text) + text = escaped.Bytes() + + mark := 0 + for i := 0; i < len(text); i++ { + if action := options.smartypants[text[i]]; action != nil { + if i > mark { + out.Write(text[mark:i]) } - if node.IsFootnotesList { - r.out(w, footnotesCloseDivBytes) + + previousChar := byte(0) + if i > 0 { + previousChar = text[i-1] } + i += action(out, &smrt, previousChar, text[i:]) + mark = i + 1 } - case Item: - openTag := liTag - closeTag := liCloseTag - if node.ListFlags&ListTypeDefinition != 0 { - openTag = ddTag - closeTag = ddCloseTag - } - if node.ListFlags&ListTypeTerm != 0 { - openTag = dtTag - closeTag = dtCloseTag + } + + if mark < len(text) { + out.Write(text[mark:]) + } +} + +func (options *Html) DocumentHeader(out *bytes.Buffer) { + if options.flags&HTML_COMPLETE_PAGE == 0 { + return + } + + ending := "" + if options.flags&HTML_USE_XHTML != 0 { + out.WriteString("<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" ") + out.WriteString("\"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n") + out.WriteString("<html xmlns=\"http://www.w3.org/1999/xhtml\">\n") + ending = " /" + } else { + out.WriteString("<!DOCTYPE html>\n") + out.WriteString("<html>\n") + } + out.WriteString("<head>\n") + out.WriteString(" <title>") + options.NormalText(out, []byte(options.title)) + out.WriteString("</title>\n") + out.WriteString(" <meta name=\"GENERATOR\" content=\"Blackfriday Markdown Processor v") + out.WriteString(VERSION) + out.WriteString("\"") + out.WriteString(ending) + out.WriteString(">\n") + out.WriteString(" <meta charset=\"utf-8\"") + out.WriteString(ending) + out.WriteString(">\n") + if options.css != "" { + out.WriteString(" <link rel=\"stylesheet\" type=\"text/css\" href=\"") + attrEscape(out, []byte(options.css)) + out.WriteString("\"") + out.WriteString(ending) + out.WriteString(">\n") + } + out.WriteString("</head>\n") + out.WriteString("<body>\n") + + options.tocMarker = out.Len() +} + +func (options *Html) DocumentFooter(out *bytes.Buffer) { + // finalize and insert the table of contents + if options.flags&HTML_TOC != 0 { + options.TocFinalize() + + // now we have to insert the table of contents into the document + var temp bytes.Buffer + + // start by making a copy of everything after the document header + temp.Write(out.Bytes()[options.tocMarker:]) + + // now clear the copied material from the main output buffer + out.Truncate(options.tocMarker) + + // corner case spacing issue + if options.flags&HTML_COMPLETE_PAGE != 0 { + out.WriteByte('\n') } - if entering { - if itemOpenCR(node) { - r.cr(w) - } - if node.ListData.RefLink != nil { - slug := slugify(node.ListData.RefLink) - r.out(w, footnoteItem(r.FootnoteAnchorPrefix, slug)) - break - } - r.out(w, openTag) - } else { - if node.ListData.RefLink != nil { - slug := slugify(node.ListData.RefLink) - if r.Flags&FootnoteReturnLinks != 0 { - r.out(w, footnoteReturnLink(r.FootnoteAnchorPrefix, r.FootnoteReturnLinkContents, slug)) - } - } - r.out(w, closeTag) - r.cr(w) + + // insert the table of contents + out.WriteString("<nav>\n") + out.Write(options.toc.Bytes()) + out.WriteString("</nav>\n") + + // corner case spacing issue + if options.flags&HTML_COMPLETE_PAGE == 0 && options.flags&HTML_OMIT_CONTENTS == 0 { + out.WriteByte('\n') } - case CodeBlock: - attrs = appendLanguageAttr(attrs, node.Info) - r.cr(w) - r.out(w, preTag) - r.tag(w, codeTag[:len(codeTag)-1], attrs) - escapeHTML(w, node.Literal) - r.out(w, codeCloseTag) - r.out(w, preCloseTag) - if node.Parent.Type != Item { - r.cr(w) + + // write out everything that came after it + if options.flags&HTML_OMIT_CONTENTS == 0 { + out.Write(temp.Bytes()) } - case Table: - if entering { - r.cr(w) - r.out(w, tableTag) - } else { - r.out(w, tableCloseTag) - r.cr(w) + } + + if options.flags&HTML_COMPLETE_PAGE != 0 { + out.WriteString("\n</body>\n") + out.WriteString("</html>\n") + } + +} + +func (options *Html) TocHeaderWithAnchor(text []byte, level int, anchor string) { + for level > options.currentLevel { + switch { + case bytes.HasSuffix(options.toc.Bytes(), []byte("</li>\n")): + // this sublist can nest underneath a header + size := options.toc.Len() + options.toc.Truncate(size - len("</li>\n")) + + case options.currentLevel > 0: + options.toc.WriteString("<li>") } - case TableCell: - openTag := tdTag - closeTag := tdCloseTag - if node.IsHeader { - openTag = thTag - closeTag = thCloseTag + if options.toc.Len() > 0 { + options.toc.WriteByte('\n') } - if entering { - align := cellAlignment(node.Align) - if align != "" { - attrs = append(attrs, fmt.Sprintf(`align="%s"`, align)) - } - if node.Prev == nil { - r.cr(w) - } - r.tag(w, openTag, attrs) - } else { - r.out(w, closeTag) - r.cr(w) + options.toc.WriteString("<ul>\n") + options.currentLevel++ + } + + for level < options.currentLevel { + options.toc.WriteString("</ul>") + if options.currentLevel > 1 { + options.toc.WriteString("</li>\n") } - case TableHead: - if entering { - r.cr(w) - r.out(w, theadTag) - } else { - r.out(w, theadCloseTag) - r.cr(w) + options.currentLevel-- + } + + options.toc.WriteString("<li><a href=\"#") + if anchor != "" { + options.toc.WriteString(anchor) + } else { + options.toc.WriteString("toc_") + options.toc.WriteString(strconv.Itoa(options.headerCount)) + } + options.toc.WriteString("\">") + options.headerCount++ + + options.toc.Write(text) + + options.toc.WriteString("</a></li>\n") +} + +func (options *Html) TocHeader(text []byte, level int) { + options.TocHeaderWithAnchor(text, level, "") +} + +func (options *Html) TocFinalize() { + for options.currentLevel > 1 { + options.toc.WriteString("</ul></li>\n") + options.currentLevel-- + } + + if options.currentLevel > 0 { + options.toc.WriteString("</ul>\n") + } +} + +func isHtmlTag(tag []byte, tagname string) bool { + found, _ := findHtmlTagPos(tag, tagname) + return found +} + +// Look for a character, but ignore it when it's in any kind of quotes, it +// might be JavaScript +func skipUntilCharIgnoreQuotes(html []byte, start int, char byte) int { + inSingleQuote := false + inDoubleQuote := false + inGraveQuote := false + i := start + for i < len(html) { + switch { + case html[i] == char && !inSingleQuote && !inDoubleQuote && !inGraveQuote: + return i + case html[i] == '\'': + inSingleQuote = !inSingleQuote + case html[i] == '"': + inDoubleQuote = !inDoubleQuote + case html[i] == '`': + inGraveQuote = !inGraveQuote } - case TableBody: - if entering { - r.cr(w) - r.out(w, tbodyTag) - // XXX: this is to adhere to a rather silly test. Should fix test. - if node.FirstChild == nil { - r.cr(w) - } - } else { - r.out(w, tbodyCloseTag) - r.cr(w) + i++ + } + return start +} + +func findHtmlTagPos(tag []byte, tagname string) (bool, int) { + i := 0 + if i < len(tag) && tag[0] != '<' { + return false, -1 + } + i++ + i = skipSpace(tag, i) + + if i < len(tag) && tag[i] == '/' { + i++ + } + + i = skipSpace(tag, i) + j := 0 + for ; i < len(tag); i, j = i+1, j+1 { + if j >= len(tagname) { + break } - case TableRow: - if entering { - r.cr(w) - r.out(w, trTag) - } else { - r.out(w, trCloseTag) - r.cr(w) + + if strings.ToLower(string(tag[i]))[0] != tagname[j] { + return false, -1 } - default: - panic("Unknown node type " + node.Type.String()) } - return GoToNext + + if i == len(tag) { + return false, -1 + } + + rightAngle := skipUntilCharIgnoreQuotes(tag, i, '>') + if rightAngle > i { + return true, rightAngle + } + + return false, -1 } -// RenderHeader writes HTML document preamble and TOC if requested. -func (r *HTMLRenderer) RenderHeader(w io.Writer, ast *Node) { - r.writeDocumentHeader(w) - if r.Flags&TOC != 0 { - r.writeTOC(w, ast) +func skipUntilChar(text []byte, start int, char byte) int { + i := start + for i < len(text) && text[i] != char { + i++ } + return i } -// RenderFooter writes HTML document footer. -func (r *HTMLRenderer) RenderFooter(w io.Writer, ast *Node) { - if r.Flags&CompletePage == 0 { - return +func skipSpace(tag []byte, i int) int { + for i < len(tag) && isspace(tag[i]) { + i++ } - io.WriteString(w, "\n</body>\n</html>\n") + return i } -func (r *HTMLRenderer) writeDocumentHeader(w io.Writer) { - if r.Flags&CompletePage == 0 { - return +func skipChar(data []byte, start int, char byte) int { + i := start + for i < len(data) && data[i] == char { + i++ } - ending := "" - if r.Flags&UseXHTML != 0 { - io.WriteString(w, "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" ") - io.WriteString(w, "\"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n") - io.WriteString(w, "<html xmlns=\"http://www.w3.org/1999/xhtml\">\n") - ending = " /" - } else { - io.WriteString(w, "<!DOCTYPE html>\n") - io.WriteString(w, "<html>\n") + return i +} + +func doubleSpace(out *bytes.Buffer) { + if out.Len() > 0 { + out.WriteByte('\n') } - io.WriteString(w, "<head>\n") - io.WriteString(w, " <title>") - if r.Flags&Smartypants != 0 { - r.sr.Process(w, []byte(r.Title)) - } else { - escapeHTML(w, []byte(r.Title)) - } - io.WriteString(w, "</title>\n") - io.WriteString(w, " <meta name=\"GENERATOR\" content=\"Blackfriday Markdown Processor v") - io.WriteString(w, Version) - io.WriteString(w, "\"") - io.WriteString(w, ending) - io.WriteString(w, ">\n") - io.WriteString(w, " <meta charset=\"utf-8\"") - io.WriteString(w, ending) - io.WriteString(w, ">\n") - if r.CSS != "" { - io.WriteString(w, " <link rel=\"stylesheet\" type=\"text/css\" href=\"") - escapeHTML(w, []byte(r.CSS)) - io.WriteString(w, "\"") - io.WriteString(w, ending) - io.WriteString(w, ">\n") - } - if r.Icon != "" { - io.WriteString(w, " <link rel=\"icon\" type=\"image/x-icon\" href=\"") - escapeHTML(w, []byte(r.Icon)) - io.WriteString(w, "\"") - io.WriteString(w, ending) - io.WriteString(w, ">\n") - } - io.WriteString(w, "</head>\n") - io.WriteString(w, "<body>\n\n") -} - -func (r *HTMLRenderer) writeTOC(w io.Writer, ast *Node) { - buf := bytes.Buffer{} - - inHeading := false - tocLevel := 0 - headingCount := 0 - - ast.Walk(func(node *Node, entering bool) WalkStatus { - if node.Type == Heading && !node.HeadingData.IsTitleblock { - inHeading = entering - if entering { - node.HeadingID = fmt.Sprintf("toc_%d", headingCount) - if node.Level == tocLevel { - buf.WriteString("</li>\n\n<li>") - } else if node.Level < tocLevel { - for node.Level < tocLevel { - tocLevel-- - buf.WriteString("</li>\n</ul>") - } - buf.WriteString("</li>\n\n<li>") - } else { - for node.Level > tocLevel { - tocLevel++ - buf.WriteString("\n<ul>\n<li>") - } - } - - fmt.Fprintf(&buf, `<a href="#toc_%d">`, headingCount) - headingCount++ - } else { - buf.WriteString("</a>") - } - return GoToNext - } +} - if inHeading { - return r.RenderNode(&buf, node, entering) - } +func isRelativeLink(link []byte) (yes bool) { + // a tag begin with '#' + if link[0] == '#' { + return true + } - return GoToNext - }) + // link begin with '/' but not '//', the second maybe a protocol relative link + if len(link) >= 2 && link[0] == '/' && link[1] != '/' { + return true + } - for ; tocLevel > 0; tocLevel-- { - buf.WriteString("</li>\n</ul>") + // only the root '/' + if len(link) == 1 && link[0] == '/' { + return true } - if buf.Len() > 0 { - io.WriteString(w, "<nav>\n") - w.Write(buf.Bytes()) - io.WriteString(w, "\n\n</nav>\n") + // current directory : begin with "./" + if bytes.HasPrefix(link, []byte("./")) { + return true } - r.lastOutputLen = buf.Len() + + // parent directory : begin with "../" + if bytes.HasPrefix(link, []byte("../")) { + return true + } + + return false +} + +func (options *Html) ensureUniqueHeaderID(id string) string { + for count, found := options.headerIDs[id]; found; count, found = options.headerIDs[id] { + tmp := fmt.Sprintf("%s-%d", id, count+1) + + if _, tmpFound := options.headerIDs[tmp]; !tmpFound { + options.headerIDs[id] = count + 1 + id = tmp + } else { + id = id + "-1" + } + } + + if _, found := options.headerIDs[id]; !found { + options.headerIDs[id] = 0 + } + + return id } diff --git a/vendor/github.com/russross/blackfriday/inline.go b/vendor/github.com/russross/blackfriday/inline.go index 3d633106..4483b8f1 100644 --- a/vendor/github.com/russross/blackfriday/inline.go +++ b/vendor/github.com/russross/blackfriday/inline.go @@ -22,9 +22,6 @@ import ( var ( urlRe = `((https?|ftp):\/\/|\/)[-A-Za-z0-9+&@#\/%?=~_|!:,.;\(\)]+` anchorRe = regexp.MustCompile(`^(<a\shref="` + urlRe + `"(\stitle="[^"<>]+")?\s?>` + urlRe + `<\/a>)`) - - // TODO: improve this regexp to catch all possible entities: - htmlEntityRe = regexp.MustCompile(`&[a-z]{2,5};`) ) // Functions to parse text within a block @@ -32,89 +29,87 @@ var ( // data is the complete block being rendered // offset is the number of valid chars before the current cursor -func (p *Markdown) inline(currBlock *Node, data []byte) { - // handlers might call us recursively: enforce a maximum depth - if p.nesting >= p.maxNesting || len(data) == 0 { +func (p *parser) inline(out *bytes.Buffer, data []byte) { + // this is called recursively: enforce a maximum depth + if p.nesting >= p.maxNesting { return } p.nesting++ - beg, end := 0, 0 - for end < len(data) { - handler := p.inlineCallback[data[end]] - if handler != nil { - if consumed, node := handler(p, data, end); consumed == 0 { - // No action from the callback. - end++ - } else { - // Copy inactive chars into the output. - currBlock.AppendChild(text(data[beg:end])) - if node != nil { - currBlock.AppendChild(node) - } - // Skip past whatever the callback used. - beg = end + consumed - end = beg - } - } else { + + i, end := 0, 0 + for i < len(data) { + // copy inactive chars into the output + for end < len(data) && p.inlineCallback[data[end]] == nil { end++ } - } - if beg < len(data) { - if data[end-1] == '\n' { - end-- + + p.r.NormalText(out, data[i:end]) + + if end >= len(data) { + break + } + i = end + + // call the trigger + handler := p.inlineCallback[data[end]] + if consumed := handler(p, out, data, i); consumed == 0 { + // no action from the callback; buffer the byte for later + end = i + 1 + } else { + // skip past whatever the callback used + i += consumed + end = i } - currBlock.AppendChild(text(data[beg:end])) } + p.nesting-- } // single and double emphasis parsing -func emphasis(p *Markdown, data []byte, offset int) (int, *Node) { +func emphasis(p *parser, out *bytes.Buffer, data []byte, offset int) int { data = data[offset:] c := data[0] + ret := 0 if len(data) > 2 && data[1] != c { // whitespace cannot follow an opening emphasis; // strikethrough only takes two characters '~~' if c == '~' || isspace(data[1]) { - return 0, nil + return 0 } - ret, node := helperEmphasis(p, data[1:], c) - if ret == 0 { - return 0, nil + if ret = helperEmphasis(p, out, data[1:], c); ret == 0 { + return 0 } - return ret + 1, node + return ret + 1 } if len(data) > 3 && data[1] == c && data[2] != c { if isspace(data[2]) { - return 0, nil + return 0 } - ret, node := helperDoubleEmphasis(p, data[2:], c) - if ret == 0 { - return 0, nil + if ret = helperDoubleEmphasis(p, out, data[2:], c); ret == 0 { + return 0 } - return ret + 2, node + return ret + 2 } if len(data) > 4 && data[1] == c && data[2] == c && data[3] != c { if c == '~' || isspace(data[3]) { - return 0, nil + return 0 } - ret, node := helperTripleEmphasis(p, data, 3, c) - if ret == 0 { - return 0, nil + if ret = helperTripleEmphasis(p, out, data, 3, c); ret == 0 { + return 0 } - return ret + 3, node + return ret + 3 } - return 0, nil + return 0 } -func codeSpan(p *Markdown, data []byte, offset int) (int, *Node) { +func codeSpan(p *parser, out *bytes.Buffer, data []byte, offset int) int { data = data[offset:] nb := 0 @@ -136,7 +131,7 @@ func codeSpan(p *Markdown, data []byte, offset int) (int, *Node) { // no matching delimiter? if i < nb && end >= len(data) { - return 0, nil + return 0 } // trim outside whitespace @@ -152,36 +147,43 @@ func codeSpan(p *Markdown, data []byte, offset int) (int, *Node) { // render the code span if fBegin != fEnd { - code := NewNode(Code) - code.Literal = data[fBegin:fEnd] - return end, code + p.r.CodeSpan(out, data[fBegin:fEnd]) } - return end, nil + return end + } // newline preceded by two spaces becomes <br> -func maybeLineBreak(p *Markdown, data []byte, offset int) (int, *Node) { - origOffset := offset - for offset < len(data) && data[offset] == ' ' { - offset++ +// newline without two spaces works when EXTENSION_HARD_LINE_BREAK is enabled +func lineBreak(p *parser, out *bytes.Buffer, data []byte, offset int) int { + // remove trailing spaces from out + outBytes := out.Bytes() + end := len(outBytes) + eol := end + for eol > 0 && outBytes[eol-1] == ' ' { + eol-- } + out.Truncate(eol) - if offset < len(data) && data[offset] == '\n' { - if offset-origOffset >= 2 { - return offset - origOffset + 1, NewNode(Hardbreak) - } - return offset - origOffset, nil + precededByTwoSpaces := offset >= 2 && data[offset-2] == ' ' && data[offset-1] == ' ' + precededByBackslash := offset >= 1 && data[offset-1] == '\\' // see http://spec.commonmark.org/0.18/#example-527 + precededByBackslash = precededByBackslash && p.flags&EXTENSION_BACKSLASH_LINE_BREAK != 0 + + if p.flags&EXTENSION_JOIN_LINES != 0 { + return 1 } - return 0, nil -} -// newline without two spaces works when HardLineBreak is enabled -func lineBreak(p *Markdown, data []byte, offset int) (int, *Node) { - if p.extensions&HardLineBreak != 0 { - return 1, NewNode(Hardbreak) + // should there be a hard line break here? + if p.flags&EXTENSION_HARD_LINE_BREAK == 0 && !precededByTwoSpaces && !precededByBackslash { + return 0 + } + + if precededByBackslash && eol > 0 { + out.Truncate(eol - 1) } - return 0, nil + p.r.LineBreak(out) + return 1 } type linkType int @@ -200,43 +202,27 @@ func isReferenceStyleLink(data []byte, pos int, t linkType) bool { return pos < len(data)-1 && data[pos] == '[' && data[pos+1] != '^' } -func maybeImage(p *Markdown, data []byte, offset int) (int, *Node) { - if offset < len(data)-1 && data[offset+1] == '[' { - return link(p, data, offset) - } - return 0, nil -} - -func maybeInlineFootnote(p *Markdown, data []byte, offset int) (int, *Node) { - if offset < len(data)-1 && data[offset+1] == '[' { - return link(p, data, offset) - } - return 0, nil -} - // '[': parse a link or an image or a footnote -func link(p *Markdown, data []byte, offset int) (int, *Node) { +func link(p *parser, out *bytes.Buffer, data []byte, offset int) int { // no links allowed inside regular links, footnote, and deferred footnotes if p.insideLink && (offset > 0 && data[offset-1] == '[' || len(data)-1 > offset && data[offset+1] == '^') { - return 0, nil + return 0 } var t linkType switch { // special case: ![^text] == deferred footnote (that follows something with // an exclamation point) - case p.extensions&Footnotes != 0 && len(data)-1 > offset && data[offset+1] == '^': + case p.flags&EXTENSION_FOOTNOTES != 0 && len(data)-1 > offset && data[offset+1] == '^': t = linkDeferredFootnote // ![alt] == image - case offset >= 0 && data[offset] == '!': + case offset > 0 && data[offset-1] == '!': t = linkImg - offset++ // ^[text] == inline footnote // [^refId] == deferred footnote - case p.extensions&Footnotes != 0: - if offset >= 0 && data[offset] == '^' { + case p.flags&EXTENSION_FOOTNOTES != 0: + if offset > 0 && data[offset-1] == '^' { t = linkInlineFootnote - offset++ } else if len(data)-1 > offset && data[offset+1] == '^' { t = linkDeferredFootnote } @@ -249,7 +235,7 @@ func link(p *Markdown, data []byte, offset int) (int, *Node) { var ( i = 1 - noteID int + noteId int title, link, altContent []byte textHasNl = false ) @@ -258,6 +244,8 @@ func link(p *Markdown, data []byte, offset int) (int, *Node) { i++ } + brace := 0 + // look for the matching closing bracket for level := 1; level > 0 && i < len(data); i++ { switch { @@ -279,12 +267,11 @@ func link(p *Markdown, data []byte, offset int) (int, *Node) { } if i >= len(data) { - return 0, nil + return 0 } txtE := i i++ - var footnoteNode *Node // skip any amount of whitespace or newline // (this is much more lax than original markdown syntax) @@ -292,8 +279,8 @@ func link(p *Markdown, data []byte, offset int) (int, *Node) { i++ } - // inline style link switch { + // inline style link case i < len(data) && data[i] == '(': // skip initial whitespace i++ @@ -304,14 +291,27 @@ func link(p *Markdown, data []byte, offset int) (int, *Node) { linkB := i - // look for link end: ' " ) + // look for link end: ' " ), check for new opening braces and take this + // into account, this may lead for overshooting and probably will require + // some fine-tuning. findlinkend: for i < len(data) { switch { case data[i] == '\\': i += 2 - case data[i] == ')' || data[i] == '\'' || data[i] == '"': + case data[i] == '(': + brace++ + i++ + + case data[i] == ')': + if brace <= 0 { + break findlinkend + } + brace-- + i++ + + case data[i] == '\'' || data[i] == '"': break findlinkend default: @@ -320,7 +320,7 @@ func link(p *Markdown, data []byte, offset int) (int, *Node) { } if i >= len(data) { - return 0, nil + return 0 } linkE := i @@ -345,7 +345,7 @@ func link(p *Markdown, data []byte, offset int) (int, *Node) { } if i >= len(data) { - return 0, nil + return 0 } // skip whitespace after title @@ -397,7 +397,7 @@ func link(p *Markdown, data []byte, offset int) (int, *Node) { i++ } if i >= len(data) { - return 0, nil + return 0 } linkE := i @@ -427,7 +427,7 @@ func link(p *Markdown, data []byte, offset int) (int, *Node) { // find the reference with matching id lr, ok := p.getRef(string(id)) if !ok { - return 0, nil + return 0 } // keep link and title from reference @@ -464,10 +464,9 @@ func link(p *Markdown, data []byte, offset int) (int, *Node) { } } - footnoteNode = NewNode(Item) if t == linkInlineFootnote { // create a new reference - noteID = len(p.notes) + 1 + noteId = len(p.notes) + 1 var fragment []byte if len(id) > 0 { @@ -478,18 +477,18 @@ func link(p *Markdown, data []byte, offset int) (int, *Node) { } copy(fragment, slugify(id)) } else { - fragment = append([]byte("footnote-"), []byte(strconv.Itoa(noteID))...) + fragment = append([]byte("footnote-"), []byte(strconv.Itoa(noteId))...) } ref := &reference{ - noteID: noteID, + noteId: noteId, hasBlock: false, link: fragment, title: id, - footnote: footnoteNode, } p.notes = append(p.notes, ref) + p.notesRecord[string(ref.link)] = struct{}{} link = ref.link title = ref.title @@ -497,26 +496,40 @@ func link(p *Markdown, data []byte, offset int) (int, *Node) { // find the reference with matching id lr, ok := p.getRef(string(id)) if !ok { - return 0, nil + return 0 } - if t == linkDeferredFootnote { - lr.noteID = len(p.notes) + 1 - lr.footnote = footnoteNode + if t == linkDeferredFootnote && !p.isFootnote(lr) { + lr.noteId = len(p.notes) + 1 p.notes = append(p.notes, lr) + p.notesRecord[string(lr.link)] = struct{}{} } // keep link and title from reference link = lr.link // if inline footnote, title == footnote contents title = lr.title - noteID = lr.noteID + noteId = lr.noteId } // rewind the whitespace i = txtE + 1 } + // build content: img alt is escaped, link content is parsed + var content bytes.Buffer + if txtE > 1 { + if t == linkImg { + content.Write(data[1:txtE]) + } else { + // links cannot contain other links, so turn off link parsing temporarily + insideLink := p.insideLink + p.insideLink = true + p.inline(&content, data[1:txtE]) + p.insideLink = insideLink + } + } + var uLink []byte if t == linkNormal || t == linkImg { if len(link) > 0 { @@ -526,54 +539,49 @@ func link(p *Markdown, data []byte, offset int) (int, *Node) { } // links need something to click on and somewhere to go - if len(uLink) == 0 || (t == linkNormal && txtE <= 1) { - return 0, nil + if len(uLink) == 0 || (t == linkNormal && content.Len() == 0) { + return 0 } } // call the relevant rendering function - var linkNode *Node switch t { case linkNormal: - linkNode = NewNode(Link) - linkNode.Destination = normalizeURI(uLink) - linkNode.Title = title if len(altContent) > 0 { - linkNode.AppendChild(text(altContent)) + p.r.Link(out, uLink, title, altContent) } else { - // links cannot contain other links, so turn off link parsing - // temporarily and recurse - insideLink := p.insideLink - p.insideLink = true - p.inline(linkNode, data[1:txtE]) - p.insideLink = insideLink + p.r.Link(out, uLink, title, content.Bytes()) } case linkImg: - linkNode = NewNode(Image) - linkNode.Destination = uLink - linkNode.Title = title - linkNode.AppendChild(text(data[1:txtE])) - i++ + outSize := out.Len() + outBytes := out.Bytes() + if outSize > 0 && outBytes[outSize-1] == '!' { + out.Truncate(outSize - 1) + } - case linkInlineFootnote, linkDeferredFootnote: - linkNode = NewNode(Link) - linkNode.Destination = link - linkNode.Title = title - linkNode.NoteID = noteID - linkNode.Footnote = footnoteNode - if t == linkInlineFootnote { - i++ + p.r.Image(out, uLink, title, content.Bytes()) + + case linkInlineFootnote: + outSize := out.Len() + outBytes := out.Bytes() + if outSize > 0 && outBytes[outSize-1] == '^' { + out.Truncate(outSize - 1) } + p.r.FootnoteRef(out, link, noteId) + + case linkDeferredFootnote: + p.r.FootnoteRef(out, link, noteId) + default: - return 0, nil + return 0 } - return i, linkNode + return i } -func (p *Markdown) inlineHTMLComment(data []byte) int { +func (p *parser) inlineHTMLComment(out *bytes.Buffer, data []byte) int { if len(data) < 5 { return 0 } @@ -592,75 +600,44 @@ func (p *Markdown) inlineHTMLComment(data []byte) int { return i + 1 } -func stripMailto(link []byte) []byte { - if bytes.HasPrefix(link, []byte("mailto://")) { - return link[9:] - } else if bytes.HasPrefix(link, []byte("mailto:")) { - return link[7:] - } else { - return link - } -} - -// autolinkType specifies a kind of autolink that gets detected. -type autolinkType int - -// These are the possible flag values for the autolink renderer. -const ( - notAutolink autolinkType = iota - normalAutolink - emailAutolink -) - // '<' when tags or autolinks are allowed -func leftAngle(p *Markdown, data []byte, offset int) (int, *Node) { +func leftAngle(p *parser, out *bytes.Buffer, data []byte, offset int) int { data = data[offset:] - altype, end := tagLength(data) - if size := p.inlineHTMLComment(data); size > 0 { + altype := LINK_TYPE_NOT_AUTOLINK + end := tagLength(data, &altype) + if size := p.inlineHTMLComment(out, data); size > 0 { end = size } if end > 2 { - if altype != notAutolink { + if altype != LINK_TYPE_NOT_AUTOLINK { var uLink bytes.Buffer unescapeText(&uLink, data[1:end+1-2]) if uLink.Len() > 0 { - link := uLink.Bytes() - node := NewNode(Link) - node.Destination = link - if altype == emailAutolink { - node.Destination = append([]byte("mailto:"), link...) - } - node.AppendChild(text(stripMailto(link))) - return end, node + p.r.AutoLink(out, uLink.Bytes(), altype) } } else { - htmlTag := NewNode(HTMLSpan) - htmlTag.Literal = data[:end] - return end, htmlTag + p.r.RawHtmlTag(out, data[:end]) } } - return end, nil + return end } // '\\' backslash escape var escapeChars = []byte("\\`*_{}[]()#+-.!:|&<>~") -func escape(p *Markdown, data []byte, offset int) (int, *Node) { +func escape(p *parser, out *bytes.Buffer, data []byte, offset int) int { data = data[offset:] if len(data) > 1 { - if p.extensions&BackslashLineBreak != 0 && data[1] == '\n' { - return 2, NewNode(Hardbreak) - } if bytes.IndexByte(escapeChars, data[1]) < 0 { - return 0, nil + return 0 } - return 2, text(data[1:2]) + p.r.NormalText(out, data[1:2]) } - return 2, nil + return 2 } func unescapeText(ob *bytes.Buffer, src []byte) { @@ -686,7 +663,7 @@ func unescapeText(ob *bytes.Buffer, src []byte) { // '&' escaped when it doesn't belong to an entity // valid entities are assumed to be anything matching &#?[A-Za-z0-9]+; -func entity(p *Markdown, data []byte, offset int) (int, *Node) { +func entity(p *parser, out *bytes.Buffer, data []byte, offset int) int { data = data[offset:] end := 1 @@ -702,70 +679,25 @@ func entity(p *Markdown, data []byte, offset int) (int, *Node) { if end < len(data) && data[end] == ';' { end++ // real entity } else { - return 0, nil // lone '&' + return 0 // lone '&' } - ent := data[:end] - // undo & escaping or it will be converted to &amp; by another - // escaper in the renderer - if bytes.Equal(ent, []byte("&")) { - ent = []byte{'&'} - } + p.r.Entity(out, data[:end]) - return end, text(ent) + return end } func linkEndsWithEntity(data []byte, linkEnd int) bool { - entityRanges := htmlEntityRe.FindAllIndex(data[:linkEnd], -1) + entityRanges := htmlEntity.FindAllIndex(data[:linkEnd], -1) return entityRanges != nil && entityRanges[len(entityRanges)-1][1] == linkEnd } -// hasPrefixCaseInsensitive is a custom implementation of -// strings.HasPrefix(strings.ToLower(s), prefix) -// we rolled our own because ToLower pulls in a huge machinery of lowercasing -// anything from Unicode and that's very slow. Since this func will only be -// used on ASCII protocol prefixes, we can take shortcuts. -func hasPrefixCaseInsensitive(s, prefix []byte) bool { - if len(s) < len(prefix) { - return false - } - delta := byte('a' - 'A') - for i, b := range prefix { - if b != s[i] && b != s[i]+delta { - return false - } - } - return true -} - -var protocolPrefixes = [][]byte{ - []byte("http://"), - []byte("https://"), - []byte("ftp://"), - []byte("file://"), - []byte("mailto:"), -} - -const shortestPrefix = 6 // len("ftp://"), the shortest of the above - -func maybeAutoLink(p *Markdown, data []byte, offset int) (int, *Node) { - // quick check to rule out most false hits - if p.insideLink || len(data) < offset+shortestPrefix { - return 0, nil - } - for _, prefix := range protocolPrefixes { - endOfHead := offset + 8 // 8 is the len() of the longest prefix - if endOfHead > len(data) { - endOfHead = len(data) - } - if hasPrefixCaseInsensitive(data[offset:endOfHead], prefix) { - return autoLink(p, data, offset) - } +func autoLink(p *parser, out *bytes.Buffer, data []byte, offset int) int { + // quick check to rule out most false hits on ':' + if p.insideLink || len(data) < offset+3 || data[offset+1] != '/' || data[offset+2] != '/' { + return 0 } - return 0, nil -} -func autoLink(p *Markdown, data []byte, offset int) (int, *Node) { // Now a more expensive check to see if we're not inside an anchor element anchorStart := offset offsetFromAnchor := 0 @@ -776,9 +708,8 @@ func autoLink(p *Markdown, data []byte, offset int) (int, *Node) { anchorStr := anchorRe.Find(data[anchorStart:]) if anchorStr != nil { - anchorClose := NewNode(HTMLSpan) - anchorClose.Literal = anchorStr[offsetFromAnchor:] - return len(anchorStr) - offsetFromAnchor, anchorClose + out.Write(anchorStr[offsetFromAnchor:]) + return len(anchorStr) - offsetFromAnchor } // scan backward for a word boundary @@ -787,14 +718,14 @@ func autoLink(p *Markdown, data []byte, offset int) (int, *Node) { rewind++ } if rewind > 6 { // longest supported protocol is "mailto" which has 6 letters - return 0, nil + return 0 } origData := data data = data[offset-rewind:] if !isSafeLink(data) { - return 0, nil + return 0 } linkEnd := 0 @@ -871,17 +802,19 @@ func autoLink(p *Markdown, data []byte, offset int) (int, *Node) { } } + // we were triggered on the ':', so we need to rewind the output a bit + if out.Len() >= rewind { + out.Truncate(len(out.Bytes()) - rewind) + } + var uLink bytes.Buffer unescapeText(&uLink, data[:linkEnd]) if uLink.Len() > 0 { - node := NewNode(Link) - node.Destination = uLink.Bytes() - node.AppendChild(text(uLink.Bytes())) - return linkEnd, node + p.r.AutoLink(out, uLink.Bytes(), LINK_TYPE_NORMAL) } - return linkEnd, nil + return linkEnd - rewind } func isEndOfLink(char byte) bool { @@ -914,17 +847,17 @@ func isSafeLink(link []byte) bool { } // return the length of the given tag, or 0 is it's not valid -func tagLength(data []byte) (autolink autolinkType, end int) { +func tagLength(data []byte, autolink *int) int { var i, j int // a valid tag can't be shorter than 3 chars if len(data) < 3 { - return notAutolink, 0 + return 0 } // begins with a '<' optionally followed by '/', followed by letter or number if data[0] != '<' { - return notAutolink, 0 + return 0 } if data[1] == '/' { i = 2 @@ -933,11 +866,11 @@ func tagLength(data []byte) (autolink autolinkType, end int) { } if !isalnum(data[i]) { - return notAutolink, 0 + return 0 } // scheme test - autolink = notAutolink + *autolink = LINK_TYPE_NOT_AUTOLINK // try to find the beginning of an URI for i < len(data) && (isalnum(data[i]) || data[i] == '.' || data[i] == '+' || data[i] == '-') { @@ -946,20 +879,21 @@ func tagLength(data []byte) (autolink autolinkType, end int) { if i > 1 && i < len(data) && data[i] == '@' { if j = isMailtoAutoLink(data[i:]); j != 0 { - return emailAutolink, i + j + *autolink = LINK_TYPE_EMAIL + return i + j } } if i > 2 && i < len(data) && data[i] == ':' { - autolink = normalAutolink + *autolink = LINK_TYPE_NORMAL i++ } // complete autolink test: no whitespace or ' or " switch { case i >= len(data): - autolink = notAutolink - case autolink != notAutolink: + *autolink = LINK_TYPE_NOT_AUTOLINK + case *autolink != 0: j = i for i < len(data) { @@ -974,20 +908,24 @@ func tagLength(data []byte) (autolink autolinkType, end int) { } if i >= len(data) { - return autolink, 0 + return 0 } if i > j && data[i] == '>' { - return autolink, i + 1 + return i + 1 } // one of the forbidden chars has been found - autolink = notAutolink + *autolink = LINK_TYPE_NOT_AUTOLINK } - i += bytes.IndexByte(data[i:], '>') - if i < 0 { - return autolink, 0 + + // look for something looking like a tag end + for i < len(data) && data[i] != '>' { + i++ + } + if i >= len(data) { + return 0 } - return autolink, i + 1 + return i + 1 } // look for the address part of a mail autolink and '>' @@ -1006,13 +944,14 @@ func isMailtoAutoLink(data []byte) int { nb++ case '-', '.', '_': - break + // Do nothing. case '>': if nb == 1 { return i + 1 + } else { + return 0 } - return 0 default: return 0 } @@ -1075,8 +1014,9 @@ func helperFindEmphChar(data []byte, c byte) int { if data[i] != '[' && data[i] != '(' { // not a link if tmpI > 0 { return tmpI + } else { + continue } - continue } cc := data[i] i++ @@ -1095,7 +1035,7 @@ func helperFindEmphChar(data []byte, c byte) int { return 0 } -func helperEmphasis(p *Markdown, data []byte, c byte) (int, *Node) { +func helperEmphasis(p *parser, out *bytes.Buffer, data []byte, c byte) int { i := 0 // skip one symbol if coming from emph3 @@ -1106,11 +1046,11 @@ func helperEmphasis(p *Markdown, data []byte, c byte) (int, *Node) { for i < len(data) { length := helperFindEmphChar(data[i:], c) if length == 0 { - return 0, nil + return 0 } i += length if i >= len(data) { - return 0, nil + return 0 } if i+1 < len(data) && data[i+1] == c { @@ -1120,46 +1060,52 @@ func helperEmphasis(p *Markdown, data []byte, c byte) (int, *Node) { if data[i] == c && !isspace(data[i-1]) { - if p.extensions&NoIntraEmphasis != 0 { + if p.flags&EXTENSION_NO_INTRA_EMPHASIS != 0 { if !(i+1 == len(data) || isspace(data[i+1]) || ispunct(data[i+1])) { continue } } - emph := NewNode(Emph) - p.inline(emph, data[:i]) - return i + 1, emph + var work bytes.Buffer + p.inline(&work, data[:i]) + p.r.Emphasis(out, work.Bytes()) + return i + 1 } } - return 0, nil + return 0 } -func helperDoubleEmphasis(p *Markdown, data []byte, c byte) (int, *Node) { +func helperDoubleEmphasis(p *parser, out *bytes.Buffer, data []byte, c byte) int { i := 0 for i < len(data) { length := helperFindEmphChar(data[i:], c) if length == 0 { - return 0, nil + return 0 } i += length if i+1 < len(data) && data[i] == c && data[i+1] == c && i > 0 && !isspace(data[i-1]) { - nodeType := Strong - if c == '~' { - nodeType = Del + var work bytes.Buffer + p.inline(&work, data[:i]) + + if work.Len() > 0 { + // pick the right renderer + if c == '~' { + p.r.StrikeThrough(out, work.Bytes()) + } else { + p.r.DoubleEmphasis(out, work.Bytes()) + } } - node := NewNode(nodeType) - p.inline(node, data[:i]) - return i + 2, node + return i + 2 } i++ } - return 0, nil + return 0 } -func helperTripleEmphasis(p *Markdown, data []byte, offset int, c byte) (int, *Node) { +func helperTripleEmphasis(p *parser, out *bytes.Buffer, data []byte, offset int, c byte) int { i := 0 origData := data data = data[offset:] @@ -1167,7 +1113,7 @@ func helperTripleEmphasis(p *Markdown, data []byte, offset int, c byte) (int, *N for i < len(data) { length := helperFindEmphChar(data[i:], c) if length == 0 { - return 0, nil + return 0 } i += length @@ -1179,36 +1125,30 @@ func helperTripleEmphasis(p *Markdown, data []byte, offset int, c byte) (int, *N switch { case i+2 < len(data) && data[i+1] == c && data[i+2] == c: // triple symbol found - strong := NewNode(Strong) - em := NewNode(Emph) - strong.AppendChild(em) - p.inline(em, data[:i]) - return i + 3, strong + var work bytes.Buffer + + p.inline(&work, data[:i]) + if work.Len() > 0 { + p.r.TripleEmphasis(out, work.Bytes()) + } + return i + 3 case (i+1 < len(data) && data[i+1] == c): // double symbol found, hand over to emph1 - length, node := helperEmphasis(p, origData[offset-2:], c) + length = helperEmphasis(p, out, origData[offset-2:], c) if length == 0 { - return 0, nil + return 0 + } else { + return length - 2 } - return length - 2, node default: // single symbol found, hand over to emph2 - length, node := helperDoubleEmphasis(p, origData[offset-1:], c) + length = helperDoubleEmphasis(p, out, origData[offset-1:], c) if length == 0 { - return 0, nil + return 0 + } else { + return length - 1 } - return length - 1, node } } - return 0, nil -} - -func text(s []byte) *Node { - node := NewNode(Text) - node.Literal = s - return node -} - -func normalizeURI(s []byte) []byte { - return s // TODO: implement + return 0 } diff --git a/vendor/github.com/russross/blackfriday/latex.go b/vendor/github.com/russross/blackfriday/latex.go new file mode 100644 index 00000000..3d30d094 --- /dev/null +++ b/vendor/github.com/russross/blackfriday/latex.go @@ -0,0 +1,334 @@ +// +// Blackfriday Markdown Processor +// Available at http://github.com/russross/blackfriday +// +// Copyright © 2011 Russ Ross <russ@russross.com>. +// Distributed under the Simplified BSD License. +// See README.md for details. +// + +// +// +// LaTeX rendering backend +// +// + +package blackfriday + +import ( + "bytes" + "strings" +) + +// Latex is a type that implements the Renderer interface for LaTeX output. +// +// Do not create this directly, instead use the LatexRenderer function. +type Latex struct { +} + +// LatexRenderer creates and configures a Latex object, which +// satisfies the Renderer interface. +// +// flags is a set of LATEX_* options ORed together (currently no such options +// are defined). +func LatexRenderer(flags int) Renderer { + return &Latex{} +} + +func (options *Latex) GetFlags() int { + return 0 +} + +// render code chunks using verbatim, or listings if we have a language +func (options *Latex) BlockCode(out *bytes.Buffer, text []byte, info string) { + if info == "" { + out.WriteString("\n\\begin{verbatim}\n") + } else { + lang := strings.Fields(info)[0] + out.WriteString("\n\\begin{lstlisting}[language=") + out.WriteString(lang) + out.WriteString("]\n") + } + out.Write(text) + if info == "" { + out.WriteString("\n\\end{verbatim}\n") + } else { + out.WriteString("\n\\end{lstlisting}\n") + } +} + +func (options *Latex) TitleBlock(out *bytes.Buffer, text []byte) { + +} + +func (options *Latex) BlockQuote(out *bytes.Buffer, text []byte) { + out.WriteString("\n\\begin{quotation}\n") + out.Write(text) + out.WriteString("\n\\end{quotation}\n") +} + +func (options *Latex) BlockHtml(out *bytes.Buffer, text []byte) { + // a pretty lame thing to do... + out.WriteString("\n\\begin{verbatim}\n") + out.Write(text) + out.WriteString("\n\\end{verbatim}\n") +} + +func (options *Latex) Header(out *bytes.Buffer, text func() bool, level int, id string) { + marker := out.Len() + + switch level { + case 1: + out.WriteString("\n\\section{") + case 2: + out.WriteString("\n\\subsection{") + case 3: + out.WriteString("\n\\subsubsection{") + case 4: + out.WriteString("\n\\paragraph{") + case 5: + out.WriteString("\n\\subparagraph{") + case 6: + out.WriteString("\n\\textbf{") + } + if !text() { + out.Truncate(marker) + return + } + out.WriteString("}\n") +} + +func (options *Latex) HRule(out *bytes.Buffer) { + out.WriteString("\n\\HRule\n") +} + +func (options *Latex) List(out *bytes.Buffer, text func() bool, flags int) { + marker := out.Len() + if flags&LIST_TYPE_ORDERED != 0 { + out.WriteString("\n\\begin{enumerate}\n") + } else { + out.WriteString("\n\\begin{itemize}\n") + } + if !text() { + out.Truncate(marker) + return + } + if flags&LIST_TYPE_ORDERED != 0 { + out.WriteString("\n\\end{enumerate}\n") + } else { + out.WriteString("\n\\end{itemize}\n") + } +} + +func (options *Latex) ListItem(out *bytes.Buffer, text []byte, flags int) { + out.WriteString("\n\\item ") + out.Write(text) +} + +func (options *Latex) Paragraph(out *bytes.Buffer, text func() bool) { + marker := out.Len() + out.WriteString("\n") + if !text() { + out.Truncate(marker) + return + } + out.WriteString("\n") +} + +func (options *Latex) Table(out *bytes.Buffer, header []byte, body []byte, columnData []int) { + out.WriteString("\n\\begin{tabular}{") + for _, elt := range columnData { + switch elt { + case TABLE_ALIGNMENT_LEFT: + out.WriteByte('l') + case TABLE_ALIGNMENT_RIGHT: + out.WriteByte('r') + default: + out.WriteByte('c') + } + } + out.WriteString("}\n") + out.Write(header) + out.WriteString(" \\\\\n\\hline\n") + out.Write(body) + out.WriteString("\n\\end{tabular}\n") +} + +func (options *Latex) TableRow(out *bytes.Buffer, text []byte) { + if out.Len() > 0 { + out.WriteString(" \\\\\n") + } + out.Write(text) +} + +func (options *Latex) TableHeaderCell(out *bytes.Buffer, text []byte, align int) { + if out.Len() > 0 { + out.WriteString(" & ") + } + out.Write(text) +} + +func (options *Latex) TableCell(out *bytes.Buffer, text []byte, align int) { + if out.Len() > 0 { + out.WriteString(" & ") + } + out.Write(text) +} + +// TODO: this +func (options *Latex) Footnotes(out *bytes.Buffer, text func() bool) { + +} + +func (options *Latex) FootnoteItem(out *bytes.Buffer, name, text []byte, flags int) { + +} + +func (options *Latex) AutoLink(out *bytes.Buffer, link []byte, kind int) { + out.WriteString("\\href{") + if kind == LINK_TYPE_EMAIL { + out.WriteString("mailto:") + } + out.Write(link) + out.WriteString("}{") + out.Write(link) + out.WriteString("}") +} + +func (options *Latex) CodeSpan(out *bytes.Buffer, text []byte) { + out.WriteString("\\texttt{") + escapeSpecialChars(out, text) + out.WriteString("}") +} + +func (options *Latex) DoubleEmphasis(out *bytes.Buffer, text []byte) { + out.WriteString("\\textbf{") + out.Write(text) + out.WriteString("}") +} + +func (options *Latex) Emphasis(out *bytes.Buffer, text []byte) { + out.WriteString("\\textit{") + out.Write(text) + out.WriteString("}") +} + +func (options *Latex) Image(out *bytes.Buffer, link []byte, title []byte, alt []byte) { + if bytes.HasPrefix(link, []byte("http://")) || bytes.HasPrefix(link, []byte("https://")) { + // treat it like a link + out.WriteString("\\href{") + out.Write(link) + out.WriteString("}{") + out.Write(alt) + out.WriteString("}") + } else { + out.WriteString("\\includegraphics{") + out.Write(link) + out.WriteString("}") + } +} + +func (options *Latex) LineBreak(out *bytes.Buffer) { + out.WriteString(" \\\\\n") +} + +func (options *Latex) Link(out *bytes.Buffer, link []byte, title []byte, content []byte) { + out.WriteString("\\href{") + out.Write(link) + out.WriteString("}{") + out.Write(content) + out.WriteString("}") +} + +func (options *Latex) RawHtmlTag(out *bytes.Buffer, tag []byte) { +} + +func (options *Latex) TripleEmphasis(out *bytes.Buffer, text []byte) { + out.WriteString("\\textbf{\\textit{") + out.Write(text) + out.WriteString("}}") +} + +func (options *Latex) StrikeThrough(out *bytes.Buffer, text []byte) { + out.WriteString("\\sout{") + out.Write(text) + out.WriteString("}") +} + +// TODO: this +func (options *Latex) FootnoteRef(out *bytes.Buffer, ref []byte, id int) { + +} + +func needsBackslash(c byte) bool { + for _, r := range []byte("_{}%$&\\~#") { + if c == r { + return true + } + } + return false +} + +func escapeSpecialChars(out *bytes.Buffer, text []byte) { + for i := 0; i < len(text); i++ { + // directly copy normal characters + org := i + + for i < len(text) && !needsBackslash(text[i]) { + i++ + } + if i > org { + out.Write(text[org:i]) + } + + // escape a character + if i >= len(text) { + break + } + out.WriteByte('\\') + out.WriteByte(text[i]) + } +} + +func (options *Latex) Entity(out *bytes.Buffer, entity []byte) { + // TODO: convert this into a unicode character or something + out.Write(entity) +} + +func (options *Latex) NormalText(out *bytes.Buffer, text []byte) { + escapeSpecialChars(out, text) +} + +// header and footer +func (options *Latex) DocumentHeader(out *bytes.Buffer) { + out.WriteString("\\documentclass{article}\n") + out.WriteString("\n") + out.WriteString("\\usepackage{graphicx}\n") + out.WriteString("\\usepackage{listings}\n") + out.WriteString("\\usepackage[margin=1in]{geometry}\n") + out.WriteString("\\usepackage[utf8]{inputenc}\n") + out.WriteString("\\usepackage{verbatim}\n") + out.WriteString("\\usepackage[normalem]{ulem}\n") + out.WriteString("\\usepackage{hyperref}\n") + out.WriteString("\n") + out.WriteString("\\hypersetup{colorlinks,%\n") + out.WriteString(" citecolor=black,%\n") + out.WriteString(" filecolor=black,%\n") + out.WriteString(" linkcolor=black,%\n") + out.WriteString(" urlcolor=black,%\n") + out.WriteString(" pdfstartview=FitH,%\n") + out.WriteString(" breaklinks=true,%\n") + out.WriteString(" pdfauthor={Blackfriday Markdown Processor v") + out.WriteString(VERSION) + out.WriteString("}}\n") + out.WriteString("\n") + out.WriteString("\\newcommand{\\HRule}{\\rule{\\linewidth}{0.5mm}}\n") + out.WriteString("\\addtolength{\\parskip}{0.5\\baselineskip}\n") + out.WriteString("\\parindent=0pt\n") + out.WriteString("\n") + out.WriteString("\\begin{document}\n") +} + +func (options *Latex) DocumentFooter(out *bytes.Buffer) { + out.WriteString("\n\\end{document}\n") +} diff --git a/vendor/github.com/russross/blackfriday/markdown.go b/vendor/github.com/russross/blackfriday/markdown.go index ff61cb05..41595d62 100644 --- a/vendor/github.com/russross/blackfriday/markdown.go +++ b/vendor/github.com/russross/blackfriday/markdown.go @@ -1,200 +1,230 @@ +// // Blackfriday Markdown Processor // Available at http://github.com/russross/blackfriday // // Copyright © 2011 Russ Ross <russ@russross.com>. // Distributed under the Simplified BSD License. // See README.md for details. +// + +// +// +// Markdown parsing and processing +// +// package blackfriday import ( "bytes" "fmt" - "io" "strings" "unicode/utf8" ) -// -// Markdown parsing and processing -// - -// Version string of the package. Appears in the rendered document when -// CompletePage flag is on. -const Version = "2.0" - -// Extensions is a bitwise or'ed collection of enabled Blackfriday's -// extensions. -type Extensions int +const VERSION = "1.5" // These are the supported markdown parsing extensions. // OR these values together to select multiple extensions. const ( - NoExtensions Extensions = 0 - NoIntraEmphasis Extensions = 1 << iota // Ignore emphasis markers inside words - Tables // Render tables - FencedCode // Render fenced code blocks - Autolink // Detect embedded URLs that are not explicitly marked - Strikethrough // Strikethrough text using ~~test~~ - LaxHTMLBlocks // Loosen up HTML block parsing rules - SpaceHeadings // Be strict about prefix heading rules - HardLineBreak // Translate newlines into line breaks - TabSizeEight // Expand tabs to eight spaces instead of four - Footnotes // Pandoc-style footnotes - NoEmptyLineBeforeBlock // No need to insert an empty line to start a (code, quote, ordered list, unordered list) block - HeadingIDs // specify heading IDs with {#id} - Titleblock // Titleblock ala pandoc - AutoHeadingIDs // Create the heading ID from the text - BackslashLineBreak // Translate trailing backslashes into line breaks - DefinitionLists // Render definition lists - - CommonHTMLFlags HTMLFlags = UseXHTML | Smartypants | - SmartypantsFractions | SmartypantsDashes | SmartypantsLatexDashes - - CommonExtensions Extensions = NoIntraEmphasis | Tables | FencedCode | - Autolink | Strikethrough | SpaceHeadings | HeadingIDs | - BackslashLineBreak | DefinitionLists + EXTENSION_NO_INTRA_EMPHASIS = 1 << iota // ignore emphasis markers inside words + EXTENSION_TABLES // render tables + EXTENSION_FENCED_CODE // render fenced code blocks + EXTENSION_AUTOLINK // detect embedded URLs that are not explicitly marked + EXTENSION_STRIKETHROUGH // strikethrough text using ~~test~~ + EXTENSION_LAX_HTML_BLOCKS // loosen up HTML block parsing rules + EXTENSION_SPACE_HEADERS // be strict about prefix header rules + EXTENSION_HARD_LINE_BREAK // translate newlines into line breaks + EXTENSION_TAB_SIZE_EIGHT // expand tabs to eight spaces instead of four + EXTENSION_FOOTNOTES // Pandoc-style footnotes + EXTENSION_NO_EMPTY_LINE_BEFORE_BLOCK // No need to insert an empty line to start a (code, quote, ordered list, unordered list) block + EXTENSION_HEADER_IDS // specify header IDs with {#id} + EXTENSION_TITLEBLOCK // Titleblock ala pandoc + EXTENSION_AUTO_HEADER_IDS // Create the header ID from the text + EXTENSION_BACKSLASH_LINE_BREAK // translate trailing backslashes into line breaks + EXTENSION_DEFINITION_LISTS // render definition lists + EXTENSION_JOIN_LINES // delete newline and join lines + + commonHtmlFlags = 0 | + HTML_USE_XHTML | + HTML_USE_SMARTYPANTS | + HTML_SMARTYPANTS_FRACTIONS | + HTML_SMARTYPANTS_DASHES | + HTML_SMARTYPANTS_LATEX_DASHES + + commonExtensions = 0 | + EXTENSION_NO_INTRA_EMPHASIS | + EXTENSION_TABLES | + EXTENSION_FENCED_CODE | + EXTENSION_AUTOLINK | + EXTENSION_STRIKETHROUGH | + EXTENSION_SPACE_HEADERS | + EXTENSION_HEADER_IDS | + EXTENSION_BACKSLASH_LINE_BREAK | + EXTENSION_DEFINITION_LISTS ) -// ListType contains bitwise or'ed flags for list and list item objects. -type ListType int +// These are the possible flag values for the link renderer. +// Only a single one of these values will be used; they are not ORed together. +// These are mostly of interest if you are writing a new output format. +const ( + LINK_TYPE_NOT_AUTOLINK = iota + LINK_TYPE_NORMAL + LINK_TYPE_EMAIL +) // These are the possible flag values for the ListItem renderer. // Multiple flag values may be ORed together. // These are mostly of interest if you are writing a new output format. const ( - ListTypeOrdered ListType = 1 << iota - ListTypeDefinition - ListTypeTerm - - ListItemContainsBlock - ListItemBeginningOfList // TODO: figure out if this is of any use now - ListItemEndOfList + LIST_TYPE_ORDERED = 1 << iota + LIST_TYPE_DEFINITION + LIST_TYPE_TERM + LIST_ITEM_CONTAINS_BLOCK + LIST_ITEM_BEGINNING_OF_LIST + LIST_ITEM_END_OF_LIST ) -// CellAlignFlags holds a type of alignment in a table cell. -type CellAlignFlags int - // These are the possible flag values for the table cell renderer. // Only a single one of these values will be used; they are not ORed together. // These are mostly of interest if you are writing a new output format. const ( - TableAlignmentLeft CellAlignFlags = 1 << iota - TableAlignmentRight - TableAlignmentCenter = (TableAlignmentLeft | TableAlignmentRight) + TABLE_ALIGNMENT_LEFT = 1 << iota + TABLE_ALIGNMENT_RIGHT + TABLE_ALIGNMENT_CENTER = (TABLE_ALIGNMENT_LEFT | TABLE_ALIGNMENT_RIGHT) ) // The size of a tab stop. const ( - TabSizeDefault = 4 - TabSizeDouble = 8 + TAB_SIZE_DEFAULT = 4 + TAB_SIZE_EIGHT = 8 ) // blockTags is a set of tags that are recognized as HTML block tags. // Any of these can be included in markdown text without special escaping. var blockTags = map[string]struct{}{ - "blockquote": struct{}{}, - "del": struct{}{}, - "div": struct{}{}, - "dl": struct{}{}, - "fieldset": struct{}{}, - "form": struct{}{}, - "h1": struct{}{}, - "h2": struct{}{}, - "h3": struct{}{}, - "h4": struct{}{}, - "h5": struct{}{}, - "h6": struct{}{}, - "iframe": struct{}{}, - "ins": struct{}{}, - "math": struct{}{}, - "noscript": struct{}{}, - "ol": struct{}{}, - "pre": struct{}{}, - "p": struct{}{}, - "script": struct{}{}, - "style": struct{}{}, - "table": struct{}{}, - "ul": struct{}{}, + "blockquote": {}, + "del": {}, + "div": {}, + "dl": {}, + "fieldset": {}, + "form": {}, + "h1": {}, + "h2": {}, + "h3": {}, + "h4": {}, + "h5": {}, + "h6": {}, + "iframe": {}, + "ins": {}, + "math": {}, + "noscript": {}, + "ol": {}, + "pre": {}, + "p": {}, + "script": {}, + "style": {}, + "table": {}, + "ul": {}, // HTML5 - "address": struct{}{}, - "article": struct{}{}, - "aside": struct{}{}, - "canvas": struct{}{}, - "figcaption": struct{}{}, - "figure": struct{}{}, - "footer": struct{}{}, - "header": struct{}{}, - "hgroup": struct{}{}, - "main": struct{}{}, - "nav": struct{}{}, - "output": struct{}{}, - "progress": struct{}{}, - "section": struct{}{}, - "video": struct{}{}, + "address": {}, + "article": {}, + "aside": {}, + "canvas": {}, + "figcaption": {}, + "figure": {}, + "footer": {}, + "header": {}, + "hgroup": {}, + "main": {}, + "nav": {}, + "output": {}, + "progress": {}, + "section": {}, + "video": {}, } -// Renderer is the rendering interface. This is mostly of interest if you are -// implementing a new rendering format. +// Renderer is the rendering interface. +// This is mostly of interest if you are implementing a new rendering format. // -// Only an HTML implementation is provided in this repository, see the README -// for external implementations. +// When a byte slice is provided, it contains the (rendered) contents of the +// element. +// +// When a callback is provided instead, it will write the contents of the +// respective element directly to the output buffer and return true on success. +// If the callback returns false, the rendering function should reset the +// output buffer as though it had never been called. +// +// Currently Html and Latex implementations are provided type Renderer interface { - // RenderNode is the main rendering method. It will be called once for - // every leaf node and twice for every non-leaf node (first with - // entering=true, then with entering=false). The method should write its - // rendition of the node to the supplied writer w. - RenderNode(w io.Writer, node *Node, entering bool) WalkStatus - - // RenderHeader is a method that allows the renderer to produce some - // content preceding the main body of the output document. The header is - // understood in the broad sense here. For example, the default HTML - // renderer will write not only the HTML document preamble, but also the - // table of contents if it was requested. - // - // The method will be passed an entire document tree, in case a particular - // implementation needs to inspect it to produce output. - // - // The output should be written to the supplied writer w. If your - // implementation has no header to write, supply an empty implementation. - RenderHeader(w io.Writer, ast *Node) - - // RenderFooter is a symmetric counterpart of RenderHeader. - RenderFooter(w io.Writer, ast *Node) + // block-level callbacks + BlockCode(out *bytes.Buffer, text []byte, infoString string) + BlockQuote(out *bytes.Buffer, text []byte) + BlockHtml(out *bytes.Buffer, text []byte) + Header(out *bytes.Buffer, text func() bool, level int, id string) + HRule(out *bytes.Buffer) + List(out *bytes.Buffer, text func() bool, flags int) + ListItem(out *bytes.Buffer, text []byte, flags int) + Paragraph(out *bytes.Buffer, text func() bool) + Table(out *bytes.Buffer, header []byte, body []byte, columnData []int) + TableRow(out *bytes.Buffer, text []byte) + TableHeaderCell(out *bytes.Buffer, text []byte, flags int) + TableCell(out *bytes.Buffer, text []byte, flags int) + Footnotes(out *bytes.Buffer, text func() bool) + FootnoteItem(out *bytes.Buffer, name, text []byte, flags int) + TitleBlock(out *bytes.Buffer, text []byte) + + // Span-level callbacks + AutoLink(out *bytes.Buffer, link []byte, kind int) + CodeSpan(out *bytes.Buffer, text []byte) + DoubleEmphasis(out *bytes.Buffer, text []byte) + Emphasis(out *bytes.Buffer, text []byte) + Image(out *bytes.Buffer, link []byte, title []byte, alt []byte) + LineBreak(out *bytes.Buffer) + Link(out *bytes.Buffer, link []byte, title []byte, content []byte) + RawHtmlTag(out *bytes.Buffer, tag []byte) + TripleEmphasis(out *bytes.Buffer, text []byte) + StrikeThrough(out *bytes.Buffer, text []byte) + FootnoteRef(out *bytes.Buffer, ref []byte, id int) + + // Low-level callbacks + Entity(out *bytes.Buffer, entity []byte) + NormalText(out *bytes.Buffer, text []byte) + + // Header and footer + DocumentHeader(out *bytes.Buffer) + DocumentFooter(out *bytes.Buffer) + + GetFlags() int } // Callback functions for inline parsing. One such function is defined // for each character that triggers a response when parsing inline data. -type inlineParser func(p *Markdown, data []byte, offset int) (int, *Node) - -// Markdown is a type that holds extensions and the runtime state used by -// Parse, and the renderer. You can not use it directly, construct it with New. -type Markdown struct { - renderer Renderer - referenceOverride ReferenceOverrideFunc - refs map[string]*reference - inlineCallback [256]inlineParser - extensions Extensions - nesting int - maxNesting int - insideLink bool +type inlineParser func(p *parser, out *bytes.Buffer, data []byte, offset int) int + +// Parser holds runtime state used by the parser. +// This is constructed by the Markdown function. +type parser struct { + r Renderer + refOverride ReferenceOverrideFunc + refs map[string]*reference + inlineCallback [256]inlineParser + flags int + nesting int + maxNesting int + insideLink bool // Footnotes need to be ordered as well as available to quickly check for // presence. If a ref is also a footnote, it's stored both in refs and here // in notes. Slice is nil if footnotes not enabled. - notes []*reference - - doc *Node - tip *Node // = doc - oldTip *Node - lastMatchedContainer *Node // = doc - allClosed bool + notes []*reference + notesRecord map[string]struct{} } -func (p *Markdown) getRef(refid string) (ref *reference, found bool) { - if p.referenceOverride != nil { - r, overridden := p.referenceOverride(refid) +func (p *parser) getRef(refid string) (ref *reference, found bool) { + if p.refOverride != nil { + r, overridden := p.refOverride(refid) if overridden { if r == nil { return nil, false @@ -202,7 +232,7 @@ func (p *Markdown) getRef(refid string) (ref *reference, found bool) { return &reference{ link: []byte(r.Link), title: []byte(r.Title), - noteID: 0, + noteId: 0, hasBlock: false, text: []byte(r.Text)}, true } @@ -212,34 +242,9 @@ func (p *Markdown) getRef(refid string) (ref *reference, found bool) { return ref, found } -func (p *Markdown) finalize(block *Node) { - above := block.Parent - block.open = false - p.tip = above -} - -func (p *Markdown) addChild(node NodeType, offset uint32) *Node { - return p.addExistingChild(NewNode(node), offset) -} - -func (p *Markdown) addExistingChild(node *Node, offset uint32) *Node { - for !p.tip.canContain(node.Type) { - p.finalize(p.tip) - } - p.tip.AppendChild(node) - p.tip = node - return node -} - -func (p *Markdown) closeUnmatchedBlocks() { - if !p.allClosed { - for p.oldTip != p.lastMatchedContainer { - parent := p.oldTip.Parent - p.finalize(p.oldTip) - p.oldTip = parent - } - p.allClosed = true - } +func (p *parser) isFootnote(ref *reference) bool { + _, ok := p.notesRecord[string(ref.link)] + return ok } // @@ -266,27 +271,102 @@ type Reference struct { // See the documentation in Options for more details on use-case. type ReferenceOverrideFunc func(reference string) (ref *Reference, overridden bool) -// New constructs a Markdown processor. You can use the same With* functions as -// for Run() to customize parser's behavior and the renderer. -func New(opts ...Option) *Markdown { - var p Markdown - for _, opt := range opts { - opt(&p) +// Options represents configurable overrides and callbacks (in addition to the +// extension flag set) for configuring a Markdown parse. +type Options struct { + // Extensions is a flag set of bit-wise ORed extension bits. See the + // EXTENSION_* flags defined in this package. + Extensions int + + // ReferenceOverride is an optional function callback that is called every + // time a reference is resolved. + // + // In Markdown, the link reference syntax can be made to resolve a link to + // a reference instead of an inline URL, in one of the following ways: + // + // * [link text][refid] + // * [refid][] + // + // Usually, the refid is defined at the bottom of the Markdown document. If + // this override function is provided, the refid is passed to the override + // function first, before consulting the defined refids at the bottom. If + // the override function indicates an override did not occur, the refids at + // the bottom will be used to fill in the link details. + ReferenceOverride ReferenceOverrideFunc +} + +// MarkdownBasic is a convenience function for simple rendering. +// It processes markdown input with no extensions enabled. +func MarkdownBasic(input []byte) []byte { + // set up the HTML renderer + htmlFlags := HTML_USE_XHTML + renderer := HtmlRenderer(htmlFlags, "", "") + + // set up the parser + return MarkdownOptions(input, renderer, Options{Extensions: 0}) +} + +// Call Markdown with most useful extensions enabled +// MarkdownCommon is a convenience function for simple rendering. +// It processes markdown input with common extensions enabled, including: +// +// * Smartypants processing with smart fractions and LaTeX dashes +// +// * Intra-word emphasis suppression +// +// * Tables +// +// * Fenced code blocks +// +// * Autolinking +// +// * Strikethrough support +// +// * Strict header parsing +// +// * Custom Header IDs +func MarkdownCommon(input []byte) []byte { + // set up the HTML renderer + renderer := HtmlRenderer(commonHtmlFlags, "", "") + return MarkdownOptions(input, renderer, Options{ + Extensions: commonExtensions}) +} + +// Markdown is the main rendering function. +// It parses and renders a block of markdown-encoded text. +// The supplied Renderer is used to format the output, and extensions dictates +// which non-standard extensions are enabled. +// +// To use the supplied Html or LaTeX renderers, see HtmlRenderer and +// LatexRenderer, respectively. +func Markdown(input []byte, renderer Renderer, extensions int) []byte { + return MarkdownOptions(input, renderer, Options{ + Extensions: extensions}) +} + +// MarkdownOptions is just like Markdown but takes additional options through +// the Options struct. +func MarkdownOptions(input []byte, renderer Renderer, opts Options) []byte { + // no point in parsing if we can't render + if renderer == nil { + return nil } + + extensions := opts.Extensions + + // fill in the render structure + p := new(parser) + p.r = renderer + p.flags = extensions + p.refOverride = opts.ReferenceOverride p.refs = make(map[string]*reference) p.maxNesting = 16 p.insideLink = false - docNode := NewNode(Document) - p.doc = docNode - p.tip = docNode - p.oldTip = docNode - p.lastMatchedContainer = docNode - p.allClosed = true + // register inline parsers - p.inlineCallback[' '] = maybeLineBreak p.inlineCallback['*'] = emphasis p.inlineCallback['_'] = emphasis - if p.extensions&Strikethrough != 0 { + if extensions&EXTENSION_STRIKETHROUGH != 0 { p.inlineCallback['~'] = emphasis } p.inlineCallback['`'] = codeSpan @@ -295,166 +375,116 @@ func New(opts ...Option) *Markdown { p.inlineCallback['<'] = leftAngle p.inlineCallback['\\'] = escape p.inlineCallback['&'] = entity - p.inlineCallback['!'] = maybeImage - p.inlineCallback['^'] = maybeInlineFootnote - if p.extensions&Autolink != 0 { - p.inlineCallback['h'] = maybeAutoLink - p.inlineCallback['m'] = maybeAutoLink - p.inlineCallback['f'] = maybeAutoLink - p.inlineCallback['H'] = maybeAutoLink - p.inlineCallback['M'] = maybeAutoLink - p.inlineCallback['F'] = maybeAutoLink - } - if p.extensions&Footnotes != 0 { + + if extensions&EXTENSION_AUTOLINK != 0 { + p.inlineCallback[':'] = autoLink + } + + if extensions&EXTENSION_FOOTNOTES != 0 { p.notes = make([]*reference, 0) + p.notesRecord = make(map[string]struct{}) } - return &p + + first := firstPass(p, input) + second := secondPass(p, first) + return second } -// Option customizes the Markdown processor's default behavior. -type Option func(*Markdown) +// first pass: +// - normalize newlines +// - extract references (outside of fenced code blocks) +// - expand tabs (outside of fenced code blocks) +// - copy everything else +func firstPass(p *parser, input []byte) []byte { + var out bytes.Buffer + tabSize := TAB_SIZE_DEFAULT + if p.flags&EXTENSION_TAB_SIZE_EIGHT != 0 { + tabSize = TAB_SIZE_EIGHT + } + beg := 0 + lastFencedCodeBlockEnd := 0 + for beg < len(input) { + // Find end of this line, then process the line. + end := beg + for end < len(input) && input[end] != '\n' && input[end] != '\r' { + end++ + } -// WithRenderer allows you to override the default renderer. -func WithRenderer(r Renderer) Option { - return func(p *Markdown) { - p.renderer = r - } -} + if p.flags&EXTENSION_FENCED_CODE != 0 { + // track fenced code block boundaries to suppress tab expansion + // and reference extraction inside them: + if beg >= lastFencedCodeBlockEnd { + if i := p.fencedCodeBlock(&out, input[beg:], false); i > 0 { + lastFencedCodeBlockEnd = beg + i + } + } + } -// WithExtensions allows you to pick some of the many extensions provided by -// Blackfriday. You can bitwise OR them. -func WithExtensions(e Extensions) Option { - return func(p *Markdown) { - p.extensions = e - } -} + // add the line body if present + if end > beg { + if end < lastFencedCodeBlockEnd { // Do not expand tabs while inside fenced code blocks. + out.Write(input[beg:end]) + } else if refEnd := isReference(p, input[beg:], tabSize); refEnd > 0 { + beg += refEnd + continue + } else { + expandTabs(&out, input[beg:end], tabSize) + } + } -// WithNoExtensions turns off all extensions and custom behavior. -func WithNoExtensions() Option { - return func(p *Markdown) { - p.extensions = NoExtensions - p.renderer = NewHTMLRenderer(HTMLRendererParameters{ - Flags: HTMLFlagsNone, - }) + if end < len(input) && input[end] == '\r' { + end++ + } + if end < len(input) && input[end] == '\n' { + end++ + } + out.WriteByte('\n') + + beg = end } -} -// WithRefOverride sets an optional function callback that is called every -// time a reference is resolved. -// -// In Markdown, the link reference syntax can be made to resolve a link to -// a reference instead of an inline URL, in one of the following ways: -// -// * [link text][refid] -// * [refid][] -// -// Usually, the refid is defined at the bottom of the Markdown document. If -// this override function is provided, the refid is passed to the override -// function first, before consulting the defined refids at the bottom. If -// the override function indicates an override did not occur, the refids at -// the bottom will be used to fill in the link details. -func WithRefOverride(o ReferenceOverrideFunc) Option { - return func(p *Markdown) { - p.referenceOverride = o + // empty input? + if out.Len() == 0 { + out.WriteByte('\n') } -} -// Run is the main entry point to Blackfriday. It parses and renders a -// block of markdown-encoded text. -// -// The simplest invocation of Run takes one argument, input: -// output := Run(input) -// This will parse the input with CommonExtensions enabled and render it with -// the default HTMLRenderer (with CommonHTMLFlags). -// -// Variadic arguments opts can customize the default behavior. Since Markdown -// type does not contain exported fields, you can not use it directly. Instead, -// use the With* functions. For example, this will call the most basic -// functionality, with no extensions: -// output := Run(input, WithNoExtensions()) -// -// You can use any number of With* arguments, even contradicting ones. They -// will be applied in order of appearance and the latter will override the -// former: -// output := Run(input, WithNoExtensions(), WithExtensions(exts), -// WithRenderer(yourRenderer)) -func Run(input []byte, opts ...Option) []byte { - r := NewHTMLRenderer(HTMLRendererParameters{ - Flags: CommonHTMLFlags, - }) - optList := []Option{WithRenderer(r), WithExtensions(CommonExtensions)} - optList = append(optList, opts...) - parser := New(optList...) - ast := parser.Parse(input) - var buf bytes.Buffer - parser.renderer.RenderHeader(&buf, ast) - ast.Walk(func(node *Node, entering bool) WalkStatus { - return parser.renderer.RenderNode(&buf, node, entering) - }) - parser.renderer.RenderFooter(&buf, ast) - return buf.Bytes() + return out.Bytes() } -// Parse is an entry point to the parsing part of Blackfriday. It takes an -// input markdown document and produces a syntax tree for its contents. This -// tree can then be rendered with a default or custom renderer, or -// analyzed/transformed by the caller to whatever non-standard needs they have. -// The return value is the root node of the syntax tree. -func (p *Markdown) Parse(input []byte) *Node { - p.block(input) - // Walk the tree and finish up some of unfinished blocks - for p.tip != nil { - p.finalize(p.tip) - } - // Walk the tree again and process inline markdown in each block - p.doc.Walk(func(node *Node, entering bool) WalkStatus { - if node.Type == Paragraph || node.Type == Heading || node.Type == TableCell { - p.inline(node, node.content) - node.content = nil - } - return GoToNext - }) - p.parseRefsToAST() - return p.doc -} +// second pass: actual rendering +func secondPass(p *parser, input []byte) []byte { + var output bytes.Buffer + + p.r.DocumentHeader(&output) + p.block(&output, input) + + if p.flags&EXTENSION_FOOTNOTES != 0 && len(p.notes) > 0 { + p.r.Footnotes(&output, func() bool { + flags := LIST_ITEM_BEGINNING_OF_LIST + for i := 0; i < len(p.notes); i += 1 { + ref := p.notes[i] + var buf bytes.Buffer + if ref.hasBlock { + flags |= LIST_ITEM_CONTAINS_BLOCK + p.block(&buf, ref.title) + } else { + p.inline(&buf, ref.title) + } + p.r.FootnoteItem(&output, ref.link, buf.Bytes(), flags) + flags &^= LIST_ITEM_BEGINNING_OF_LIST | LIST_ITEM_CONTAINS_BLOCK + } -func (p *Markdown) parseRefsToAST() { - if p.extensions&Footnotes == 0 || len(p.notes) == 0 { - return + return true + }) } - p.tip = p.doc - block := p.addBlock(List, nil) - block.IsFootnotesList = true - block.ListFlags = ListTypeOrdered - flags := ListItemBeginningOfList - // Note: this loop is intentionally explicit, not range-form. This is - // because the body of the loop will append nested footnotes to p.notes and - // we need to process those late additions. Range form would only walk over - // the fixed initial set. - for i := 0; i < len(p.notes); i++ { - ref := p.notes[i] - p.addExistingChild(ref.footnote, 0) - block := ref.footnote - block.ListFlags = flags | ListTypeOrdered - block.RefLink = ref.link - if ref.hasBlock { - flags |= ListItemContainsBlock - p.block(ref.title) - } else { - p.inline(block, ref.title) - } - flags &^= ListItemBeginningOfList | ListItemContainsBlock - } - above := block.Parent - finalizeList(block) - p.tip = above - block.Walk(func(node *Node, entering bool) WalkStatus { - if node.Type == Paragraph || node.Type == Heading { - p.inline(node, node.content) - node.content = nil - } - return GoToNext - }) + + p.r.DocumentFooter(&output) + + if p.nesting != 0 { + panic("Nesting level did not end at zero") + } + + return output.Bytes() } // @@ -486,56 +516,18 @@ func (p *Markdown) parseRefsToAST() { // // are not yet supported. -// reference holds all information necessary for a reference-style links or -// footnotes. -// -// Consider this markdown with reference-style links: -// -// [link][ref] -// -// [ref]: /url/ "tooltip title" -// -// It will be ultimately converted to this HTML: -// -// <p><a href=\"/url/\" title=\"title\">link</a></p> -// -// And a reference structure will be populated as follows: -// -// p.refs["ref"] = &reference{ -// link: "/url/", -// title: "tooltip title", -// } -// -// Alternatively, reference can contain information about a footnote. Consider -// this markdown: -// -// Text needing a footnote.[^a] -// -// [^a]: This is the note -// -// A reference structure will be populated as follows: -// -// p.refs["a"] = &reference{ -// link: "a", -// title: "This is the note", -// noteID: <some positive int>, -// } -// -// TODO: As you can see, it begs for splitting into two dedicated structures -// for refs and for footnotes. +// References are parsed and stored in this struct. type reference struct { link []byte title []byte - noteID int // 0 if not a footnote ref + noteId int // 0 if not a footnote ref hasBlock bool - footnote *Node // a link to the Item node within a list of footnotes - - text []byte // only gets populated by refOverride feature with Reference.Text + text []byte } func (r *reference) String() string { - return fmt.Sprintf("{link: %q, title: %q, text: %q, noteID: %d, hasBlock: %v}", - r.link, r.title, r.text, r.noteID, r.hasBlock) + return fmt.Sprintf("{link: %q, title: %q, text: %q, noteId: %d, hasBlock: %v}", + r.link, r.title, r.text, r.noteId, r.hasBlock) } // Check whether or not data starts with a reference link. @@ -543,7 +535,7 @@ func (r *reference) String() string { // (in the render struct). // Returns the number of bytes to skip to move past it, // or zero if the first line is not a reference. -func isReference(p *Markdown, data []byte, tabSize int) int { +func isReference(p *parser, data []byte, tabSize int) int { // up to 3 optional leading spaces if len(data) < 4 { return 0 @@ -553,18 +545,18 @@ func isReference(p *Markdown, data []byte, tabSize int) int { i++ } - noteID := 0 + noteId := 0 // id part: anything but a newline between brackets if data[i] != '[' { return 0 } i++ - if p.extensions&Footnotes != 0 { + if p.flags&EXTENSION_FOOTNOTES != 0 { if i < len(data) && data[i] == '^' { // we can set it to anything here because the proper noteIds will // be assigned later during the second pass. It just has to be != 0 - noteID = 1 + noteId = 1 i++ } } @@ -576,11 +568,7 @@ func isReference(p *Markdown, data []byte, tabSize int) int { return 0 } idEnd := i - // footnotes can have empty ID, like this: [^], but a reference can not be - // empty like this: []. Break early if it's not a footnote and there's no ID - if noteID == 0 && idOffset == idEnd { - return 0 - } + // spacer: colon (space | tab)* newline? (space | tab)* i++ if i >= len(data) || data[i] != ':' { @@ -611,7 +599,7 @@ func isReference(p *Markdown, data []byte, tabSize int) int { hasBlock bool ) - if p.extensions&Footnotes != 0 && noteID != 0 { + if p.flags&EXTENSION_FOOTNOTES != 0 && noteId != 0 { linkOffset, linkEnd, raw, hasBlock = scanFootnote(p, data, i, tabSize) lineEnd = linkEnd } else { @@ -624,11 +612,11 @@ func isReference(p *Markdown, data []byte, tabSize int) int { // a valid ref has been found ref := &reference{ - noteID: noteID, + noteId: noteId, hasBlock: hasBlock, } - if noteID > 0 { + if noteId > 0 { // reusing the link field for the id since footnotes don't have links ref.link = data[idOffset:idEnd] // if footnote, it's not really a title, it's the contained text @@ -646,12 +634,15 @@ func isReference(p *Markdown, data []byte, tabSize int) int { return lineEnd } -func scanLinkRef(p *Markdown, data []byte, i int) (linkOffset, linkEnd, titleOffset, titleEnd, lineEnd int) { +func scanLinkRef(p *parser, data []byte, i int) (linkOffset, linkEnd, titleOffset, titleEnd, lineEnd int) { // link: whitespace-free sequence, optionally between angle brackets if data[i] == '<' { i++ } linkOffset = i + if i == len(data) { + return + } for i < len(data) && data[i] != ' ' && data[i] != '\t' && data[i] != '\n' && data[i] != '\r' { i++ } @@ -714,13 +705,13 @@ func scanLinkRef(p *Markdown, data []byte, i int) (linkOffset, linkEnd, titleOff return } -// The first bit of this logic is the same as Parser.listItem, but the rest +// The first bit of this logic is the same as (*parser).listItem, but the rest // is much simpler. This function simply finds the entire block and shifts it // over by one tab if it is indeed a block (just returns the line if it's not). // blockEnd is the end of the section in the input buffer, and contents is the // extracted text that was shifted over one tab. It will need to be rendered at // the end of the document. -func scanFootnote(p *Markdown, data []byte, i, indentSize int) (blockStart, blockEnd int, contents []byte, hasBlock bool) { +func scanFootnote(p *parser, data []byte, i, indentSize int) (blockStart, blockEnd int, contents []byte, hasBlock bool) { if i == 0 || len(data) == 0 { return } @@ -813,7 +804,17 @@ func ispunct(c byte) bool { // Test if a character is a whitespace character. func isspace(c byte) bool { - return c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\f' || c == '\v' + return ishorizontalspace(c) || isverticalspace(c) +} + +// Test if a character is a horizontal whitespace character. +func ishorizontalspace(c byte) bool { + return c == ' ' || c == '\t' +} + +// Test if a character is a vertical whitespace character. +func isverticalspace(c byte) bool { + return c == '\n' || c == '\r' || c == '\f' || c == '\v' } // Test if a character is letter. diff --git a/vendor/github.com/russross/blackfriday/node.go b/vendor/github.com/russross/blackfriday/node.go deleted file mode 100644 index 51b9e8c1..00000000 --- a/vendor/github.com/russross/blackfriday/node.go +++ /dev/null @@ -1,354 +0,0 @@ -package blackfriday - -import ( - "bytes" - "fmt" -) - -// NodeType specifies a type of a single node of a syntax tree. Usually one -// node (and its type) corresponds to a single markdown feature, e.g. emphasis -// or code block. -type NodeType int - -// Constants for identifying different types of nodes. See NodeType. -const ( - Document NodeType = iota - BlockQuote - List - Item - Paragraph - Heading - HorizontalRule - Emph - Strong - Del - Link - Image - Text - HTMLBlock - CodeBlock - Softbreak - Hardbreak - Code - HTMLSpan - Table - TableCell - TableHead - TableBody - TableRow -) - -var nodeTypeNames = []string{ - Document: "Document", - BlockQuote: "BlockQuote", - List: "List", - Item: "Item", - Paragraph: "Paragraph", - Heading: "Heading", - HorizontalRule: "HorizontalRule", - Emph: "Emph", - Strong: "Strong", - Del: "Del", - Link: "Link", - Image: "Image", - Text: "Text", - HTMLBlock: "HTMLBlock", - CodeBlock: "CodeBlock", - Softbreak: "Softbreak", - Hardbreak: "Hardbreak", - Code: "Code", - HTMLSpan: "HTMLSpan", - Table: "Table", - TableCell: "TableCell", - TableHead: "TableHead", - TableBody: "TableBody", - TableRow: "TableRow", -} - -func (t NodeType) String() string { - return nodeTypeNames[t] -} - -// ListData contains fields relevant to a List and Item node type. -type ListData struct { - ListFlags ListType - Tight bool // Skip <p>s around list item data if true - BulletChar byte // '*', '+' or '-' in bullet lists - Delimiter byte // '.' or ')' after the number in ordered lists - RefLink []byte // If not nil, turns this list item into a footnote item and triggers different rendering - IsFootnotesList bool // This is a list of footnotes -} - -// LinkData contains fields relevant to a Link node type. -type LinkData struct { - Destination []byte // Destination is what goes into a href - Title []byte // Title is the tooltip thing that goes in a title attribute - NoteID int // NoteID contains a serial number of a footnote, zero if it's not a footnote - Footnote *Node // If it's a footnote, this is a direct link to the footnote Node. Otherwise nil. -} - -// CodeBlockData contains fields relevant to a CodeBlock node type. -type CodeBlockData struct { - IsFenced bool // Specifies whether it's a fenced code block or an indented one - Info []byte // This holds the info string - FenceChar byte - FenceLength int - FenceOffset int -} - -// TableCellData contains fields relevant to a TableCell node type. -type TableCellData struct { - IsHeader bool // This tells if it's under the header row - Align CellAlignFlags // This holds the value for align attribute -} - -// HeadingData contains fields relevant to a Heading node type. -type HeadingData struct { - Level int // This holds the heading level number - HeadingID string // This might hold heading ID, if present - IsTitleblock bool // Specifies whether it's a title block -} - -// Node is a single element in the abstract syntax tree of the parsed document. -// It holds connections to the structurally neighboring nodes and, for certain -// types of nodes, additional information that might be needed when rendering. -type Node struct { - Type NodeType // Determines the type of the node - Parent *Node // Points to the parent - FirstChild *Node // Points to the first child, if any - LastChild *Node // Points to the last child, if any - Prev *Node // Previous sibling; nil if it's the first child - Next *Node // Next sibling; nil if it's the last child - - Literal []byte // Text contents of the leaf nodes - - HeadingData // Populated if Type is Heading - ListData // Populated if Type is List - CodeBlockData // Populated if Type is CodeBlock - LinkData // Populated if Type is Link - TableCellData // Populated if Type is TableCell - - content []byte // Markdown content of the block nodes - open bool // Specifies an open block node that has not been finished to process yet -} - -// NewNode allocates a node of a specified type. -func NewNode(typ NodeType) *Node { - return &Node{ - Type: typ, - open: true, - } -} - -func (n *Node) String() string { - ellipsis := "" - snippet := n.Literal - if len(snippet) > 16 { - snippet = snippet[:16] - ellipsis = "..." - } - return fmt.Sprintf("%s: '%s%s'", n.Type, snippet, ellipsis) -} - -// Unlink removes node 'n' from the tree. -// It panics if the node is nil. -func (n *Node) Unlink() { - if n.Prev != nil { - n.Prev.Next = n.Next - } else if n.Parent != nil { - n.Parent.FirstChild = n.Next - } - if n.Next != nil { - n.Next.Prev = n.Prev - } else if n.Parent != nil { - n.Parent.LastChild = n.Prev - } - n.Parent = nil - n.Next = nil - n.Prev = nil -} - -// AppendChild adds a node 'child' as a child of 'n'. -// It panics if either node is nil. -func (n *Node) AppendChild(child *Node) { - child.Unlink() - child.Parent = n - if n.LastChild != nil { - n.LastChild.Next = child - child.Prev = n.LastChild - n.LastChild = child - } else { - n.FirstChild = child - n.LastChild = child - } -} - -// InsertBefore inserts 'sibling' immediately before 'n'. -// It panics if either node is nil. -func (n *Node) InsertBefore(sibling *Node) { - sibling.Unlink() - sibling.Prev = n.Prev - if sibling.Prev != nil { - sibling.Prev.Next = sibling - } - sibling.Next = n - n.Prev = sibling - sibling.Parent = n.Parent - if sibling.Prev == nil { - sibling.Parent.FirstChild = sibling - } -} - -func (n *Node) isContainer() bool { - switch n.Type { - case Document: - fallthrough - case BlockQuote: - fallthrough - case List: - fallthrough - case Item: - fallthrough - case Paragraph: - fallthrough - case Heading: - fallthrough - case Emph: - fallthrough - case Strong: - fallthrough - case Del: - fallthrough - case Link: - fallthrough - case Image: - fallthrough - case Table: - fallthrough - case TableHead: - fallthrough - case TableBody: - fallthrough - case TableRow: - fallthrough - case TableCell: - return true - default: - return false - } -} - -func (n *Node) canContain(t NodeType) bool { - if n.Type == List { - return t == Item - } - if n.Type == Document || n.Type == BlockQuote || n.Type == Item { - return t != Item - } - if n.Type == Table { - return t == TableHead || t == TableBody - } - if n.Type == TableHead || n.Type == TableBody { - return t == TableRow - } - if n.Type == TableRow { - return t == TableCell - } - return false -} - -// WalkStatus allows NodeVisitor to have some control over the tree traversal. -// It is returned from NodeVisitor and different values allow Node.Walk to -// decide which node to go to next. -type WalkStatus int - -const ( - // GoToNext is the default traversal of every node. - GoToNext WalkStatus = iota - // SkipChildren tells walker to skip all children of current node. - SkipChildren - // Terminate tells walker to terminate the traversal. - Terminate -) - -// NodeVisitor is a callback to be called when traversing the syntax tree. -// Called twice for every node: once with entering=true when the branch is -// first visited, then with entering=false after all the children are done. -type NodeVisitor func(node *Node, entering bool) WalkStatus - -// Walk is a convenience method that instantiates a walker and starts a -// traversal of subtree rooted at n. -func (n *Node) Walk(visitor NodeVisitor) { - w := newNodeWalker(n) - for w.current != nil { - status := visitor(w.current, w.entering) - switch status { - case GoToNext: - w.next() - case SkipChildren: - w.entering = false - w.next() - case Terminate: - return - } - } -} - -type nodeWalker struct { - current *Node - root *Node - entering bool -} - -func newNodeWalker(root *Node) *nodeWalker { - return &nodeWalker{ - current: root, - root: root, - entering: true, - } -} - -func (nw *nodeWalker) next() { - if (!nw.current.isContainer() || !nw.entering) && nw.current == nw.root { - nw.current = nil - return - } - if nw.entering && nw.current.isContainer() { - if nw.current.FirstChild != nil { - nw.current = nw.current.FirstChild - nw.entering = true - } else { - nw.entering = false - } - } else if nw.current.Next == nil { - nw.current = nw.current.Parent - nw.entering = false - } else { - nw.current = nw.current.Next - nw.entering = true - } -} - -func dump(ast *Node) { - fmt.Println(dumpString(ast)) -} - -func dumpR(ast *Node, depth int) string { - if ast == nil { - return "" - } - indent := bytes.Repeat([]byte("\t"), depth) - content := ast.Literal - if content == nil { - content = ast.content - } - result := fmt.Sprintf("%s%s(%q)\n", indent, ast.Type, content) - for n := ast.FirstChild; n != nil; n = n.Next { - result += dumpR(n, depth+1) - } - return result -} - -func dumpString(ast *Node) string { - return dumpR(ast, 0) -} diff --git a/vendor/github.com/russross/blackfriday/smartypants.go b/vendor/github.com/russross/blackfriday/smartypants.go index 3a220e94..f25bd07d 100644 --- a/vendor/github.com/russross/blackfriday/smartypants.go +++ b/vendor/github.com/russross/blackfriday/smartypants.go @@ -17,14 +17,11 @@ package blackfriday import ( "bytes" - "io" ) -// SPRenderer is a struct containing state of a Smartypants renderer. -type SPRenderer struct { +type smartypantsData struct { inSingleQuote bool inDoubleQuote bool - callbacks [256]smartCallback } func wordBoundary(c byte) bool { @@ -121,7 +118,7 @@ func smartQuoteHelper(out *bytes.Buffer, previousChar byte, nextChar byte, quote return true } -func (r *SPRenderer) smartSingleQuote(out *bytes.Buffer, previousChar byte, text []byte) int { +func smartSingleQuote(out *bytes.Buffer, smrt *smartypantsData, previousChar byte, text []byte) int { if len(text) >= 2 { t1 := tolower(text[1]) @@ -130,7 +127,7 @@ func (r *SPRenderer) smartSingleQuote(out *bytes.Buffer, previousChar byte, text if len(text) >= 3 { nextChar = text[2] } - if smartQuoteHelper(out, previousChar, nextChar, 'd', &r.inDoubleQuote, false) { + if smartQuoteHelper(out, previousChar, nextChar, 'd', &smrt.inDoubleQuote, false) { return 1 } } @@ -155,7 +152,7 @@ func (r *SPRenderer) smartSingleQuote(out *bytes.Buffer, previousChar byte, text if len(text) > 1 { nextChar = text[1] } - if smartQuoteHelper(out, previousChar, nextChar, 's', &r.inSingleQuote, false) { + if smartQuoteHelper(out, previousChar, nextChar, 's', &smrt.inSingleQuote, false) { return 0 } @@ -163,7 +160,7 @@ func (r *SPRenderer) smartSingleQuote(out *bytes.Buffer, previousChar byte, text return 0 } -func (r *SPRenderer) smartParens(out *bytes.Buffer, previousChar byte, text []byte) int { +func smartParens(out *bytes.Buffer, smrt *smartypantsData, previousChar byte, text []byte) int { if len(text) >= 3 { t1 := tolower(text[1]) t2 := tolower(text[2]) @@ -188,7 +185,7 @@ func (r *SPRenderer) smartParens(out *bytes.Buffer, previousChar byte, text []by return 0 } -func (r *SPRenderer) smartDash(out *bytes.Buffer, previousChar byte, text []byte) int { +func smartDash(out *bytes.Buffer, smrt *smartypantsData, previousChar byte, text []byte) int { if len(text) >= 2 { if text[1] == '-' { out.WriteString("—") @@ -205,7 +202,7 @@ func (r *SPRenderer) smartDash(out *bytes.Buffer, previousChar byte, text []byte return 0 } -func (r *SPRenderer) smartDashLatex(out *bytes.Buffer, previousChar byte, text []byte) int { +func smartDashLatex(out *bytes.Buffer, smrt *smartypantsData, previousChar byte, text []byte) int { if len(text) >= 3 && text[1] == '-' && text[2] == '-' { out.WriteString("—") return 2 @@ -219,13 +216,13 @@ func (r *SPRenderer) smartDashLatex(out *bytes.Buffer, previousChar byte, text [ return 0 } -func (r *SPRenderer) smartAmpVariant(out *bytes.Buffer, previousChar byte, text []byte, quote byte, addNBSP bool) int { +func smartAmpVariant(out *bytes.Buffer, smrt *smartypantsData, previousChar byte, text []byte, quote byte, addNBSP bool) int { if bytes.HasPrefix(text, []byte(""")) { nextChar := byte(0) if len(text) >= 7 { nextChar = text[6] } - if smartQuoteHelper(out, previousChar, nextChar, quote, &r.inDoubleQuote, addNBSP) { + if smartQuoteHelper(out, previousChar, nextChar, quote, &smrt.inDoubleQuote, addNBSP) { return 5 } } @@ -238,18 +235,18 @@ func (r *SPRenderer) smartAmpVariant(out *bytes.Buffer, previousChar byte, text return 0 } -func (r *SPRenderer) smartAmp(angledQuotes, addNBSP bool) func(*bytes.Buffer, byte, []byte) int { +func smartAmp(angledQuotes, addNBSP bool) func(out *bytes.Buffer, smrt *smartypantsData, previousChar byte, text []byte) int { var quote byte = 'd' if angledQuotes { quote = 'a' } - return func(out *bytes.Buffer, previousChar byte, text []byte) int { - return r.smartAmpVariant(out, previousChar, text, quote, addNBSP) + return func(out *bytes.Buffer, smrt *smartypantsData, previousChar byte, text []byte) int { + return smartAmpVariant(out, smrt, previousChar, text, quote, addNBSP) } } -func (r *SPRenderer) smartPeriod(out *bytes.Buffer, previousChar byte, text []byte) int { +func smartPeriod(out *bytes.Buffer, smrt *smartypantsData, previousChar byte, text []byte) int { if len(text) >= 3 && text[1] == '.' && text[2] == '.' { out.WriteString("…") return 2 @@ -264,13 +261,13 @@ func (r *SPRenderer) smartPeriod(out *bytes.Buffer, previousChar byte, text []by return 0 } -func (r *SPRenderer) smartBacktick(out *bytes.Buffer, previousChar byte, text []byte) int { +func smartBacktick(out *bytes.Buffer, smrt *smartypantsData, previousChar byte, text []byte) int { if len(text) >= 2 && text[1] == '`' { nextChar := byte(0) if len(text) >= 3 { nextChar = text[2] } - if smartQuoteHelper(out, previousChar, nextChar, 'd', &r.inDoubleQuote, false) { + if smartQuoteHelper(out, previousChar, nextChar, 'd', &smrt.inDoubleQuote, false) { return 1 } } @@ -279,7 +276,7 @@ func (r *SPRenderer) smartBacktick(out *bytes.Buffer, previousChar byte, text [] return 0 } -func (r *SPRenderer) smartNumberGeneric(out *bytes.Buffer, previousChar byte, text []byte) int { +func smartNumberGeneric(out *bytes.Buffer, smrt *smartypantsData, previousChar byte, text []byte) int { if wordBoundary(previousChar) && previousChar != '/' && len(text) >= 3 { // is it of the form digits/digits(word boundary)?, i.e., \d+/\d+\b // note: check for regular slash (/) or fraction slash (⁄, 0x2044, or 0xe2 81 84 in utf-8) @@ -321,7 +318,7 @@ func (r *SPRenderer) smartNumberGeneric(out *bytes.Buffer, previousChar byte, te return 0 } -func (r *SPRenderer) smartNumber(out *bytes.Buffer, previousChar byte, text []byte) int { +func smartNumber(out *bytes.Buffer, smrt *smartypantsData, previousChar byte, text []byte) int { if wordBoundary(previousChar) && previousChar != '/' && len(text) >= 3 { if text[0] == '1' && text[1] == '/' && text[2] == '2' { if len(text) < 4 || wordBoundary(text[3]) && text[3] != '/' { @@ -349,27 +346,27 @@ func (r *SPRenderer) smartNumber(out *bytes.Buffer, previousChar byte, text []by return 0 } -func (r *SPRenderer) smartDoubleQuoteVariant(out *bytes.Buffer, previousChar byte, text []byte, quote byte) int { +func smartDoubleQuoteVariant(out *bytes.Buffer, smrt *smartypantsData, previousChar byte, text []byte, quote byte) int { nextChar := byte(0) if len(text) > 1 { nextChar = text[1] } - if !smartQuoteHelper(out, previousChar, nextChar, quote, &r.inDoubleQuote, false) { + if !smartQuoteHelper(out, previousChar, nextChar, quote, &smrt.inDoubleQuote, false) { out.WriteString(""") } return 0 } -func (r *SPRenderer) smartDoubleQuote(out *bytes.Buffer, previousChar byte, text []byte) int { - return r.smartDoubleQuoteVariant(out, previousChar, text, 'd') +func smartDoubleQuote(out *bytes.Buffer, smrt *smartypantsData, previousChar byte, text []byte) int { + return smartDoubleQuoteVariant(out, smrt, previousChar, text, 'd') } -func (r *SPRenderer) smartAngledDoubleQuote(out *bytes.Buffer, previousChar byte, text []byte) int { - return r.smartDoubleQuoteVariant(out, previousChar, text, 'a') +func smartAngledDoubleQuote(out *bytes.Buffer, smrt *smartypantsData, previousChar byte, text []byte) int { + return smartDoubleQuoteVariant(out, smrt, previousChar, text, 'a') } -func (r *SPRenderer) smartLeftAngle(out *bytes.Buffer, previousChar byte, text []byte) int { +func smartLeftAngle(out *bytes.Buffer, smrt *smartypantsData, previousChar byte, text []byte) int { i := 0 for i < len(text) && text[i] != '>' { @@ -380,78 +377,54 @@ func (r *SPRenderer) smartLeftAngle(out *bytes.Buffer, previousChar byte, text [ return i } -type smartCallback func(out *bytes.Buffer, previousChar byte, text []byte) int +type smartCallback func(out *bytes.Buffer, smrt *smartypantsData, previousChar byte, text []byte) int -// NewSmartypantsRenderer constructs a Smartypants renderer object. -func NewSmartypantsRenderer(flags HTMLFlags) *SPRenderer { - var ( - r SPRenderer +type smartypantsRenderer [256]smartCallback - smartAmpAngled = r.smartAmp(true, false) - smartAmpAngledNBSP = r.smartAmp(true, true) - smartAmpRegular = r.smartAmp(false, false) - smartAmpRegularNBSP = r.smartAmp(false, true) - - addNBSP = flags&SmartypantsQuotesNBSP != 0 - ) +var ( + smartAmpAngled = smartAmp(true, false) + smartAmpAngledNBSP = smartAmp(true, true) + smartAmpRegular = smartAmp(false, false) + smartAmpRegularNBSP = smartAmp(false, true) +) - if flags&SmartypantsAngledQuotes == 0 { - r.callbacks['"'] = r.smartDoubleQuote +func smartypants(flags int) *smartypantsRenderer { + r := new(smartypantsRenderer) + addNBSP := flags&HTML_SMARTYPANTS_QUOTES_NBSP != 0 + if flags&HTML_SMARTYPANTS_ANGLED_QUOTES == 0 { + r['"'] = smartDoubleQuote if !addNBSP { - r.callbacks['&'] = smartAmpRegular + r['&'] = smartAmpRegular } else { - r.callbacks['&'] = smartAmpRegularNBSP + r['&'] = smartAmpRegularNBSP } } else { - r.callbacks['"'] = r.smartAngledDoubleQuote + r['"'] = smartAngledDoubleQuote if !addNBSP { - r.callbacks['&'] = smartAmpAngled + r['&'] = smartAmpAngled } else { - r.callbacks['&'] = smartAmpAngledNBSP + r['&'] = smartAmpAngledNBSP } } - r.callbacks['\''] = r.smartSingleQuote - r.callbacks['('] = r.smartParens - if flags&SmartypantsDashes != 0 { - if flags&SmartypantsLatexDashes == 0 { - r.callbacks['-'] = r.smartDash + r['\''] = smartSingleQuote + r['('] = smartParens + if flags&HTML_SMARTYPANTS_DASHES != 0 { + if flags&HTML_SMARTYPANTS_LATEX_DASHES == 0 { + r['-'] = smartDash } else { - r.callbacks['-'] = r.smartDashLatex + r['-'] = smartDashLatex } } - r.callbacks['.'] = r.smartPeriod - if flags&SmartypantsFractions == 0 { - r.callbacks['1'] = r.smartNumber - r.callbacks['3'] = r.smartNumber + r['.'] = smartPeriod + if flags&HTML_SMARTYPANTS_FRACTIONS == 0 { + r['1'] = smartNumber + r['3'] = smartNumber } else { for ch := '1'; ch <= '9'; ch++ { - r.callbacks[ch] = r.smartNumberGeneric + r[ch] = smartNumberGeneric } } - r.callbacks['<'] = r.smartLeftAngle - r.callbacks['`'] = r.smartBacktick - return &r -} - -// Process is the entry point of the Smartypants renderer. -func (r *SPRenderer) Process(w io.Writer, text []byte) { - mark := 0 - for i := 0; i < len(text); i++ { - if action := r.callbacks[text[i]]; action != nil { - if i > mark { - w.Write(text[mark:i]) - } - previousChar := byte(0) - if i > 0 { - previousChar = text[i-1] - } - var tmp bytes.Buffer - i += action(&tmp, previousChar, text[i:]) - w.Write(tmp.Bytes()) - mark = i + 1 - } - } - if mark < len(text) { - w.Write(text[mark:]) - } + r['<'] = smartLeftAngle + r['`'] = smartBacktick + return r } |