├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── cmd └── mark │ └── main.go ├── grammar.go ├── lexer.go ├── lexer_test.go ├── mark.go ├── mark_test.go ├── node.go ├── parser.go ├── parser_test.go └── test ├── auto_links.html ├── auto_links.text ├── backslash_escapes.html ├── backslash_escapes.text ├── blockquote_list_item.html ├── blockquote_list_item.text ├── blockquotes_code_blocks.html ├── blockquotes_code_blocks.text ├── blockquotes_def.html ├── blockquotes_def.text ├── blockquotes_nested.html ├── blockquotes_nested.text ├── blockquotes_text.html ├── blockquotes_text.text ├── code_blocks.html ├── code_blocks.text ├── code_spans.html ├── code_spans.text ├── emphasis.html ├── emphasis.text ├── gfm_code_blocks.html ├── gfm_code_blocks.text ├── gfm_del.html ├── gfm_del.text ├── gfm_tables.html ├── gfm_tables.text ├── headers.html ├── headers.text ├── hr.html ├── hr.text ├── html_block.html ├── html_block.text ├── image_reference.html ├── image_reference.text ├── images.html ├── images.text ├── link_reference.html ├── link_reference.text ├── links_shortcut_references.html ├── links_shortcut_references.text ├── loose_list.html ├── loose_list.text ├── main.html ├── main.text ├── nested_emphasis.html ├── nested_emphasis.text ├── same_bullet.html ├── same_bullet.text ├── smartyfractions.html ├── smartyfractions.text ├── smartypants.html ├── smartypants.text ├── task_list.html ├── task_list.text ├── text_list.html ├── text_list.text ├── unordered_lists.html └── unordered_lists.text /.gitignore: -------------------------------------------------------------------------------- 1 | draft* 2 | coverage/ 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - tip 4 | before_install: 5 | - go get github.com/axw/gocov/gocov 6 | - go get github.com/mattn/goveralls 7 | - if ! go get code.google.com/p/go.tools/cmd/cover; then go get golang.org/x/tools/cmd/cover; fi 8 | script: 9 | - $HOME/gopath/bin/goveralls -service=travis-ci 10 | 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2015 Ariel Mashraki 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Archived. use https://github.com/russross/blackfriday instead 2 | 3 | # Mark [![Test coverage][coveralls-image]][coveralls-url] [![Build status][travis-image]][travis-url] [![Go doc][doc-image]][doc-url] [](https://raw.githubusercontent.com/a8m/mark/master/LICENSE) 4 | > A [markdown](http://daringfireball.net/projects/markdown/) processor written in Go. built for fun. 5 | 6 | Mark is a markdown processor that supports all the features of GFM, smartypants and smart-fractions rendering. 7 | It was built with a nice-ish concurrency model that fully inspired from [Rob Pike - Lexical Scanning talk](https://www.youtube.com/watch?v=HxaD_trXwRE) and [marked](https://github.com/chjj/marked) project. 8 | Please note that any contribution is welcomed and appreciated, so feel free to take some task [here](#todo). 9 | 10 | ## Table of contents: 11 | - [Get Started](#get-started) 12 | - [Examples](#examples) 13 | - [Documentation](#documentation) 14 | - [Render](#render) 15 | - [type Mark](#mark) 16 | - [New](#new) 17 | - [AddRenderFn](#markaddrenderfn) 18 | - [Render](#markrender) 19 | - [smartypants and smartfractions](##smartypants-and-smartfractions) 20 | - [Todo](#todo) 21 | 22 | ### Get Started 23 | #### Installation 24 | ```sh 25 | $ go get github.com/a8m/mark 26 | ``` 27 | #### Examples 28 | __Add to your project:__ 29 | ```go 30 | import ( 31 | "fmt" 32 | "github.com/a8m/mark" 33 | ) 34 | 35 | func main() { 36 | html := mark.Render("I am using __markdown__.") 37 | fmt.Println(html) 38 | //
I am using markdown.
39 | } 40 | ``` 41 | 42 | __or using as a command line tool:__ 43 | 44 | 1\. install: 45 | ```sh 46 | $ go get github.com/a8m/mark/cmd/mark 47 | ``` 48 | 49 | 2\. usage: 50 | ```sh 51 | $ echo 'hello __world__...' | mark -smartypants 52 | ``` 53 | or: 54 | ```sh 55 | $ mark -i hello.text -o hello.html 56 | ``` 57 | 58 | #### Documentation 59 | ##### Render 60 | Staic rendering function. 61 | ```go 62 | html := mark.Render("I am using __markdown__.") 63 | fmt.Println(html) 64 | //I am using markdown.
65 | ``` 66 | 67 | ##### Mark 68 | ##### New 69 | `New` get string as an input, and `mark.Options` as configuration and return a new `Mark`. 70 | ```go 71 | m := mark.New("hello world...", &mark.Options{ 72 | Smartypants: true, 73 | }) 74 | fmt.Println(m.Render()) 75 | //hello world…
76 | // Note: you can instantiate it like so: mark.New("...", nil) to get the default options. 77 | ``` 78 | 79 | ##### Mark.AddRenderFn 80 | `AddRenderFn` let you pass `NodeType`, and `RenderFn` function and override the default `Node` rendering. 81 | To get all Nodes type and their fields/methods, see the full documentation: [go-doc](http://godoc.org/github.com/a8m/mark) 82 | 83 | Example 1: 84 | ```go 85 | m := mark.New("hello", nil) 86 | m.AddRenderFn(mark.NodeParagraph, func(node mark.Node) (s string) { 87 | p, _ := node.(*mark.ParagraphNode) 88 | s += "" 89 | for _, n := range p.Nodes { 90 | s += n.Render() 91 | } 92 | s += "
" 93 | return 94 | }) 95 | fmt.Println(m.Render()) 96 | //hello
97 | ``` 98 | 99 | Example 2: 100 | ```go 101 | m := mark.New("# Hello world", &mark.Options{ 102 | Smartypants: true, 103 | Fractions: true, 104 | }) 105 | m.AddRenderFn(mark.NodeHeading, func(node mark.Node) string { 106 | h, _ := node.(*mark.HeadingNode) 107 | return fmt.Sprintf("hello
119 | ``` 120 | 121 | #### Smartypants and Smartfractions 122 | Mark also support [smartypants](http://daringfireball.net/projects/smartypants/) and smartfractions rendering 123 | ```go 124 | func main() { 125 | opts := mark.DefaultOptions() 126 | opts.Smartypants = true 127 | opts.Fractions = true 128 | m := mark.New("'hello', 1/2 beer please...", opts) 129 | fmt.Println(m.Render()) 130 | // ‘hello’, ½ beer please… 131 | } 132 | ``` 133 | 134 | ### Todo 135 | - Commonmark support v0.2 136 | - Expand documentation 137 | - Configuration options 138 | - gfm, table 139 | - heading(auto hashing) 140 | 141 | ### License 142 | MIT 143 | 144 | [travis-url]: https://travis-ci.org/a8m/mark 145 | [travis-image]: https://api.travis-ci.org/a8m/mark.svg 146 | [coveralls-image]: https://coveralls.io/repos/a8m/mark/badge.svg?branch=master&service=github 147 | [coveralls-url]: https://coveralls.io/r/a8m/mark 148 | [doc-image]: https://godoc.org/github.com/a8m/mark?status.svg 149 | [doc-url]: https://godoc.org/github.com/a8m/mark 150 | -------------------------------------------------------------------------------- /cmd/mark/main.go: -------------------------------------------------------------------------------- 1 | // mark command line tool. available at https://github.com/a8m/mark 2 | package main 3 | 4 | import ( 5 | "bufio" 6 | "flag" 7 | "fmt" 8 | "io" 9 | "os" 10 | 11 | "github.com/a8m/mark" 12 | ) 13 | 14 | var ( 15 | input = flag.String("i", "", "") 16 | output = flag.String("o", "", "") 17 | smarty = flag.Bool("smartypants", false, "") 18 | fractions = flag.Bool("fractions", false, "") 19 | ) 20 | 21 | var usage = `Usage: mark [options...] 22 | 23 | Options: 24 | -i Specify file input, otherwise use last argument as input file. 25 | If no input file is specified, read from stdin. 26 | -o Specify file output. If none is specified, write to stdout. 27 | 28 | -smartypants Use "smart" typograhic punctuation for things like 29 | quotes and dashes. 30 | -fractions Traslate fraction like to suitable HTML elements 31 | ` 32 | 33 | func main() { 34 | flag.Usage = func() { 35 | fmt.Fprint(os.Stderr, fmt.Sprintf(usage)) 36 | } 37 | flag.Parse() 38 | // read 39 | var reader *bufio.Reader 40 | if *input != "" { 41 | file, err := os.Open(*input) 42 | if err != nil { 43 | usageAndExit(fmt.Sprintf("Error to open file input: %s.", *input)) 44 | } 45 | defer file.Close() 46 | reader = bufio.NewReader(file) 47 | } else { 48 | stat, err := os.Stdin.Stat() 49 | if err != nil || (stat.Mode()&os.ModeCharDevice) != 0 { 50 | usageAndExit("") 51 | } 52 | reader = bufio.NewReader(os.Stdin) 53 | } 54 | // collect data 55 | var data string 56 | for { 57 | line, err := reader.ReadString('\n') 58 | if err != nil { 59 | if err == io.EOF { 60 | break 61 | } 62 | usageAndExit("failed to reading input.") 63 | } 64 | data += line 65 | } 66 | // write 67 | var ( 68 | err error 69 | file = os.Stdout 70 | ) 71 | if *output != "" { 72 | if file, err = os.Create(*output); err != nil { 73 | usageAndExit("error to create the wanted output file.") 74 | } 75 | } 76 | // mark rendering 77 | opts := mark.DefaultOptions() 78 | opts.Smartypants = *smarty 79 | opts.Fractions = *fractions 80 | m := mark.New(data, opts) 81 | if _, err := file.WriteString(m.Render()); err != nil { 82 | usageAndExit(fmt.Sprintf("error writing output to: %s.", file.Name())) 83 | } 84 | } 85 | 86 | func usageAndExit(msg string) { 87 | if msg != "" { 88 | fmt.Fprintf(os.Stderr, msg) 89 | fmt.Fprintf(os.Stderr, "\n\n") 90 | } 91 | flag.Usage() 92 | fmt.Fprintf(os.Stderr, "\n") 93 | os.Exit(1) 94 | } 95 | -------------------------------------------------------------------------------- /grammar.go: -------------------------------------------------------------------------------- 1 | package mark 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | ) 7 | 8 | // Block Grammar 9 | var ( 10 | reHr = regexp.MustCompile(`^(?:(?:\* *){3,}|(?:_ *){3,}|(?:- *){3,}) *(?:\n+|$)`) 11 | reHeading = regexp.MustCompile(`^ *(#{1,6})(?: +#*| +([^\n]*?)|)(?: +#*|) *(?:\n|$)`) 12 | reLHeading = regexp.MustCompile(`^([^\n]+?) *\n {0,3}(=|-){1,} *(?:\n+|$)`) 13 | reBlockQuote = regexp.MustCompile(`^ *>[^\n]*(\n[^\n]+)*\n*`) 14 | reDefLink = regexp.MustCompile(`(?s)^ *\[([^\]]+)\]: *\n? *([^\s>]+)>?(?: *\n? *["'(](.+?)['")])? *(?:\n+|$)`) 15 | reSpaceGen = func(i int) *regexp.Regexp { 16 | return regexp.MustCompile(fmt.Sprintf(`(?m)^ {1,%d}`, i)) 17 | } 18 | ) 19 | 20 | var reList = struct { 21 | item, marker, loose *regexp.Regexp 22 | scanLine, scanNewLine func(src string) string 23 | }{ 24 | regexp.MustCompile(`^( *)(?:[*+-]|\d{1,9}\.) (.*)(?:\n|)`), 25 | regexp.MustCompile(`^ *([*+-]|\d+\.) +`), 26 | regexp.MustCompile(`(?m)\n\n(.*)`), 27 | regexp.MustCompile(`^(.*)(?:\n|)`).FindString, 28 | regexp.MustCompile(`^\n{1,}`).FindString, 29 | } 30 | 31 | var reCodeBlock = struct { 32 | *regexp.Regexp 33 | trim func(src, repl string) string 34 | }{ 35 | regexp.MustCompile(`^( {4}[^\n]+(?: *\n)*)+`), 36 | regexp.MustCompile("(?m)^( {0,4})").ReplaceAllLiteralString, 37 | } 38 | 39 | var reGfmCode = struct { 40 | *regexp.Regexp 41 | endGen func(end string, i int) *regexp.Regexp 42 | }{ 43 | regexp.MustCompile("^( {0,3})([`~]{3,}) *(\\S*)?(?:.*)"), 44 | func(end string, i int) *regexp.Regexp { 45 | return regexp.MustCompile(fmt.Sprintf(`(?s)(.*?)(?:((?m)^ {0,3}%s{%d,} *$)|$)`, end, i)) 46 | }, 47 | } 48 | 49 | var reTable = struct { 50 | item, itemLp *regexp.Regexp 51 | split func(s string, n int) []string 52 | trim func(src, repl string) string 53 | }{ 54 | regexp.MustCompile(`^ *(\S.*\|.*)\n *([-:]+ *\|[-| :]*)\n((?:.*\|.*(?:\n|$))*)\n*`), 55 | regexp.MustCompile(`(^ *\|.+)\n( *\| *[-:]+[-| :]*)\n((?: *\|.*(?:\n|$))*)\n*`), 56 | regexp.MustCompile(` *\| *`).Split, 57 | regexp.MustCompile(`^ *\| *| *\| *$`).ReplaceAllString, 58 | } 59 | 60 | var reHTML = struct { 61 | CDATA_OPEN, CDATA_CLOSE string 62 | item, comment, tag, span *regexp.Regexp 63 | endTagGen func(tag string) *regexp.Regexp 64 | }{ 65 | `![CDATA[`, 66 | "?\\]\\]", 67 | regexp.MustCompile(`^<(\w+|!\[CDATA\[)(?:"[^"]*"|'[^']*'|[^'">])*?>`), 68 | regexp.MustCompile(`(?sm)`), 69 | regexp.MustCompile(`^|^<\/?\w+(?:"[^"]*"|'[^']*'|[^'">])*?>`), 70 | // TODO: Add all span-tags and move to config. 71 | regexp.MustCompile(`^(a|em|strong|small|s|q|data|time|code|sub|sup|i|b|u|span|br|del|img)$`), 72 | func(tag string) *regexp.Regexp { 73 | return regexp.MustCompile(fmt.Sprintf(`(?s)(.+?)<\/%s> *`, tag)) 74 | }, 75 | } 76 | 77 | // Inline Grammar 78 | var ( 79 | reBr = regexp.MustCompile(`^(?: {2,}|\\)\n`) 80 | reLinkText = `(?:\[[^\]]*\]|[^\[\]]|\])*` 81 | reLinkHref = `\s*(.*?)>?(?:\s+['"\(](.*?)['"\)])?\s*` 82 | reGfmLink = regexp.MustCompile(`^(https?:\/\/[^\s<]+[^<.,:;"')\]\s])`) 83 | reLink = regexp.MustCompile(fmt.Sprintf(`(?s)^!?\[(%s)\]\(%s\)`, reLinkText, reLinkHref)) 84 | reAutoLink = regexp.MustCompile(`^<([^ >]+(@|:\/)[^ >]+)>`) 85 | reRefLink = regexp.MustCompile(`^!?\[((?:\[[^\]]*\]|[^\[\]]|\])*)\](?:\s*\[([^\]]*)\])?`) 86 | reImage = regexp.MustCompile(fmt.Sprintf(`(?s)^!?\[(%s)\]\(%s\)`, reLinkText, reLinkHref)) 87 | reCode = regexp.MustCompile("(?s)^`{1,2}\\s*(.*?[^`])\\s*`{1,2}") 88 | reStrike = regexp.MustCompile(`(?s)^~{2}(.+?)~{2}`) 89 | reEmphasise = `(?s)^_{%[1]d}(\S.*?_*)_{%[1]d}|^\*{%[1]d}(\S.*?\**)\*{%[1]d}` 90 | reItalic = regexp.MustCompile(fmt.Sprintf(reEmphasise, 1)) 91 | reStrong = regexp.MustCompile(fmt.Sprintf(reEmphasise, 2)) 92 | ) 93 | -------------------------------------------------------------------------------- /lexer.go: -------------------------------------------------------------------------------- 1 | package mark 2 | 3 | import ( 4 | "regexp" 5 | "strings" 6 | "unicode/utf8" 7 | ) 8 | 9 | // type position 10 | type Pos int 11 | 12 | // itemType identifies the type of lex items. 13 | type itemType int 14 | 15 | // Item represent a token or text string returned from the scanner 16 | type item struct { 17 | typ itemType // The type of this item. 18 | pos Pos // The starting position, in bytes, of this item in the input string. 19 | val string // The value of this item. 20 | } 21 | 22 | const eof = -1 // Zero value so closed channel delivers EOF 23 | 24 | const ( 25 | itemError itemType = iota // Error occurred; value is text of error 26 | itemEOF 27 | itemNewLine 28 | itemHTML 29 | itemHeading 30 | itemLHeading 31 | itemBlockQuote 32 | itemList 33 | itemListItem 34 | itemLooseItem 35 | itemCodeBlock 36 | itemGfmCodeBlock 37 | itemHr 38 | itemTable 39 | itemLpTable 40 | itemTableRow 41 | itemTableCell 42 | itemStrong 43 | itemItalic 44 | itemStrike 45 | itemCode 46 | itemLink 47 | itemDefLink 48 | itemRefLink 49 | itemAutoLink 50 | itemGfmLink 51 | itemImage 52 | itemRefImage 53 | itemText 54 | itemBr 55 | itemPipe 56 | itemIndent 57 | ) 58 | 59 | // stateFn represents the state of the scanner as a function that returns the next state. 60 | type stateFn func(*lexer) stateFn 61 | 62 | // Lexer interface, used to composed it inside the parser 63 | type Lexer interface { 64 | nextItem() item 65 | } 66 | 67 | // lexer holds the state of the scanner. 68 | type lexer struct { 69 | input string // the string being scanned 70 | state stateFn // the next lexing function to enter 71 | pos Pos // current position in the input 72 | start Pos // start position of this item 73 | width Pos // width of last rune read from input 74 | lastPos Pos // position of most recent item returned by nextItem 75 | items chan item // channel of scanned items 76 | } 77 | 78 | // lex creates a new lexer for the input string. 79 | func lex(input string) *lexer { 80 | l := &lexer{ 81 | input: input, 82 | items: make(chan item), 83 | } 84 | go l.run() 85 | return l 86 | } 87 | 88 | // lexInline create a new lexer for one phase lexing(inline blocks). 89 | func lexInline(input string) *lexer { 90 | l := &lexer{ 91 | input: input, 92 | items: make(chan item), 93 | } 94 | go l.lexInline() 95 | return l 96 | } 97 | 98 | // run runs the state machine for the lexer. 99 | func (l *lexer) run() { 100 | for l.state = lexAny; l.state != nil; { 101 | l.state = l.state(l) 102 | } 103 | close(l.items) 104 | } 105 | 106 | // next return the next rune in the input 107 | func (l *lexer) next() rune { 108 | if int(l.pos) >= len(l.input) { 109 | l.width = 0 110 | return eof 111 | } 112 | r, w := utf8.DecodeRuneInString(l.input[l.pos:]) 113 | l.width = Pos(w) 114 | l.pos += l.width 115 | return r 116 | } 117 | 118 | // lexAny scanner is kind of forwarder, it get the current char in the text 119 | // and forward it to the appropriate scanner based on some conditions. 120 | func lexAny(l *lexer) stateFn { 121 | switch r := l.peek(); r { 122 | case '*', '-', '_': 123 | return lexHr 124 | case '+', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': 125 | return lexList 126 | case '<': 127 | return lexHTML 128 | case '>': 129 | return lexBlockQuote 130 | case '[': 131 | return lexDefLink 132 | case '#': 133 | return lexHeading 134 | case '`', '~': 135 | return lexGfmCode 136 | case ' ': 137 | if reCodeBlock.MatchString(l.input[l.pos:]) { 138 | return lexCode 139 | } else if reGfmCode.MatchString(l.input[l.pos:]) { 140 | return lexGfmCode 141 | } 142 | // Keep moving forward until we get all the indentation size 143 | for ; r == l.peek(); r = l.next() { 144 | } 145 | l.emit(itemIndent) 146 | return lexAny 147 | case '|': 148 | if m := reTable.itemLp.MatchString(l.input[l.pos:]); m { 149 | l.emit(itemLpTable) 150 | return lexTable 151 | } 152 | fallthrough 153 | default: 154 | if m := reTable.item.MatchString(l.input[l.pos:]); m { 155 | l.emit(itemTable) 156 | return lexTable 157 | } 158 | return lexText 159 | } 160 | } 161 | 162 | // lexHeading test if the current text position is an heading item. 163 | // is so, it will emit an item and return back to lenAny function 164 | // else, lex it as a simple text value 165 | func lexHeading(l *lexer) stateFn { 166 | if m := reHeading.FindString(l.input[l.pos:]); m != "" { 167 | l.pos += Pos(len(m)) 168 | l.emit(itemHeading) 169 | return lexAny 170 | } 171 | return lexText 172 | } 173 | 174 | // lexHr test if the current text position is an horizontal rules item. 175 | // is so, it will emit an horizontal rule item and return back to lenAny function 176 | // else, forward it to lexList function 177 | func lexHr(l *lexer) stateFn { 178 | if match := reHr.FindString(l.input[l.pos:]); match != "" { 179 | l.pos += Pos(len(match)) 180 | l.emit(itemHr) 181 | return lexAny 182 | } 183 | return lexList 184 | } 185 | 186 | // lexGfmCode test if the current text position is start of GFM code-block item. 187 | // if so, it will generate regexp based on the fence type[`~] and it length. 188 | // it scan until the end, and then emit the code-block item and return back to the 189 | // lenAny forwarder. 190 | // else, lex it as a simple inline text. 191 | func lexGfmCode(l *lexer) stateFn { 192 | if match := reGfmCode.FindStringSubmatch(l.input[l.pos:]); len(match) != 0 { 193 | l.pos += Pos(len(match[0])) 194 | fence := match[2] 195 | // Generate Regexp based on fence type[`~] and length 196 | reGfmEnd := reGfmCode.endGen(fence[0:1], len(fence)) 197 | infoContainer := reGfmEnd.FindStringSubmatch(l.input[l.pos:]) 198 | l.pos += Pos(len(infoContainer[0])) 199 | infoString := infoContainer[1] 200 | // Remove leading and trailing spaces 201 | if indent := len(match[1]); indent > 0 { 202 | reSpace := reSpaceGen(indent) 203 | infoString = reSpace.ReplaceAllString(infoString, "") 204 | } 205 | l.emit(itemGfmCodeBlock, match[0]+infoString) 206 | return lexAny 207 | } 208 | return lexText 209 | } 210 | 211 | // lexCode scans code block. 212 | func lexCode(l *lexer) stateFn { 213 | match := reCodeBlock.FindString(l.input[l.pos:]) 214 | l.pos += Pos(len(match)) 215 | l.emit(itemCodeBlock) 216 | return lexAny 217 | } 218 | 219 | // lexText scans until end-of-line(\n) 220 | func lexText(l *lexer) stateFn { 221 | // Drain text before emitting 222 | emit := func(item itemType, pos Pos) { 223 | if l.pos > l.start { 224 | l.emit(itemText) 225 | } 226 | l.pos += pos 227 | l.emit(item) 228 | } 229 | Loop: 230 | for { 231 | switch r := l.peek(); r { 232 | case eof: 233 | emit(itemEOF, Pos(0)) 234 | break Loop 235 | case '\n': 236 | // CM 4.4: An indented code block cannot interrupt a paragraph. 237 | if l.pos > l.start && strings.HasPrefix(l.input[l.pos+1:], " ") { 238 | l.next() 239 | continue 240 | } 241 | emit(itemNewLine, l.width) 242 | break Loop 243 | default: 244 | // Test for Setext-style headers 245 | if m := reLHeading.FindString(l.input[l.pos:]); m != "" { 246 | emit(itemLHeading, Pos(len(m))) 247 | break Loop 248 | } 249 | l.next() 250 | } 251 | } 252 | return lexAny 253 | } 254 | 255 | // backup steps back one rune. Can only be called once per call of next. 256 | func (l *lexer) backup() { 257 | l.pos -= l.width 258 | } 259 | 260 | // peek returns but does not consume the next rune in the input. 261 | func (l *lexer) peek() rune { 262 | r := l.next() 263 | l.backup() 264 | return r 265 | } 266 | 267 | // emit passes an item back to the client. 268 | func (l *lexer) emit(t itemType, s ...string) { 269 | if len(s) == 0 { 270 | s = append(s, l.input[l.start:l.pos]) 271 | } 272 | l.items <- item{t, l.start, s[0]} 273 | l.start = l.pos 274 | } 275 | 276 | // lexItem return the next item token, called by the parser. 277 | func (l *lexer) nextItem() item { 278 | item := <-l.items 279 | l.lastPos = l.pos 280 | return item 281 | } 282 | 283 | // One phase lexing(inline reason) 284 | func (l *lexer) lexInline() { 285 | escape := regexp.MustCompile("^\\\\([\\`*{}\\[\\]()#+\\-.!_>~|])") 286 | // Drain text before emitting 287 | emit := func(item itemType, pos int) { 288 | if l.pos > l.start { 289 | l.emit(itemText) 290 | } 291 | l.pos += Pos(pos) 292 | l.emit(item) 293 | } 294 | Loop: 295 | for { 296 | switch r := l.peek(); r { 297 | case eof: 298 | if l.pos > l.start { 299 | l.emit(itemText) 300 | } 301 | break Loop 302 | // backslash escaping 303 | case '\\': 304 | if m := escape.FindStringSubmatch(l.input[l.pos:]); len(m) != 0 { 305 | if l.pos > l.start { 306 | l.emit(itemText) 307 | } 308 | l.pos += Pos(len(m[0])) 309 | l.emit(itemText, m[1]) 310 | break 311 | } 312 | fallthrough 313 | case ' ': 314 | if m := reBr.FindString(l.input[l.pos:]); m != "" { 315 | // pos - length of new-line 316 | emit(itemBr, len(m)) 317 | break 318 | } 319 | l.next() 320 | case '_', '*', '~', '`': 321 | input := l.input[l.pos:] 322 | // Strong 323 | if m := reStrong.FindString(input); m != "" { 324 | emit(itemStrong, len(m)) 325 | break 326 | } 327 | // Italic 328 | if m := reItalic.FindString(input); m != "" { 329 | emit(itemItalic, len(m)) 330 | break 331 | } 332 | // Strike 333 | if m := reStrike.FindString(input); m != "" { 334 | emit(itemStrike, len(m)) 335 | break 336 | } 337 | // InlineCode 338 | if m := reCode.FindString(input); m != "" { 339 | emit(itemCode, len(m)) 340 | break 341 | } 342 | l.next() 343 | // itemLink, itemImage, itemRefLink, itemRefImage 344 | case '[', '!': 345 | input := l.input[l.pos:] 346 | if m := reLink.FindString(input); m != "" { 347 | pos := len(m) 348 | if r == '[' { 349 | emit(itemLink, pos) 350 | } else { 351 | emit(itemImage, pos) 352 | } 353 | break 354 | } 355 | if m := reRefLink.FindString(input); m != "" { 356 | pos := len(m) 357 | if r == '[' { 358 | emit(itemRefLink, pos) 359 | } else { 360 | emit(itemRefImage, pos) 361 | } 362 | break 363 | } 364 | l.next() 365 | // itemAutoLink, htmlBlock 366 | case '<': 367 | if m := reAutoLink.FindString(l.input[l.pos:]); m != "" { 368 | emit(itemAutoLink, len(m)) 369 | break 370 | } 371 | if match, res := l.matchHTML(l.input[l.pos:]); match { 372 | emit(itemHTML, len(res)) 373 | break 374 | } 375 | l.next() 376 | default: 377 | if m := reGfmLink.FindString(l.input[l.pos:]); m != "" { 378 | emit(itemGfmLink, len(m)) 379 | break 380 | } 381 | l.next() 382 | } 383 | } 384 | close(l.items) 385 | } 386 | 387 | // lexHTML. 388 | func lexHTML(l *lexer) stateFn { 389 | if match, res := l.matchHTML(l.input[l.pos:]); match { 390 | l.pos += Pos(len(res)) 391 | l.emit(itemHTML) 392 | return lexAny 393 | } 394 | return lexText 395 | } 396 | 397 | // Test if the given input is match the HTML pattern(blocks only) 398 | func (l *lexer) matchHTML(input string) (bool, string) { 399 | if m := reHTML.comment.FindString(input); m != "" { 400 | return true, m 401 | } 402 | if m := reHTML.item.FindStringSubmatch(input); len(m) != 0 { 403 | el, name := m[0], m[1] 404 | // if name is a span... is a text 405 | if reHTML.span.MatchString(name) { 406 | return false, "" 407 | } 408 | // if it's a self-closed html element, but not a itemAutoLink 409 | if strings.HasSuffix(el, "/>") && !reAutoLink.MatchString(el) { 410 | return true, el 411 | } 412 | if name == reHTML.CDATA_OPEN { 413 | name = reHTML.CDATA_CLOSE 414 | } 415 | reEndTag := reHTML.endTagGen(name) 416 | if m := reEndTag.FindString(input); m != "" { 417 | return true, m 418 | } 419 | } 420 | return false, "" 421 | } 422 | 423 | // lexDefLink scans link definition 424 | func lexDefLink(l *lexer) stateFn { 425 | if m := reDefLink.FindString(l.input[l.pos:]); m != "" { 426 | l.pos += Pos(len(m)) 427 | l.emit(itemDefLink) 428 | return lexAny 429 | } 430 | return lexText 431 | } 432 | 433 | // lexList scans ordered and unordered lists. 434 | func lexList(l *lexer) stateFn { 435 | match, items := l.matchList(l.input[l.pos:]) 436 | if !match { 437 | return lexText 438 | } 439 | var space int 440 | var typ itemType 441 | for i, item := range items { 442 | // Emit itemList on the first loop 443 | if i == 0 { 444 | l.emit(itemList, reList.marker.FindStringSubmatch(item)[1]) 445 | } 446 | // Initialize each loop 447 | typ = itemListItem 448 | space = len(item) 449 | l.pos += Pos(space) 450 | item = reList.marker.ReplaceAllString(item, "") 451 | // Indented 452 | if strings.Contains(item, "\n ") { 453 | space -= len(item) 454 | reSpace := reSpaceGen(space) 455 | item = reSpace.ReplaceAllString(item, "") 456 | } 457 | // If current is loose 458 | for _, l := range reList.loose.FindAllString(item, -1) { 459 | if len(strings.TrimSpace(l)) > 0 || i != len(items)-1 { 460 | typ = itemLooseItem 461 | break 462 | } 463 | } 464 | // or previous 465 | if typ != itemLooseItem && i > 0 && strings.HasSuffix(items[i-1], "\n\n") { 466 | typ = itemLooseItem 467 | } 468 | l.emit(typ, strings.TrimSpace(item)) 469 | } 470 | return lexAny 471 | } 472 | 473 | func (l *lexer) matchList(input string) (bool, []string) { 474 | var res []string 475 | reItem := reList.item 476 | if !reItem.MatchString(input) { 477 | return false, res 478 | } 479 | // First item 480 | m := reItem.FindStringSubmatch(input) 481 | item, depth := m[0], len(m[1]) 482 | input = input[len(item):] 483 | // Loop over the input 484 | for len(input) > 0 { 485 | // Count new-lines('\n') 486 | if m := reList.scanNewLine(input); m != "" { 487 | item += m 488 | input = input[len(m):] 489 | if len(m) >= 2 || !reItem.MatchString(input) && !strings.HasPrefix(input, " ") { 490 | break 491 | } 492 | } 493 | // DefLink or hr 494 | if reDefLink.MatchString(input) || reHr.MatchString(input) { 495 | break 496 | } 497 | // It's list in the same depth 498 | if m := reItem.FindStringSubmatch(input); len(m) > 0 && len(m[1]) == depth { 499 | if item != "" { 500 | res = append(res, item) 501 | } 502 | item = m[0] 503 | input = input[len(item):] 504 | } else { 505 | m := reList.scanLine(input) 506 | item += m 507 | input = input[len(m):] 508 | } 509 | } 510 | // Drain res 511 | if item != "" { 512 | res = append(res, item) 513 | } 514 | return true, res 515 | } 516 | 517 | // Test if the given input match blockquote 518 | func (l *lexer) matchBlockQuote(input string) (bool, string) { 519 | match := reBlockQuote.FindString(input) 520 | if match == "" { 521 | return false, match 522 | } 523 | lines := strings.Split(match, "\n") 524 | for i, line := range lines { 525 | // if line is a link-definition or horizontal role, we cut the match until this point 526 | if reDefLink.MatchString(line) || reHr.MatchString(line) { 527 | match = strings.Join(lines[0:i], "\n") 528 | break 529 | } 530 | } 531 | return true, match 532 | } 533 | 534 | // lexBlockQuote 535 | func lexBlockQuote(l *lexer) stateFn { 536 | if match, res := l.matchBlockQuote(l.input[l.pos:]); match { 537 | l.pos += Pos(len(res)) 538 | l.emit(itemBlockQuote) 539 | return lexAny 540 | } 541 | return lexText 542 | } 543 | 544 | // lexTable 545 | func lexTable(l *lexer) stateFn { 546 | re := reTable.item 547 | if l.peek() == '|' { 548 | re = reTable.itemLp 549 | } 550 | table := re.FindStringSubmatch(l.input[l.pos:]) 551 | l.pos += Pos(len(table[0])) 552 | l.start = l.pos 553 | // Ignore the first match, and flat all rows(by splitting \n) 554 | rows := append(table[1:3], strings.Split(table[3], "\n")...) 555 | for _, row := range rows { 556 | if row == "" { 557 | continue 558 | } 559 | l.emit(itemTableRow) 560 | rawCells := reTable.trim(row, "") 561 | cells := reTable.split(rawCells, -1) 562 | // Emit cells in the current row 563 | for _, cell := range cells { 564 | l.emit(itemTableCell, cell) 565 | } 566 | } 567 | return lexAny 568 | } 569 | -------------------------------------------------------------------------------- /lexer_test.go: -------------------------------------------------------------------------------- 1 | package mark 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | var itemName = map[itemType]string{ 9 | itemError: "Error", 10 | itemEOF: "EOF", 11 | itemNewLine: "NewLine", 12 | itemHTML: "HTML", 13 | itemHeading: "Heading", 14 | itemLHeading: "LHeading", 15 | itemBlockQuote: "BlockQuote", 16 | itemList: "List", 17 | itemListItem: "ListItem", 18 | itemLooseItem: "LooseItem", 19 | itemCodeBlock: "CodeBlock", 20 | itemGfmCodeBlock: "GfmCodeBlock", 21 | itemHr: "Hr", 22 | itemTable: "Table", 23 | itemLpTable: "LpTable", 24 | itemTableRow: "TableRow", 25 | itemTableCell: "TableCell", 26 | itemText: "Text", 27 | itemLink: "Link", 28 | itemDefLink: "DefLink", 29 | itemRefLink: "RefLink", 30 | itemAutoLink: "AutoLink", 31 | itemGfmLink: "GfmLink", 32 | itemStrong: "Strong", 33 | itemItalic: "Italic", 34 | itemStrike: "Strike", 35 | itemCode: "Code", 36 | itemImage: "Image", 37 | itemRefImage: "RefImage", 38 | itemBr: "Br", 39 | itemPipe: "Pipe", 40 | } 41 | 42 | func (i itemType) String() string { 43 | s := itemName[i] 44 | if s == "" { 45 | return fmt.Sprintf("item%d", int(i)) 46 | } 47 | return s 48 | } 49 | 50 | type lexTest struct { 51 | name string 52 | input string 53 | items []item 54 | } 55 | 56 | var ( 57 | tEOF = item{itemEOF, 0, ""} 58 | tNewLine = item{itemNewLine, 0, "\n"} 59 | tBr = item{itemBr, 0, " \n"} 60 | tPipe = item{itemPipe, 0, "|"} 61 | tTable = item{itemTable, 0, ""} 62 | tLpTable = item{itemLpTable, 0, ""} 63 | tRow = item{itemTableRow, 0, ""} 64 | ) 65 | 66 | var blockTests = []lexTest{ 67 | {"empty", "", []item{tEOF}}, 68 | {"heading", "# Hello", []item{ 69 | {itemHeading, 0, "# Hello"}, 70 | tEOF, 71 | }}, 72 | {"lheading", "Hello\n===", []item{ 73 | {itemLHeading, 0, "Hello\n==="}, 74 | tEOF, 75 | }}, 76 | {"blockqoute", "> foo bar", []item{ 77 | {itemBlockQuote, 0, "> foo bar"}, 78 | tEOF, 79 | }}, 80 | {"unordered list", "- foo\n- bar", []item{ 81 | {itemList, 0, "-"}, 82 | {itemListItem, 0, "foo"}, 83 | {itemListItem, 0, "bar"}, 84 | tEOF, 85 | }}, 86 | {"ordered list", "1. foo\n2. bar", []item{ 87 | {itemList, 0, "1."}, 88 | {itemListItem, 0, "foo"}, 89 | {itemListItem, 0, "bar"}, 90 | tEOF, 91 | }}, 92 | {"loose-items", "- foo\n\n- bar", []item{ 93 | {itemList, 0, "-"}, 94 | {itemLooseItem, 0, "foo"}, 95 | {itemLooseItem, 0, "bar"}, 96 | tEOF, 97 | }}, 98 | {"code-block", " foo\n bar", []item{ 99 | {itemCodeBlock, 0, " foo\n bar"}, 100 | tEOF, 101 | }}, 102 | {"gfm-code-block-1", "~~~js\nfoo\n~~~", []item{ 103 | {itemGfmCodeBlock, 0, "~~~js\nfoo\n"}, 104 | tEOF, 105 | }}, 106 | {"gfm-code-block-2", "```js\nfoo\n```", []item{ 107 | {itemGfmCodeBlock, 0, "```js\nfoo\n"}, 108 | tEOF, 109 | }}, 110 | {"hr1", "* * *\n***", []item{ 111 | {itemHr, 0, "* * *\n"}, 112 | {itemHr, 0, "***"}, 113 | tEOF, 114 | }}, 115 | {"hr2", "- - -\n---", []item{ 116 | {itemHr, 0, "- - -\n"}, 117 | {itemHr, 0, "---"}, 118 | tEOF, 119 | }}, 120 | {"hr3", "_ _ _\n___", []item{ 121 | {itemHr, 0, "_ _ _\n"}, 122 | {itemHr, 0, "___"}, 123 | tEOF, 124 | }}, 125 | {"table", "Id | Name\n---|-----\n1 | Ariel", []item{ 126 | tTable, 127 | tRow, 128 | {itemTableCell, 0, "Id"}, 129 | {itemTableCell, 0, "Name"}, 130 | tRow, 131 | {itemTableCell, 0, "---"}, 132 | {itemTableCell, 0, "-----"}, 133 | tRow, 134 | {itemTableCell, 0, "1"}, 135 | {itemTableCell, 0, "Ariel"}, 136 | tEOF, 137 | }}, 138 | {"lp-table", "|Id | Name|\n|---|-----|\n|1 | Ariel|", []item{ 139 | tLpTable, 140 | tRow, 141 | {itemTableCell, 0, "Id"}, 142 | {itemTableCell, 0, "Name"}, 143 | tRow, 144 | {itemTableCell, 0, "---"}, 145 | {itemTableCell, 0, "-----"}, 146 | tRow, 147 | {itemTableCell, 0, "1"}, 148 | {itemTableCell, 0, "Ariel"}, 149 | tEOF, 150 | }}, 151 | {"text-1", "hello\nworld", []item{ 152 | {itemText, 0, "hello"}, 153 | tNewLine, 154 | {itemText, 0, "world"}, 155 | tEOF, 156 | }}, 157 | {"text-2", "__hello__\n__world__", []item{ 158 | {itemText, 0, "__hello__"}, 159 | tNewLine, 160 | {itemText, 0, "__world__"}, 161 | tEOF, 162 | }}, 163 | {"text-3", "~**_hello world_**~", []item{ 164 | {itemText, 0, "~**_hello world_**~"}, 165 | tEOF, 166 | }}, 167 | {"text-4", " hello world", []item{ 168 | {itemIndent, 0, " "}, 169 | {itemText, 0, "hello world"}, 170 | tEOF, 171 | }}, 172 | {"deflink", "[1]: http://example.com", []item{ 173 | {itemDefLink, 0, "[1]: http://example.com"}, 174 | tEOF, 175 | }}, 176 | } 177 | 178 | var inlineTests = []lexTest{ 179 | {"text-1", "hello", []item{ 180 | {itemText, 0, "hello"}, 181 | }}, 182 | {"text-2", "hello\nworld", []item{ 183 | {itemText, 0, "hello\nworld"}, 184 | }}, 185 | {"br", "hello \nworld", []item{ 186 | {itemText, 0, "hello"}, 187 | tBr, 188 | {itemText, 0, "world"}, 189 | }}, 190 | {"strong-1", "**hello**", []item{ 191 | {itemStrong, 0, "**hello**"}, 192 | }}, 193 | {"strong-2", "__world__", []item{ 194 | {itemStrong, 0, "__world__"}, 195 | }}, 196 | {"italic-1", "*hello*", []item{ 197 | {itemItalic, 0, "*hello*"}, 198 | }}, 199 | {"italic-2", "_hello_", []item{ 200 | {itemItalic, 0, "_hello_"}, 201 | }}, 202 | {"strike", "~~hello~~", []item{ 203 | {itemStrike, 0, "~~hello~~"}, 204 | }}, 205 | {"code", "`hello`", []item{ 206 | {itemCode, 0, "`hello`"}, 207 | }}, 208 | {"link-1", "[hello](world)", []item{ 209 | {itemLink, 0, "[hello](world)"}, 210 | }}, 211 | {"link-2", "[hello](world 'title')", []item{ 212 | {itemLink, 0, "[hello](world 'title')"}, 213 | }}, 214 | {"autolink-1", "foobar
", 13 | " foo bar": "foo bar
", 14 | "|foo|bar": "|foo|bar
", 15 | "foo \nbar": "foo
bar
bar foo
", 17 | "**bar** foo __bar__": "bar foo bar
", 18 | "**bar**__baz__": "barbaz
", 19 | "**bar**foo__bar__": "barfoobar
", 20 | "_bar_baz": "barbaz
", 21 | "_foo_~~bar~~ baz": "foobar baz
baz baz
bool
and thats it.
foo
", 28 | "__foo _bar___": "foo bar
", 29 | "__*foo*__": "foo
", 30 | "_**mixim**_": "mixim
", 31 | "~~__*mixim*__~~": "mixim
mixim
1
2
3
1
\n2
", 36 | "1\n\n\n2": "1
\n2
", 37 | "1\n\n\n\n\n\n\n\n2": "1
\n2
", 38 | // Heading 39 | "# 1\n## 2": "p
\nLink: example
", 47 | "Link: [not really": "Link: [not really
", 48 | "http://localhost:3000": "", 49 | "Link: http://yeah.com": "Link: http://yeah.com
", 50 | "Link: http://l.co
", 52 | "Link:foo\nbar
",
55 | "\tfoo\nbar": "foo\n
\nbar
", 56 | // GfmCodeBlock 57 | "```js\nvar a;\n```": "\nvar a;\n
",
58 | "~~~\nvar b;~~let d = 1~~~~": "\nvar b;~~let d = 1~~~~
",
59 | "~~~js\n": "\n
",
60 | // Hr
61 | "foo\n****\nbar": "foo
\nbar
", 62 | "foo\n___": "foo
\nimg:
< hello
", 81 | "hello >": "hello >
", 82 | "foo & bar": "foo & bar
", 83 | "'foo'": "'foo'
", 84 | "\"foo\"": ""foo"
", 85 | "©": "©
", 86 | // Backslash escaping 87 | "\\**foo\\**": "*foo*
", 88 | "\\*foo\\*": "*foo*
", 89 | "\\_underscores\\_": "_underscores_
", 90 | "\\## header": "## header
", 91 | "header\n\\===": "header\n\\===
", 92 | } 93 | for input, expected := range cases { 94 | if actual := Render(input); actual != expected { 95 | t.Errorf("%s: got\n%+v\nexpected\n%+v", input, actual, expected) 96 | } 97 | } 98 | } 99 | 100 | func TestData(t *testing.T) { 101 | var testFiles []string 102 | files, err := ioutil.ReadDir("test") 103 | if err != nil { 104 | t.Error("Couldn't open 'test' directory") 105 | } 106 | for _, file := range files { 107 | if name := file.Name(); strings.HasSuffix(name, ".text") { 108 | testFiles = append(testFiles, "test/"+strings.TrimSuffix(name, ".text")) 109 | } 110 | } 111 | re := regexp.MustCompile(`\n`) 112 | for _, file := range testFiles { 113 | html, err := ioutil.ReadFile(file + ".html") 114 | if err != nil { 115 | t.Errorf("Error to read html file: %s", file) 116 | } 117 | text, err := ioutil.ReadFile(file + ".text") 118 | if err != nil { 119 | t.Errorf("Error to read text file: %s", file) 120 | } 121 | // Remove '\n' 122 | sHTML := re.ReplaceAllLiteralString(string(html), "") 123 | output := Render(string(text)) 124 | opts := DefaultOptions() 125 | if strings.Contains(file, "smartypants") { 126 | opts.Smartypants = true 127 | output = New(string(text), opts).Render() 128 | } 129 | if strings.Contains(file, "smartyfractions") { 130 | opts.Fractions = true 131 | output = New(string(text), opts).Render() 132 | } 133 | sText := re.ReplaceAllLiteralString(output, "") 134 | if sHTML != sText { 135 | t.Errorf("%s: got\n\t%+v\nexpected\n\t%+v", file, sText, sHTML) 136 | } 137 | } 138 | } 139 | 140 | // TODO: Add more tests for it. 141 | func TestRenderFn(t *testing.T) { 142 | m := New("hello world", nil) 143 | m.AddRenderFn(NodeParagraph, func(n Node) (s string) { 144 | if p, ok := n.(*ParagraphNode); ok { 145 | s += "" 146 | for _, pp := range p.Nodes { 147 | s += pp.Render() 148 | } 149 | s += "
" 150 | } 151 | return 152 | }) 153 | expected := "hello world
" 154 | if actual := m.Render(); actual != expected { 155 | t.Errorf("RenderFn: got\n\t%+v\nexpected\n\t%+v", actual, expected) 156 | } 157 | } 158 | 159 | type CommonMarkSpec struct { 160 | name string 161 | input string 162 | expected string 163 | } 164 | 165 | var CMCases = []CommonMarkSpec{ 166 | {"6", "- `one\n- two`", "+++
"}, 169 | {"9", "===", "===
"}, 170 | {"10", "--\n**\n__", "--**__
"}, 171 | {"11", " ***\n ***\n ***", "***
"},
173 | {"14", "_____________________________________", "-
"}, 179 | {"21", "- foo\n***\n- bar", "Foo
bar
"}, 181 | {"23", "Foo\n---\nbar", "bar
"}, 182 | {"24", "* Foo\n* * *\n* Bar", "####### foo
"}, 196 | {"28", "#5 bolt\n\n#foobar", "#5 bolt
\n#foobar
"}, 197 | {"29", "\\## foo", "## foo
"}, 198 | {"30", "# foo *bar* \\*baz\\*", "# foo
"},
206 | {"34", `
207 | foo
208 | # bar`, `
209 | foo 210 | # bar
`}, 211 | {"35", `## foo ## 212 | ### bar ###`, `Foo bar
235 |Bar foo
`}, 237 | {"43", ` 238 | ## 239 | # 240 | ### ###`, ` 241 | 242 | 243 | `}, 244 | {"44", ` 245 | Foo *bar* 246 | ========= 247 | 248 | Foo *bar* 249 | ---------`, ` 250 |Foo
273 | ---
274 |
275 | Foo
276 |
277 | Foo 284 | ---
`}, 285 | {"50", `Foo 286 | = = 287 | 288 | Foo 289 | --- -`, `Foo 290 | = =
291 |Foo
292 |`
\nof dashes"/>
"}, 298 | {"54", ` 299 | > Foo 300 | ---`, ` 301 |302 |304 |Foo
303 |
Baz
`}, 319 | {"58", "====", "====
"}, 320 | {"59", `--- 321 | ---`, "foo
329 |
330 | 335 |337 |foo
336 |
a simple
344 | indented code block
345 |
`},
346 | {"65", `
347 | - foo
348 |
349 | bar`, `
350 | foo
353 |bar
354 |foo
361 |<a/>
370 | *hi*
371 |
372 | - one
373 |
`},
374 | {"68", `
375 | chunk1
376 |
377 | chunk2
378 |
379 |
380 |
381 | chunk3`, `
382 | chunk1
383 |
384 | chunk2
385 |
386 |
387 |
388 | chunk3
389 |
`},
390 | {"69", `
391 | chunk1
392 |
393 | chunk2`, `
394 | chunk1
395 |
396 | chunk2
397 |
`},
398 | {"70", `
399 | Foo
400 | bar`, `
401 | Foo 402 | bar
`}, 403 | {"71", ` foo 404 | bar`, `foo
405 |
406 | bar
`}, 407 | {"72", `# Header 408 | foo 409 | Header 410 | ------ 411 | foo 412 | ----`, `foo
414 |
415 | foo
417 |
418 | foo
421 | bar
422 |
`},
423 | {"74", `
424 | foo
425 | `, `foo
426 |
`},
427 | {"75", " foo ", `foo
428 |
`},
429 | {"76", "```\n< \n>\n```", `<
430 | >
431 |
`},
432 | {"77", `~~~
433 | <
434 | >
435 | ~~~`, `<
436 | >
437 |
`},
438 | {"78", "```\naaa\n~~~\n```", `aaa
439 | ~~~
440 |
`},
441 | {"79", "~~~\naaa\n```\n~~~", "aaa\n```\n
"},
442 | {"80", "````\naaa\n```\n``````", "aaa\n```\n
"},
443 | {"81", `
444 | ~~~~
445 | aaa
446 | ~~~
447 | ~~~~`, `
448 | aaa
449 | ~~~
450 |
`},
451 | {"82", "```", "
"},
452 | {"83", "`````\n\n```\naaa", "\n```\naaa\n
"},
453 | {"84", "> ```\n> aaa\n\nbbb", `
454 | 455 |458 |457 |aaa 456 |
bbb
`}, 459 | {"85", "```\n\n \n```", "\n \n
"},
460 | {"86", "```\n```", `
`},
461 | {"87", " ```\n aaa\naaa\n```", `
462 | aaa
463 | aaa
464 |
`},
465 | {"88", " ```\naaa\n aaa\naaa\n ```", `
466 | aaa
467 | aaa
468 | aaa
469 |
`},
470 | {"89", " ```\n aaa\n aaa\n aaa\n ```", `
471 | aaa
472 | aaa
473 | aaa
474 |
`},
475 | {"90", " ```\n aaa\n ```", "```\naaa\n```\n
"},
476 | {"91", "```\naaa\n ```", `aaa
477 |
`},
478 | {"92", " ```\naaa\n ```", `aaa
479 |
`},
480 | {"93", "```\naaa\n ```", "aaa\n ```\n
"},
481 | {"95", `
482 | ~~~~~~
483 | aaa
484 | ~~~ ~~`, `
485 | aaa
486 | ~~~ ~~
487 |
`},
488 | {"96", "foo\n```\nbar\n```\nbaz", `foo
489 |bar
490 |
491 | baz
`}, 492 | {"97", `foo 493 | --- 494 | ~~~ 495 | bar 496 | ~~~ 497 | # baz`, `bar
499 |
500 | ``` aaa\n
"},
502 | {"103", `
503 | 506 | hi 507 | | 508 |
515 | hi 516 | | 517 |
okay.
`}, 520 | // Move out the id, beacuse the regexp below 521 | {"107", ` 522 | `, ` 525 | `}, 528 | {"108", ` 529 | `, ` 532 | `}, 535 | {"113", ``, ``}, 536 | {"114", ` 537 |538 | foo 539 | |
541 | foo 542 | |
foo
553 | import Text.HTML.TagSoup
554 |
555 | main :: IO ()
556 | main = print $ parseTags tags
557 |
`, `
558 |
559 | import Text.HTML.TagSoup
560 |
561 | main :: IO ()
562 | main = print $ parseTags tags
563 |
`},
564 | {"123", `
565 | `, `
570 | `},
575 | {"124", `
576 | `, `
582 | `},
588 | {"127", `
589 | - Foo
603 |Foo 611 | 612 | baz
`}, 613 | {"141", ` 614 |627 | Hi 628 | | 629 | 630 |
636 | Hi 637 | | 638 |
[foo]:
674 |[foo]
`}, 675 | {"153", ` 676 | [foo] 677 | 678 | [foo]: url`, ``}, 679 | {"154", ` 680 | [foo] 681 | 682 | [foo]: first 683 | [foo]: second`, ``}, 684 | {"155", ` 685 | [FOO]: /url 686 | 687 | [Foo]`, ``}, 688 | {"157", "[foo]: /url", ""}, 689 | {"158", ` 690 | [ 691 | foo 692 | ]: /url 693 | bar`, "bar
"}, 694 | {"159", `[foo]: /url "title" ok`, "[foo]: /url "title" ok
"}, 695 | {"160", ` 696 | [foo]: /url 697 | "title" ok`, ""title" ok
"}, 698 | {"161", ` 699 | [foo]: /url "title" 700 | 701 | [foo]`, ` 702 |[foo]: /url "title"
703 |
704 | [foo]
`}, 705 | {"162", "```\n[foo]: /url\n```\n\n[foo]", ` 706 |[foo]: /url
707 |
708 | [foo]
`}, 709 | {"166", ` 710 | [foo] 711 | 712 | > [foo]: /url`, ` 713 | 714 |715 |`}, 716 | {"167", ` 717 | aaa 718 | 719 | bbb`, ` 720 |
aaa
721 |bbb
`}, 722 | {"168", ` 723 | aaa 724 | bbb 725 | 726 | ccc 727 | ddd`, ` 728 |aaa 729 | bbb
730 |ccc 731 | ddd
`}, 732 | {"169", ` 733 | aaa 734 | 735 | 736 | bbb`, ` 737 |aaa
738 |bbb
`}, 739 | {"170", ` 740 | aaa 741 | bbb`, ` 742 |aaa 743 | bbb
`}, 744 | {"171", ` 745 | aaa 746 | bbb 747 | ccc`, ` 748 |aaa 749 | bbb 750 | ccc
`}, 751 | {"172", ` 752 | aaa 753 | bbb`, ` 754 |aaa 755 | bbb
`}, 756 | {"173", ` 757 | aaa 758 | bbb`, ` 759 |aaa
760 |
761 | bbb
`}, 762 | {"174", ` 763 | aaa 764 | bbb `, ` 765 |aaa
766 | bbb
aaa
777 |783 |`}, 787 | {"177", ` 788 | ># Foo 789 | >bar 790 | > baz`, ` 791 |Foo
784 |bar 785 | baz
786 |
792 |`}, 796 | {"178", ` 797 | > # Foo 798 | > bar 799 | > baz`, ` 800 |Foo
793 |bar 794 | baz
795 |
801 |`}, 805 | {"179", ` 806 | > # Foo 807 | > bar 808 | > baz`, ` 809 |Foo
802 |bar 803 | baz
804 |
> # Foo
810 | > bar
811 | > baz
812 |
`},
813 | {"180", `
814 | > # Foo
815 | > bar
816 | baz`, `
817 | 818 |`}, 822 | {"181", ` 823 | > bar 824 | baz 825 | > foo`, ` 826 |Foo
819 |bar 820 | baz
821 |
827 |`}, 831 | {"182", ` 832 | > foo 833 | ---`, ` 834 |bar 828 | baz 829 | foo
830 |
835 |837 |foo
836 |
841 |`}, 842 | {"187", ` 843 | > 844 | > 845 | > `, ` 846 |
847 |`}, 848 | {"188", ` 849 | > 850 | > foo 851 | > `, ` 852 |
853 |`}, 855 | {"189", ` 856 | > foo 857 | 858 | > bar`, ` 859 |foo
854 |
860 |862 |foo
861 |
863 |`}, 865 | {"190", ` 866 | > foo 867 | > bar`, ` 868 |bar
864 |
869 |`}, 872 | {"191", ` 873 | > foo 874 | > 875 | > bar`, ` 876 |foo 870 | bar
871 |
877 |`}, 880 | {"192", ` 881 | foo 882 | > bar`, ` 883 |foo
878 |bar
879 |
foo
884 |885 |`}, 887 | {"193", ` 888 | > aaa 889 | *** 890 | > bbb`, ` 891 |bar
886 |
892 |894 |aaa
893 |
896 |`}, 898 | {"194", ` 899 | > bar 900 | baz`, ` 901 |bbb
897 |
902 |`}, 905 | {"195", ` 906 | > bar 907 | 908 | baz`, ` 909 |bar 903 | baz
904 |
910 |912 |bar
911 |
baz
`}, 913 | {"197", ` 914 | > > > foo 915 | bar`, ` 916 |917 |`}, 924 | {"198", ` 925 | >>> foo 926 | > bar 927 | >>baz`, ` 928 |918 |923 |919 |922 |foo 920 | bar
921 |
929 |`}, 937 | {"199", ` 938 | > code 939 | 940 | > not code`, ` 941 |930 |936 |931 |935 |foo 932 | bar 933 | baz
934 |
942 |945 |944 |code 943 |
946 |`}, 948 | {"200", ` 949 | A paragraph 950 | with two lines. 951 | 952 | indented code 953 | 954 | > A block quote.`, ` 955 |not code
947 |
A paragraph 956 | with two lines.
957 |indented code
958 |
959 | 960 |`}, 962 | {"201", ` 963 | 1. A paragraph 964 | with two lines. 965 | 966 | indented code 967 | 968 | > A block quote.`, ` 969 |A block quote.
961 |
A paragraph 972 | with two lines.
973 |indented code
974 |
975 | 976 |978 |A block quote.
977 |
one
987 |two
988 |one
997 |two
998 |1005 |`}, 1014 | {"207", ` 1015 | >>- one 1016 | >> 1017 | > > two`, ` 1018 |1006 |1013 |1007 |
1012 |- 1008 |
1011 |one
1009 |two
1010 |
1019 |`}, 1026 | {"208", `-one 1027 | 1028 | 2.two`, ` 1029 |1020 |1025 |1021 |
1023 |- one
1022 |two
1024 |
-one
1030 |2.two
`}, 1031 | {"210", ` 1032 | 1. foo 1033 | 1034 | ~~~ 1035 | bar 1036 | ~~~ 1037 | 1038 | baz 1039 | 1040 | > bam`, ` 1041 |foo
1044 |bar
1045 |
1046 | baz
1047 |1048 |1050 |bam
1049 |
1234567890. not ok
`}, 1053 | {"215", `-1. not ok`, `-1. not ok
`}, 1054 | {"216", ` 1055 | - foo 1056 | 1057 | bar`, ` 1058 |foo
1061 |bar
1062 |
1063 | indented code
1072 |
1073 | paragraph
1074 |more code
1075 |
`},
1076 | {"221", `
1077 | foo
1078 |
1079 | bar`, `
1080 | foo
1081 |bar
`}, 1082 | {"223", ` 1083 | - foo 1084 | 1085 | bar`, ` 1086 |foo
1089 |bar
1090 |1. A paragraph
1109 | with two lines.
1110 |
1111 | indented code
1112 |
1113 | > A block quote.
1114 |
`},
1115 | {"234", `
1116 | 1. A paragraph
1117 | with two lines.`, `
1118 | 1126 |`}, 1135 | {"236", ` 1136 | > 1. > Blockquote 1137 | continued here.`, ` 1138 |1127 |
1134 |- 1128 |
1133 |1129 |1132 |Blockquote 1130 | continued here.
1131 |
1139 |`}, 1148 | {"237", ` 1149 | - foo 1150 | - bar 1151 | - baz`, ` 1152 |1140 |
1147 |- 1141 |
1146 |1142 |1145 |Blockquote 1143 | continued here.
1144 |
Foo
1189 |foo
1203 |bar
1206 |bar
1221 | bim
1243 |
`},
1244 | {"251", `
1245 | - foo
1246 | - bar
1247 |
1248 |
1249 | - baz
1250 | - bim`, `
1251 | foo
1271 |notcode
1272 |foo
1275 |code
1278 |
`},
1279 | {"261", `
1280 | * a
1281 | > b
1282 | >
1283 | * c`, `
1284 | 1287 |1289 |b
1288 |
foo
1310 |
1311 | bar
1312 |foo
1322 |baz
1326 |a
1339 |d
1346 |hi
lo`
foo
1358 | bar
\[\]
1360 |
`},
1361 | {"276", `
1362 | ~~~
1363 | \[\]
1364 | ~~~`, `
1365 | \[\]
1366 |
`},
1367 | {"294", "`foo`", `foo
foo\\
bar`
<a href="
">`
`foo
"}, 1371 | {"309", "*foo bar*", "foo bar
"}, 1372 | {"310", "a * foo bar*", "a * foo bar*
"}, 1373 | {"313", "foo*bar*", "foobar
"}, 1374 | {"314", "5*6*78", "5678
"}, 1375 | {"315", "_foo bar_", "foo bar
"}, 1376 | {"316", "_ foo bar_", "_ foo bar_
"}, 1377 | {"322", "foo-_(bar)_", "foo-(bar)
"}, 1378 | {"323", "_foo*", "_foo*
"}, 1379 | {"328", "*foo*bar", "foobar
"}, 1380 | {"335", "_(bar)_.", "(bar).
"}, 1381 | {"336", "**foo bar**", "foo bar
"}, 1382 | {"339", "foo**bar**", "foobar
"}, 1383 | {"340", "__foo bar__", "foo bar
"}, 1384 | {"348", "foo-__(bar)__", "foo-(bar)
"}, 1385 | {"352", "**Gomphocarpus (*Gomphocarpus physocarpus*, syn.*Asclepias physocarpa*)**", 1386 | "Gomphocarpus (Gomphocarpus physocarpus, syn.Asclepias physocarpa)
"}, 1387 | {"353", "**foo \"*bar*\" foo**", "foo "bar" foo
"}, 1388 | {"354", "**foo**bar", "foobar
"}, 1389 | {"361", "__(bar)__.", "(bar).
"}, 1390 | {"362", "*foo [bar](/url)*", "foo bar
"}, 1391 | {"363", "*foo\nbar*", "foo\nbar
"}, 1392 | {"375", "** is not an empty emphasis", "** is not an empty emphasis
"}, 1393 | {"377", "**foo [bar](/url)**", "foo bar
"}, 1394 | {"378", "**foo\nbar**", "foo\nbar
"}, 1395 | {"379", "__foo _bar_ baz__", "foo bar baz
"}, 1396 | {"383", "**foo *bar* baz**", "foo bar baz
"}, 1397 | {"385", "***foo* bar**", "foo bar
"}, 1398 | {"386", "**foo *bar***", "foo bar
"}, 1399 | {"389", "__ is not an empty emphasis", "__ is not an empty emphasis
"}, 1400 | {"392", "foo *\\**", "foo *
"}, 1401 | {"393", "foo *_*", "foo _
"}, 1402 | {"395", "foo **\\***", "foo *
"}, 1403 | {"396", "foo **_**", "foo _
"}, 1404 | {"404", "foo _\\__", "foo _
"}, 1405 | {"405", "foo _*_", "foo *
"}, 1406 | {"407", "foo __\\___", "foo _
"}, 1407 | {"408", "foo __*__", "foo *
"}, 1408 | {"415", "**foo**", "foo
"}, 1409 | {"416", "*_foo_*", "foo
"}, 1410 | {"417", "__foo__", "foo
"}, 1411 | {"418", "_*foo*_", "foo
"}, 1412 | {"419", "****foo****", "foo
"}, 1413 | {"420", "____foo____", "foo
"}, 1414 | {"422", "***foo***", "foo
"}, 1415 | {"424", "*foo _bar* baz_", "foo _bar baz_
"}, 1416 | {"438", "[link](/uri \"title\")", ""}, 1417 | {"439", "[link](/uri)", ""}, 1418 | {"440", "[link]()", ""}, 1419 | {"441", "[link](<>)", ""}, 1420 | {"451", ` 1421 | [link](#fragment) 1422 | 1423 | [link](http://example.com#fragment) 1424 | 1425 | [link](http://example.com?foo=bar&baz#fragment)`, ` 1426 | 1427 | 1428 | `}, 1429 | {"455", ` 1430 | [link](/url "title") 1431 | [link](/url 'title') 1432 | [link](/url (title))`, ` 1433 | `}, 1436 | {"458", `[link](/url 'title "and" title')`, ``}, 1437 | {"460", "[link] (/uri)", "[link] (/uri)
"}, 1438 | {"461", "[link [foo [bar]]](/uri)", ``}, 1439 | {"463", "[link [bar](/uri)", `[link bar
`}, 1440 | {"471", "[foo *bar](baz*)", ``}, 1441 | {"472", "*foo [bar* baz]", "foo [bar baz]
"}, 1442 | {"476", ` 1443 | [foo][bar] 1444 | 1445 | [bar]: /url "title"`, ``}, 1446 | {"477", ` 1447 | [link [foo [bar]]][ref] 1448 | 1449 | [ref]: /uri`, ``}, 1450 | {"484", ` 1451 | [foo *bar][ref] 1452 | 1453 | [ref]: /uri`, ``}, 1454 | {"488", ` 1455 | [foo][BaR] 1456 | 1457 | [bar]: /url "title"`, ``}, 1458 | {"489", ` 1459 | [Толпой][Толпой] is a Russian word. 1460 | 1461 | [ТОЛПОЙ]: /url`, `Толпой is a Russian word.
`}, 1462 | {"491", ` 1463 | [foo] [bar] 1464 | 1465 | [bar]: /url "title"`, ``}, 1466 | {"492", ` 1467 | [foo] 1468 | [bar] 1469 | 1470 | [bar]: /url "title"`, ``}, 1471 | {"493", ` 1472 | [foo]: /url1 1473 | 1474 | [foo]: /url2 1475 | 1476 | [bar][foo]`, ``}, 1477 | {"496", ` 1478 | [foo][ref[bar]] 1479 | 1480 | [ref[bar]]: /uri`, ` 1481 |[foo][ref[bar]]
1482 |[ref[bar]]: /uri
`}, 1483 | {"497", ` 1484 | [[[foo]]] 1485 | 1486 | [[[foo]]]: /url`, ` 1487 |[[[foo]]]
1488 |[[[foo]]]: /url
`}, 1489 | {"498", ` 1490 | [foo][ref\[] 1491 | 1492 | [ref\[]: /uri`, ``}, 1493 | {"499", ` 1494 | [] 1495 | 1496 | []: /uri`, ` 1497 |[]
1498 |[]: /uri
`}, 1499 | {"501", ` 1500 | [foo][] 1501 | 1502 | [foo]: /url "title"`, ``}, 1503 | {"502", ` 1504 | [*foo* bar][] 1505 | 1506 | [*foo* bar]: /url "title"`, ` 1507 | `}, 1508 | {"503", ` 1509 | [Foo][] 1510 | 1511 | [foo]: /url "title"`, ``}, 1512 | {"504", ` 1513 | [foo] 1514 | [] 1515 | 1516 | [foo]: /url "title"`, ``}, 1517 | {"505", ` 1518 | [foo] 1519 | 1520 | [foo]: /url "title"`, ``}, 1521 | {"506", ` 1522 | [*foo* bar] 1523 | 1524 | [*foo* bar]: /url "title"`, ` 1525 | `}, 1526 | {"508", ` 1527 | [[bar [foo] 1528 | 1529 | [foo]: /url`, `[[bar foo
`}, 1530 | {"509", ` 1531 | [Foo] 1532 | 1533 | [foo]: /url "title"`, ``}, 1534 | {"510", ` 1535 | [foo] bar 1536 | 1537 | [foo]: /url`, `foo bar
`}, 1538 | {"511", ` 1539 | \[foo] 1540 | 1541 | [foo]: /url "title"`, `[foo]
`}, 1542 | {"513", ` 1543 | [foo][bar] 1544 | 1545 | [foo]: /url1 1546 | [bar]: /url2`, ``}, 1547 | {"515", ` 1548 | [foo][bar][baz] 1549 | 1550 | [baz]: /url1 1551 | [bar]: /url2`, ``}, 1552 | {"517", ``, `My
![[foo]]
1588 |[[foo]]: /url "title"
`}, 1589 | {"536", ` 1590 | ![Foo] 1591 | 1592 | [foo]: /url "title"`, `![foo]
`}, 1597 | {"538", ` 1598 | \![foo] 1599 | 1600 | [foo]: /url "title"`, `!foo
`}, 1601 | {"539", `http://foo.bar.baz/test?q=hello&id=22&boolean
`}, 1604 | {"541", `<>
"}, 1607 | {"554", `foo@bar.example.com`, `foo@bar.example.com
`}, 1608 | {"555", "foo &<]]>
"}, 1621 | {"576", ` 1622 | foo 1623 | baz`, ` 1624 |foo
1625 | baz
foo
1630 | baz
foo
baz
foo
1638 | bar
foo
1643 | bar
foo\
`}, 1645 | {"588", `foo `, `foo
`}, 1646 | {"589", `### foo\`, `foo 1652 | baz
`}, 1653 | {"592", ` 1654 | foo 1655 | baz`, ` 1656 |foo 1657 | baz
`}, 1658 | {"594", `Foo χρῆν`, `Foo χρῆν
`}, 1659 | {"595", `Multiple spaces`, `Multiple spaces
`}, 1660 | } 1661 | 1662 | func TestCommonMark(t *testing.T) { 1663 | reID := regexp.MustCompile(` +?id=".*"`) 1664 | for _, c := range CMCases { 1665 | // Remove the auto-hashing until it'll be in the configuration 1666 | actual := reID.ReplaceAllString(Render(c.input), "") 1667 | if strings.Replace(actual, "\n", "", -1) != strings.Replace(c.expected, "\n", "", -1) { 1668 | t.Errorf("\ninput:%s\ngot:\n%s\nexpected:\n%s\nlink: http://spec.commonmark.org/0.21/#example-%s\n", 1669 | c.input, actual, c.expected, c.name) 1670 | } 1671 | } 1672 | } 1673 | -------------------------------------------------------------------------------- /node.go: -------------------------------------------------------------------------------- 1 | package mark 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | "strconv" 7 | "strings" 8 | ) 9 | 10 | // A Node is an element in the parse tree. 11 | type Node interface { 12 | Type() NodeType 13 | Render() string 14 | } 15 | 16 | // NodeType identifies the type of a parse tree node. 17 | type NodeType int 18 | 19 | // Type returns itself and provides an easy default implementation 20 | // for embedding in a Node. Embedded in all non-trivial Nodes. 21 | func (t NodeType) Type() NodeType { 22 | return t 23 | } 24 | 25 | // Render function, used for overriding default rendering. 26 | type RenderFn func(Node) string 27 | 28 | const ( 29 | NodeText NodeType = iota // A plain text 30 | NodeParagraph // A Paragraph 31 | NodeEmphasis // An emphasis(strong, em, ...) 32 | NodeHeading // A heading (h1, h2, ...) 33 | NodeBr // A link break 34 | NodeHr // A horizontal rule 35 | NodeImage // An image 36 | NodeRefImage // A image reference 37 | NodeList // A list of ListItems 38 | NodeListItem // A list item node 39 | NodeLink // A link(href) 40 | NodeRefLink // A link reference 41 | NodeDefLink // A link definition 42 | NodeTable // A table of NodeRows 43 | NodeRow // A row of NodeCells 44 | NodeCell // A table-cell(td) 45 | NodeCode // A code block(wrapped with pre) 46 | NodeBlockQuote // A blockquote 47 | NodeHTML // An inline HTML 48 | NodeCheckbox // A checkbox 49 | ) 50 | 51 | // ParagraphNode hold simple paragraph node contains text 52 | // that may be emphasis. 53 | type ParagraphNode struct { 54 | NodeType 55 | Pos 56 | Nodes []Node 57 | } 58 | 59 | // Render returns the html representation of ParagraphNode 60 | func (n *ParagraphNode) Render() (s string) { 61 | for _, node := range n.Nodes { 62 | s += node.Render() 63 | } 64 | return wrap("p", s) 65 | } 66 | 67 | func (p *parse) newParagraph(pos Pos) *ParagraphNode { 68 | return &ParagraphNode{NodeType: NodeParagraph, Pos: pos} 69 | } 70 | 71 | // TextNode holds plain text. 72 | type TextNode struct { 73 | NodeType 74 | Pos 75 | Text string 76 | } 77 | 78 | // Render returns the string representation of TexNode 79 | func (n *TextNode) Render() string { 80 | return n.Text 81 | } 82 | 83 | func (p *parse) newText(pos Pos, text string) *TextNode { 84 | return &TextNode{NodeType: NodeText, Pos: pos, Text: p.text(text)} 85 | } 86 | 87 | // HTMLNode holds the raw html source. 88 | type HTMLNode struct { 89 | NodeType 90 | Pos 91 | Src string 92 | } 93 | 94 | // Render returns the src of the HTMLNode 95 | func (n *HTMLNode) Render() string { 96 | return n.Src 97 | } 98 | 99 | func (p *parse) newHTML(pos Pos, src string) *HTMLNode { 100 | return &HTMLNode{NodeType: NodeHTML, Pos: pos, Src: src} 101 | } 102 | 103 | // HrNode represents horizontal rule 104 | type HrNode struct { 105 | NodeType 106 | Pos 107 | } 108 | 109 | // Render returns the html representation of hr. 110 | func (n *HrNode) Render() string { 111 | return "Link: http://example.com/.
2 | 3 |These should all get escaped:
2 | 3 |Backtick: `
4 | 5 |Asterisk: *
6 | 7 |Underscore: _
8 | 9 |Left brace: {
10 | 11 |Right brace: }
12 | 13 |Left bracket: [
14 | 15 |Right bracket: ]
16 | 17 |Left paren: (
18 | 19 |Right paren: )
20 | 21 |Hash: #
22 | 23 |Period: .
24 | 25 |Bang: !
26 | 27 |Plus: +
28 | 29 |Minus: -
30 | 31 |These should not, because they occur within a code block:
32 | 33 |Backslash: \\
34 |
35 | Backtick: \`
36 |
37 | Asterisk: \*
38 |
39 | Underscore: \_
40 |
41 | Left brace: \{
42 |
43 | Right brace: \}
44 |
45 | Left bracket: \[
46 |
47 | Right bracket: \]
48 |
49 | Left paren: \(
50 |
51 | Right paren: \)
52 |
53 | Greater-than: \>
54 |
55 | Hash: \#
56 |
57 | Period: \.
58 |
59 | Bang: \!
60 |
61 | Plus: \+
62 |
63 | Minus: \-
64 |
65 |
--------------------------------------------------------------------------------
/test/backslash_escapes.text:
--------------------------------------------------------------------------------
1 | These should all get escaped:
2 |
3 | Backtick: \`
4 |
5 | Asterisk: \*
6 |
7 | Underscore: \_
8 |
9 | Left brace: \{
10 |
11 | Right brace: \}
12 |
13 | Left bracket: \[
14 |
15 | Right bracket: \]
16 |
17 | Left paren: \(
18 |
19 | Right paren: \)
20 |
21 | Hash: \#
22 |
23 | Period: \.
24 |
25 | Bang: \!
26 |
27 | Plus: \+
28 |
29 | Minus: \-
30 |
31 | These should not, because they occur within a code block:
32 |
33 | Backslash: \\
34 |
35 | Backtick: \`
36 |
37 | Asterisk: \*
38 |
39 | Underscore: \_
40 |
41 | Left brace: \{
42 |
43 | Right brace: \}
44 |
45 | Left bracket: \[
46 |
47 | Right bracket: \]
48 |
49 | Left paren: \(
50 |
51 | Right paren: \)
52 |
53 | Greater-than: \>
54 |
55 | Hash: \#
56 |
57 | Period: \.
58 |
59 | Bang: \!
60 |
61 | Plus: \+
62 |
63 | Minus: \-
64 |
--------------------------------------------------------------------------------
/test/blockquote_list_item.html:
--------------------------------------------------------------------------------
1 | This fails in markdown.pl and upskirt:
2 | 3 |hello
6 |7 |9 |world
8 |
foo
12 |13 |15 |bar
14 |
2 |-------------------------------------------------------------------------------- /test/blockquotes_code_blocks.text: -------------------------------------------------------------------------------- 1 | > Example: 2 | > 3 | > sub status { 4 | > print working 5 | > } 6 | > 7 | > Or: 8 | > 9 | > sub status { 10 | > return working 11 | > } -------------------------------------------------------------------------------- /test/blockquotes_def.html: -------------------------------------------------------------------------------- 1 |Example:
3 |sub status { 4 | print working 5 | } 6 |
Or:
7 |sub status { 8 | return working 9 | } 10 |
2 |5 |foo 3 | bar
4 |
6 |-------------------------------------------------------------------------------- /test/blockquotes_def.text: -------------------------------------------------------------------------------- 1 | > foo 2 | > bar 3 | [1]: foo 4 | > bar -------------------------------------------------------------------------------- /test/blockquotes_nested.html: -------------------------------------------------------------------------------- 1 |bar
7 |
2 |-------------------------------------------------------------------------------- /test/blockquotes_nested.text: -------------------------------------------------------------------------------- 1 | > foo 2 | > 3 | > > bar 4 | > 5 | > foo -------------------------------------------------------------------------------- /test/blockquotes_text.html: -------------------------------------------------------------------------------- 1 |foo
3 |4 |6 |bar
5 |foo
7 |
Blockquotes Text
2 | 3 |4 |8 | 9 |Hello 5 | World. 6 | Ariel here
7 |
12 |16 | 17 |Hello 13 | World. 14 | Ariel here
15 |
20 |-------------------------------------------------------------------------------- /test/blockquotes_text.text: -------------------------------------------------------------------------------- 1 | Blockquotes Text 2 | 3 | > Hello 4 | World. 5 | Ariel here 6 | 7 | --- 8 | 9 | > Hello 10 | > World. 11 | > Ariel here 12 | 13 | ___ 14 | 15 | > **foo** bar _baz_ 16 | > hello 17 | > ## h2 -------------------------------------------------------------------------------- /test/code_blocks.html: -------------------------------------------------------------------------------- 1 |foo bar baz 21 | hello
22 |h2
23 |
code block on the first line
2 |
3 |
4 | Regular text.
5 | 6 |code block indented by spaces
7 |
8 |
9 | Regular text.
10 | 11 |the lines in this block
12 | all contain trailing spaces
13 |
14 |
15 | Regular Text.
16 | 17 |code block on the last line
18 |
--------------------------------------------------------------------------------
/test/code_blocks.text:
--------------------------------------------------------------------------------
1 | code block on the first line
2 |
3 | Regular text.
4 |
5 | code block indented by spaces
6 |
7 | Regular text.
8 |
9 | the lines in this block
10 | all contain trailing spaces
11 |
12 | Regular Text.
13 |
14 | code block on the last line
15 |
--------------------------------------------------------------------------------
/test/code_spans.html:
--------------------------------------------------------------------------------
1 | test
This is paragraph contians span block
.
hello world
2 | 3 |hello world
4 | 5 |hello world
6 | 7 |hello world
8 | 9 |hello world
10 | 11 |hello world
12 | 13 |hello world
14 | 15 |hello world
16 | 17 |hello* world
18 | -------------------------------------------------------------------------------- /test/emphasis.text: -------------------------------------------------------------------------------- 1 | hello **world** 2 | 3 | hello __world__ 4 | 5 | hello _world_ 6 | 7 | hello *world* 8 | 9 | ***hello*** world 10 | 11 | ___hello___ world 12 | 13 | __*hello*__ world 14 | 15 | **_hello_** world 16 | 17 | __hello*__ world 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /test/gfm_code_blocks.html: -------------------------------------------------------------------------------- 1 |var foo = 1
2 |
3 |
4 | echo hello
5 |
--------------------------------------------------------------------------------
/test/gfm_code_blocks.text:
--------------------------------------------------------------------------------
1 | ``` js
2 | var foo = 1
3 | ```
4 |
5 | ~~~bash
6 | echo hello
7 | ~~~
8 |
--------------------------------------------------------------------------------
/test/gfm_del.html:
--------------------------------------------------------------------------------
1 | hello world
foo ~bar~ baz
Heading 1 | 5 |Heading 2 | 6 |
---|---|
Cell 1 | 11 |Cell 2 | 12 |
Cell 3 | 15 |Cell 4 | 16 |
Header 1 | 24 |Header 2 | 25 |Header 3 | 26 |Header 4 | 27 |
---|---|---|---|
Cell 1 | 32 |Cell 2 | 33 |Cell 3 | 34 |Cell 4 | 35 |
Cell 5 | 38 |Cell 6 | 39 |Cell 7 | 40 |Cell 8 | 41 |
Header 1 | 49 |Header 2 | 50 |
---|---|
Cell 1 | 55 |Cell 2 | 56 |
Cell 3 | 59 |Cell 4 | 60 |
Header 1 | 68 |Header 2 | 69 |Header 3 | 70 |Header 4 | 71 |
---|---|---|---|
Cell 1 | 76 |Cell 2 | 77 |Cell 3 | 78 |Cell 4 | 79 |
Cell 5 | 82 |Cell 6 | 83 |Cell 7 | 84 |Cell 8 | 85 |
Id | 93 |Description | 94 |
---|---|
12313 | 99 |foo bar baz |
100 |
67522 | 103 |foo bar baz |
104 |
#foo
8 | 9 |#bar
10 | 11 |Dash
2 | 3 |---
16 |
--------------------------------------------------------------------------------
/test/hr.text:
--------------------------------------------------------------------------------
1 | Dash
2 |
3 | ---
4 |
5 | ___
6 |
7 | * * *
8 |
9 | ---
10 |
11 | ***
12 |
13 | ___
14 |
15 | ---
--------------------------------------------------------------------------------
/test/html_block.html:
--------------------------------------------------------------------------------
1 | foobar
2 |hello world
-------------------------------------------------------------------------------- /test/html_block.text: -------------------------------------------------------------------------------- 1 | foobar 2 | 3 |Image Reference
2 | 3 |Foo bar.
2 | 3 |Foo bar.
4 | 5 |Foo bar.
6 | 7 |With embedded [brackets].
8 | 9 |Indented once.
10 | 11 |Indented twice.
12 | 13 |Indented thrice.
14 | 15 |Indented [four][] times.
16 | 17 |[four]: /url
18 |
19 |
20 | this should work
23 | 24 |So should this.
25 | 26 |And this.
27 | 28 |And this.
29 | 30 |And this.
31 | 32 |But not [that] [].
33 | 34 |Nor [that][].
35 | 36 |Nor [that].
37 | 38 |In this case, this points to something else.
-------------------------------------------------------------------------------- /test/link_reference.text: -------------------------------------------------------------------------------- 1 | Foo [bar] [1]. 2 | 3 | Foo [bar][1]. 4 | 5 | Foo [bar] 6 | [1]. 7 | 8 | [1]: /url/ "Title" 9 | 10 | 11 | With [embedded [brackets]] [b]. 12 | 13 | 14 | Indented [once][]. 15 | 16 | Indented [twice][]. 17 | 18 | Indented [thrice][]. 19 | 20 | Indented [four][] times. 21 | 22 | [once]: /url 23 | 24 | [twice]: /url 25 | 26 | [thrice]: /url 27 | 28 | [four]: /url 29 | 30 | 31 | [b]: /url/ 32 | 33 | *** 34 | 35 | [this] [this] should work 36 | 37 | So should [this][this]. 38 | 39 | And [this] []. 40 | 41 | And [this][]. 42 | 43 | And [this]. 44 | 45 | But not [that] []. 46 | 47 | Nor [that][]. 48 | 49 | Nor [that]. 50 | 51 | In this case, [this](/somethingelse/) points to something else. 52 | 53 | [this]: foo 54 | -------------------------------------------------------------------------------- /test/links_shortcut_references.html: -------------------------------------------------------------------------------- 1 |This is the simple case.
2 | -------------------------------------------------------------------------------- /test/links_shortcut_references.text: -------------------------------------------------------------------------------- 1 | This is the [simple case]. 2 | 3 | [simple case]: /simple 4 | 5 | [this] [that] and the [other] 6 | 7 | [this]: /this 8 | [that]: /that 9 | [other]: /other -------------------------------------------------------------------------------- /test/loose_list.html: -------------------------------------------------------------------------------- 1 |Loose list:
2 | 3 |foo
5 |foo
7 |foo
9 |hello
14 |world
16 |buddy
18 |should work
23 |in nested
25 |haha, yap!
27 |Just a note, I've found that I can't test my markdown parser vs others. 3 | For example, both markdown.js and showdown code blocks in lists wrong. They're 4 | also completely inconsistent with regards to paragraphs in list items.
5 |A link. Not anymore.
6 | 8 | 9 |List Item 1
11 |List Item 2
13 |New List Item 2 18 | Another item
19 |Code goes here.
20 | Lots of it...
21 |
List Item 3 27 | The final item.
28 |List Item 4 30 | The real final item.
31 |Paragraph.
34 |35 |45 |36 |
44 |- bq Item 1
37 |- bq Item 2
43 |38 |
42 |- New bq Item 1
39 |- New bq Item 2 40 | Text here
41 |
47 |53 |Another blockquote! 48 | I really need to get 49 | more creative with 50 | mockup text.. 51 | markdown.js breaks here again
52 |
Hello world. Here is a link.
55 | And an image .
Code goes here.
57 | Lots of it...
58 |
--------------------------------------------------------------------------------
/test/main.text:
--------------------------------------------------------------------------------
1 | [test]: http://google.com/ "Google"
2 |
3 | # A heading
4 |
5 | Just a note, I've found that I can't test my markdown parser vs others.
6 | For example, both markdown.js and showdown code blocks in lists wrong. They're
7 | also completely [inconsistent][test] with regards to paragraphs in list items.
8 |
9 | A link. Not anymore.
10 |
11 |
13 |
14 | * List Item 1
15 |
16 | * List Item 2
17 | * New List Item 1
18 | Hi, this is a list item.
19 | * New List Item 2
20 | Another item
21 |
22 | Code goes here.
23 | Lots of it...
24 |
25 | * New List Item 3
26 | The last item
27 |
28 | * List Item 3
29 | The final item.
30 |
31 | * List Item 4
32 | The real final item.
33 |
34 | Paragraph.
35 |
36 | > * bq Item 1
37 | > * bq Item 2
38 | > * New bq Item 1
39 | > * New bq Item 2
40 | > Text here
41 |
42 | * * *
43 |
44 | > Another blockquote!
45 | > I really need to get
46 | > more creative with
47 | > mockup text..
48 | > markdown.js breaks here again
49 |
50 | Another Heading
51 | -------------
52 |
53 | Hello *world*. Here is a [link](//hello).
54 | And an image .
55 |
56 | Code goes here.
57 | Lots of it...
--------------------------------------------------------------------------------
/test/nested_emphasis.html:
--------------------------------------------------------------------------------
1 | hello world 2 | hello 3 | world 4 | Ariel 5 | here
-------------------------------------------------------------------------------- /test/nested_emphasis.text: -------------------------------------------------------------------------------- 1 | _hello **world**_ 2 | ___hello___ 3 | ***world*** 4 | *__Ariel__* 5 | _**here**_ -------------------------------------------------------------------------------- /test/same_bullet.html: -------------------------------------------------------------------------------- 1 |½, ¼ and ¾; ¼th and ¾ths
2 | 3 |1/2/2015, 1/4/2015, 3/4/2015; 2015/1/2, 2015/1/4, 2015/3/4
4 | 5 |½, ⅔, 81⁄100 and 1000000⁄1048576
-------------------------------------------------------------------------------- /test/smartyfractions.text: -------------------------------------------------------------------------------- 1 | 1/2, 1/4 and 3/4; 1/4th and 3/4ths 2 | 3 | 1/2/2015, 1/4/2015, 3/4/2015; 2015/1/2, 2015/1/4, 2015/3/4 4 | 5 | 1/2, 2/3, 81/100 and 1000000/1048576 -------------------------------------------------------------------------------- /test/smartypants.html: -------------------------------------------------------------------------------- 1 |Hello world ‘how’ “are” you – today…
2 | 3 |“It’s a more ‘challenging’ smartypants test…”
4 | 5 |‘And,’ as a bonus — “one 6 | multiline” test!
-------------------------------------------------------------------------------- /test/smartypants.text: -------------------------------------------------------------------------------- 1 | Hello world 'how' "are" you -- today... 2 | 3 | "It's a more 'challenging' smartypants test..." 4 | 5 | 'And,' as a bonus --- "one 6 | multiline" test! -------------------------------------------------------------------------------- /test/task_list.html: -------------------------------------------------------------------------------- 1 |foo
bar
Unordered lists:
2 | 3 |-paragraph
30 | 31 |+paragraph
32 | 33 |*paragraph
-------------------------------------------------------------------------------- /test/unordered_lists.text: -------------------------------------------------------------------------------- 1 | Unordered lists: 2 | - foo 3 | - foo 4 | - foo 5 | 6 | 7 | * bar 8 | * bar 9 | * bar 10 | 11 | 12 | + baz 13 | + baz 14 | + baz 15 | 16 | 17 | -paragraph 18 | 19 | +paragraph 20 | 21 | *paragraph --------------------------------------------------------------------------------