├── .github ├── ISSUE_TEMPLATE.md └── workflows │ └── test.yaml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── _benchmark ├── cmark │ ├── .gitignore │ ├── Makefile │ ├── _data.md │ ├── cmark_benchmark.c │ └── goldmark_benchmark.go └── go │ ├── _data.md │ └── benchmark_test.go ├── _test ├── extra.txt ├── options.txt └── spec.json ├── ast ├── ast.go ├── block.go └── inline.go ├── commonmark_test.go ├── extension ├── _test │ ├── definition_list.txt │ ├── footnote.txt │ ├── linkify.txt │ ├── strikethrough.txt │ ├── table.txt │ ├── tasklist.txt │ └── typographer.txt ├── ast │ ├── definition_list.go │ ├── footnote.go │ ├── strikethrough.go │ ├── table.go │ └── tasklist.go ├── definition_list.go ├── definition_list_test.go ├── footnote.go ├── footnote_test.go ├── gfm.go ├── linkify.go ├── linkify_test.go ├── strikethrough.go ├── strikethrough_test.go ├── table.go ├── table_test.go ├── tasklist.go ├── tasklist_test.go ├── typographer.go └── typographer_test.go ├── extra_test.go ├── fuzz ├── fuzz.go └── fuzz_test.go ├── go.mod ├── go.sum ├── markdown.go ├── options_test.go ├── parser ├── attribute.go ├── atx_heading.go ├── auto_link.go ├── blockquote.go ├── code_block.go ├── code_span.go ├── delimiter.go ├── emphasis.go ├── fcode_block.go ├── html_block.go ├── link.go ├── link_ref.go ├── list.go ├── list_item.go ├── paragraph.go ├── parser.go ├── raw_html.go ├── setext_headings.go └── thematic_break.go ├── renderer ├── blocks │ └── blocks.go ├── html │ └── html.go └── renderer.go ├── testutil ├── testutil.go └── testutil_test.go ├── text ├── reader.go └── segment.go └── util ├── html5entities.go ├── util.go ├── util_safe.go └── util_unsafe.go /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | - [ ] goldmark is fully compliant with the CommonMark. Before submitting issue, you **must** read [CommonMark spec](https://spec.commonmark.org/0.29/) and confirm your output is different from [CommonMark online demo](https://spec.commonmark.org/dingus/). 2 | - [ ] Before you make a feature request, **you should consider implement the new feature as an extension by yourself** . To keep goldmark itself simple, most new features should be implemented as an extension. 3 | 4 | Please answer the following before submitting your issue: 5 | 6 | 1. What version of goldmark are you using? : 7 | 2. What version of Go are you using? : 8 | 3. What operating system and processor architecture are you using? : 9 | 4. What did you do? : 10 | 5. What did you expect to see? : 11 | 6. What did you see instead? : 12 | 7. (Feature request only): Why you can not implement it as an extension?: 13 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | name: test 3 | jobs: 4 | test: 5 | strategy: 6 | fail-fast: false 7 | matrix: 8 | go-version: [1.12.x, 1.13.x] 9 | platform: [ubuntu-latest, macos-latest, windows-latest] 10 | runs-on: ${{ matrix.platform }} 11 | steps: 12 | - name: Install Go 13 | uses: actions/setup-go@v1 14 | with: 15 | go-version: ${{ matrix.go-version }} 16 | - name: Checkout code 17 | uses: actions/checkout@v1 18 | - name: Run tests 19 | run: go test -v ./... -covermode=count -coverprofile=coverage.out -coverpkg=./... 20 | - name: Send coverage 21 | if: "matrix.platform == 'ubuntu-latest'" 22 | env: 23 | COVERALLS_TOKEN: ${{ secrets.GITHUB_TOKEN }} 24 | run: | 25 | GO111MODULE=off go get github.com/mattn/goveralls 26 | $(go env GOPATH)/bin/goveralls -coverprofile=coverage.out -service=github 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, build with `go test -c` 9 | *.test 10 | *.pprof 11 | 12 | # Output of the go coverage tool, specifically when used with LiteIDE 13 | *.out 14 | 15 | .DS_Store 16 | fuzz/corpus 17 | fuzz/crashers 18 | fuzz/suppressions 19 | fuzz/fuzz-fuzz.zip 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Yusuke Inuzuka 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: test fuzz 2 | 3 | test: 4 | go test -coverprofile=profile.out -coverpkg=github.com/enkogu/goldmark,github.com/enkogu/goldmark/ast,github.com/enkogu/goldmark/extension,github.com/enkogu/goldmark/extension/ast,github.com/enkogu/goldmark/parser,github.com/enkogu/goldmark/renderer,github.com/enkogu/goldmark/renderer/html,github.com/enkogu/goldmark/text,github.com/enkogu/goldmark/util ./... 5 | 6 | cov: test 7 | go tool cover -html=profile.out 8 | 9 | fuzz: 10 | which go-fuzz > /dev/null 2>&1 || (GO111MODULE=off go get -u github.com/dvyukov/go-fuzz/go-fuzz github.com/dvyukov/go-fuzz/go-fuzz-build; GO111MODULE=off go get -d github.com/dvyukov/go-fuzz-corpus; true) 11 | rm -rf ./fuzz/corpus 12 | rm -rf ./fuzz/crashers 13 | rm -rf ./fuzz/suppressions 14 | rm -f ./fuzz/fuzz-fuzz.zip 15 | cd ./fuzz && go-fuzz-build 16 | cd ./fuzz && go-fuzz 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | goldmark 2 | ========================================== 3 | 4 | [![http://godoc.org/github.com/enkogu/goldmark](https://godoc.org/github.com/enkogu/goldmark?status.svg)](http://godoc.org/github.com/enkogu/goldmark) 5 | [![https://github.com/enkogu/goldmark/actions?query=workflow:test](https://github.com/enkogu/goldmark/workflows/test/badge.svg?branch=master&event=push)](https://github.com/enkogu/goldmark/actions?query=workflow:test) 6 | [![https://coveralls.io/github/yuin/goldmark](https://coveralls.io/repos/github/yuin/goldmark/badge.svg?branch=master)](https://coveralls.io/github/yuin/goldmark) 7 | [![https://goreportcard.com/report/github.com/enkogu/goldmark](https://goreportcard.com/badge/github.com/enkogu/goldmark)](https://goreportcard.com/report/github.com/enkogu/goldmark) 8 | 9 | > A Markdown parser written in Go. Easy to extend, standard compliant, well structured. 10 | 11 | goldmark is compliant with CommonMark 0.29. 12 | 13 | 14 | Motivation 15 | ---------------------- 16 | I need a Markdown parser for Go that meets following conditions: 17 | 18 | - Easy to extend. 19 | - Markdown is poor in document expressions compared with other light markup languages like reStructuredText. 20 | - We have extensions to the Markdown syntax, e.g. PHP Markdown Extra, GitHub Flavored Markdown. 21 | - Standard compliant. 22 | - Markdown has many dialects. 23 | - GitHub Flavored Markdown is widely used and it is based on CommonMark aside from whether CommonMark is good specification or not. 24 | - CommonMark is too complicated and hard to implement. 25 | - Well structured. 26 | - AST based, and preserves source position of nodes. 27 | - Written in pure Go. 28 | 29 | [golang-commonmark](https://gitlab.com/golang-commonmark/markdown) may be a good choice, but it seems to be a copy of the [markdown-it](https://github.com/markdown-it). 30 | 31 | [blackfriday.v2](https://github.com/russross/blackfriday/tree/v2) is a fast and widely used implementation, but it is not CommonMark compliant and cannot be extended from outside of the package since its AST uses not interfaces but structs. 32 | 33 | Furthermore, its behavior differs from other implementations in some cases especially of lists. ([Deep nested lists don't output correctly #329](https://github.com/russross/blackfriday/issues/329), [List block cannot have a second line #244](https://github.com/russross/blackfriday/issues/244), etc). 34 | 35 | This behavior sometimes causes problems. If you migrate your Markdown text to blackfriday-based wikis from GitHub, many lists will immediately be broken. 36 | 37 | As mentioned above, CommonMark is too complicated and hard to implement, So Markdown parsers based on CommonMark barely exist. 38 | 39 | Features 40 | ---------------------- 41 | 42 | - **Standard compliant.** goldmark gets full compliance with the latest CommonMark spec. 43 | - **Extensible.** Do you want to add a `@username` mention syntax to Markdown? 44 | You can easily do it in goldmark. You can add your AST nodes, 45 | parsers for block level elements, parsers for inline level elements, 46 | transformers for paragraphs, transformers for whole AST structure, and 47 | renderers. 48 | - **Performance.** goldmark performs pretty much equally to cmark, 49 | the CommonMark reference implementation written in C. 50 | - **Robust.** goldmark is tested with [go-fuzz](https://github.com/dvyukov/go-fuzz), a fuzz testing tool. 51 | - **Builtin extensions.** goldmark ships with common extensions like tables, strikethrough, 52 | task lists, and definition lists. 53 | - **Depends only on standard libraries.** 54 | 55 | Installation 56 | ---------------------- 57 | ```bash 58 | $ go get github.com/enkogu/goldmark 59 | ``` 60 | 61 | 62 | Usage 63 | ---------------------- 64 | Import packages: 65 | 66 | ``` 67 | import ( 68 | "bytes" 69 | "github.com/enkogu/goldmark" 70 | ) 71 | ``` 72 | 73 | 74 | Convert Markdown documents with the CommonMark compliant mode: 75 | 76 | ```go 77 | var buf bytes.Buffer 78 | if err := goldmark.Convert(source, &buf); err != nil { 79 | panic(err) 80 | } 81 | ``` 82 | 83 | With options 84 | ------------------------------ 85 | 86 | ```go 87 | var buf bytes.Buffer 88 | if err := goldmark.Convert(source, &buf, parser.WithContext(ctx)); err != nil { 89 | panic(err) 90 | } 91 | ``` 92 | 93 | | Functional option | Type | Description | 94 | | ----------------- | ---- | ----------- | 95 | | `parser.WithContext` | A parser.Context | Context for the parsing phase. | 96 | 97 | Custom parser and renderer 98 | -------------------------- 99 | ```go 100 | import ( 101 | "bytes" 102 | "github.com/enkogu/goldmark" 103 | "github.com/enkogu/goldmark/extension" 104 | "github.com/enkogu/goldmark/parser" 105 | "github.com/enkogu/goldmark/renderer/html" 106 | ) 107 | 108 | md := goldmark.New( 109 | goldmark.WithExtensions(extension.GFM), 110 | goldmark.WithParserOptions( 111 | parser.WithAutoHeadingID(), 112 | ), 113 | goldmark.WithRendererOptions( 114 | html.WithHardWraps(), 115 | html.WithXHTML(), 116 | ), 117 | ) 118 | var buf bytes.Buffer 119 | if err := md.Convert(source, &buf); err != nil { 120 | panic(err) 121 | } 122 | ``` 123 | 124 | Parser and Renderer options 125 | ------------------------------ 126 | 127 | ### Parser options 128 | 129 | | Functional option | Type | Description | 130 | | ----------------- | ---- | ----------- | 131 | | `parser.WithBlockParsers` | A `util.PrioritizedSlice` whose elements are `parser.BlockParser` | Parsers for parsing block level elements. | 132 | | `parser.WithInlineParsers` | A `util.PrioritizedSlice` whose elements are `parser.InlineParser` | Parsers for parsing inline level elements. | 133 | | `parser.WithParagraphTransformers` | A `util.PrioritizedSlice` whose elements are `parser.ParagraphTransformer` | Transformers for transforming paragraph nodes. | 134 | | `parser.WithAutoHeadingID` | `-` | Enables auto heading ids. | 135 | | `parser.WithAttribute` | `-` | Enables custom attributes. Currently only headings supports attributes. | 136 | 137 | ### HTML Renderer options 138 | 139 | | Functional option | Type | Description | 140 | | ----------------- | ---- | ----------- | 141 | | `html.WithWriter` | `html.Writer` | `html.Writer` for writing contents to an `io.Writer`. | 142 | | `html.WithHardWraps` | `-` | Render new lines as `
`.| 143 | | `html.WithXHTML` | `-` | Render as XHTML. | 144 | | `html.WithUnsafe` | `-` | By default, goldmark does not render raw HTMLs and potentially dangerous links. With this option, goldmark renders these contents as it is. | 145 | 146 | ### Built-in extensions 147 | 148 | - `extension.Table` 149 | - [GitHub Flavored Markdown: Tables](https://github.github.com/gfm/#tables-extension-) 150 | - `extension.Strikethrough` 151 | - [GitHub Flavored Markdown: Strikethrough](https://github.github.com/gfm/#strikethrough-extension-) 152 | - `extension.Linkify` 153 | - [GitHub Flavored Markdown: Autolinks](https://github.github.com/gfm/#autolinks-extension-) 154 | - `extension.TaskList` 155 | - [GitHub Flavored Markdown: Task list items](https://github.github.com/gfm/#task-list-items-extension-) 156 | - `extension.GFM` 157 | - This extension enables Table, Strikethrough, Linkify and TaskList. 158 | - This extension does not filter tags defined in [6.11: Disallowed Raw HTML (extension)](https://github.github.com/gfm/#disallowed-raw-html-extension-). 159 | If you need to filter HTML tags, see [Security](#security) 160 | - `extension.DefinitionList` 161 | - [PHP Markdown Extra: Definition lists](https://michelf.ca/projects/php-markdown/extra/#def-list) 162 | - `extension.Footnote` 163 | - [PHP Markdown Extra: Footnotes](https://michelf.ca/projects/php-markdown/extra/#footnotes) 164 | - `extension.Typographer` 165 | - This extension substitutes punctuations with typographic entities like [smartypants](https://daringfireball.net/projects/smartypants/). 166 | 167 | ### Attributes 168 | `parser.WithAttribute` option allows you to define attributes on some elements. 169 | 170 | Currently only headings support attributes. 171 | 172 | **Attributes are being discussed in the 173 | [CommonMark forum](https://talk.commonmark.org/t/consistent-attribute-syntax/272). 174 | This syntax may possibly change in the future.** 175 | 176 | 177 | #### Headings 178 | 179 | ``` 180 | ## heading ## {#id .className attrName=attrValue class="class1 class2"} 181 | 182 | ## heading {#id .className attrName=attrValue class="class1 class2"} 183 | ``` 184 | 185 | ``` 186 | heading {#id .className attrName=attrValue} 187 | ============ 188 | ``` 189 | 190 | ### Typographer extension 191 | 192 | Typographer extension translates plain ASCII punctuation characters into typographic punctuation HTML entities. 193 | 194 | Default substitutions are: 195 | 196 | | Punctuation | Default entity | 197 | | ------------ | ---------- | 198 | | `'` | `‘`, `’` | 199 | | `"` | `“`, `”` | 200 | | `--` | `–` | 201 | | `---` | `—` | 202 | | `...` | `…` | 203 | | `<<` | `«` | 204 | | `>>` | `»` | 205 | 206 | You can overwrite the substitutions by `extensions.WithTypographicSubstitutions`. 207 | 208 | ```go 209 | markdown := goldmark.New( 210 | goldmark.WithExtensions( 211 | extension.NewTypographer( 212 | extension.WithTypographicSubstitutions(extension.TypographicSubstitutions{ 213 | extension.LeftSingleQuote: []byte("‚"), 214 | extension.RightSingleQuote: nil, // nil disables a substitution 215 | }), 216 | ), 217 | ), 218 | ) 219 | ``` 220 | 221 | 222 | 223 | Create extensions 224 | -------------------- 225 | **TODO** 226 | 227 | See `extension` directory for examples of extensions. 228 | 229 | Summary: 230 | 231 | 1. Define AST Node as a struct in which `ast.BaseBlock` or `ast.BaseInline` is embedded. 232 | 2. Write a parser that implements `parser.BlockParser` or `parser.InlineParser`. 233 | 3. Write a renderer that implements `renderer.NodeRenderer`. 234 | 4. Define your goldmark extension that implements `goldmark.Extender`. 235 | 236 | Security 237 | -------------------- 238 | By default, goldmark does not render raw HTMLs and potentially dangerous URLs. 239 | If you need to gain more control over untrusted contents, it is recommended to 240 | use an HTML sanitizer such as [bluemonday](https://github.com/microcosm-cc/bluemonday). 241 | 242 | Benchmark 243 | -------------------- 244 | You can run this benchmark in the `_benchmark` directory. 245 | 246 | ### against other golang libraries 247 | 248 | blackfriday v2 seems the fastest, but it is not CommonMark compliant, so the performance of 249 | blackfriday v2 cannot simply be compared with that of the other CommonMark compliant libraries. 250 | 251 | Though goldmark builds clean extensible AST structure and get full compliance with 252 | CommonMark, it is reasonably fast and has lower memory consumption. 253 | 254 | ``` 255 | goos: darwin 256 | goarch: amd64 257 | BenchmarkMarkdown/Blackfriday-v2-12 326 3465240 ns/op 3298861 B/op 20047 allocs/op 258 | BenchmarkMarkdown/GoldMark-12 303 3927494 ns/op 2574809 B/op 13853 allocs/op 259 | BenchmarkMarkdown/CommonMark-12 244 4900853 ns/op 2753851 B/op 20527 allocs/op 260 | BenchmarkMarkdown/Lute-12 130 9195245 ns/op 9175030 B/op 123534 allocs/op 261 | BenchmarkMarkdown/GoMarkdown-12 9 113541994 ns/op 2187472 B/op 22173 allocs/op 262 | ``` 263 | 264 | ### against cmark (CommonMark reference implementation written in C) 265 | 266 | ``` 267 | ----------- cmark ----------- 268 | file: _data.md 269 | iteration: 50 270 | average: 0.0037760639 sec 271 | go run ./goldmark_benchmark.go 272 | ------- goldmark ------- 273 | file: _data.md 274 | iteration: 50 275 | average: 0.0040964230 sec 276 | ``` 277 | 278 | As you can see, goldmark performs pretty much equally to cmark. 279 | 280 | Extensions 281 | -------------------- 282 | 283 | - [goldmark-meta](https://github.com/enkogu/goldmark-meta): A YAML metadata 284 | extension for the goldmark Markdown parser. 285 | - [goldmark-highlighting](https://github.com/enkogu/goldmark-highlighting): A Syntax highlighting extension 286 | for the goldmark markdown parser. 287 | - [goldmark-mathjax](https://github.com/litao91/goldmark-mathjax): Mathjax support for goldmark markdown parser 288 | 289 | Donation 290 | -------------------- 291 | BTC: 1NEDSyUmo4SMTDP83JJQSWi1MvQUGGNMZB 292 | 293 | License 294 | -------------------- 295 | MIT 296 | 297 | Author 298 | -------------------- 299 | Yusuke Inuzuka 300 | -------------------------------------------------------------------------------- /_benchmark/cmark/.gitignore: -------------------------------------------------------------------------------- 1 | cmark-master 2 | cmark_benchmark 3 | -------------------------------------------------------------------------------- /_benchmark/cmark/Makefile: -------------------------------------------------------------------------------- 1 | CMARK_BIN=cmark_benchmark 2 | CMARK_RUN=LD_LIBRARY_PATH=${LD_LIBRARY_PATH}:./cmark-master/build/src ./$(CMARK_BIN) 3 | ifeq ($(OS),Windows_NT) 4 | CMARK_BIN=cmark_benchmark.exe 5 | CMARK_RUN=bash -c "PATH=./cmark-master/build/src:$${PATH} ./$(CMARK_BIN)" 6 | endif 7 | 8 | .PHONY: run 9 | 10 | run: $(CMARK_BIN) 11 | $(CMARK_RUN) 12 | go run ./goldmark_benchmark.go 13 | 14 | ./cmark-master/build/src/config.h: 15 | wget -nc -O cmark.zip https://github.com/commonmark/cmark/archive/master.zip 16 | unzip cmark.zip 17 | rm -f cmark.zip 18 | cd cmark-master && make 19 | 20 | $(CMARK_BIN): ./cmark-master/build/src/config.h 21 | gcc -I./cmark-master/build/src -I./cmark-master/src cmark_benchmark.c -o $(CMARK_BIN) -L./cmark-master/build/src -lcmark 22 | -------------------------------------------------------------------------------- /_benchmark/cmark/cmark_benchmark.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #ifdef WIN32 4 | # include 5 | #else 6 | # include 7 | # include 8 | #endif 9 | #include "cmark.h" 10 | 11 | 12 | #ifdef WIN32 13 | 14 | double get_time() 15 | { 16 | LARGE_INTEGER t, f; 17 | QueryPerformanceCounter(&t); 18 | QueryPerformanceFrequency(&f); 19 | return (double)t.QuadPart/(double)f.QuadPart; 20 | } 21 | 22 | #else 23 | 24 | 25 | double get_time() 26 | { 27 | struct timeval t; 28 | struct timezone tzp; 29 | gettimeofday(&t, &tzp); 30 | return t.tv_sec + t.tv_usec*1e-6; 31 | } 32 | 33 | #endif 34 | 35 | int main(int argc, char **argv) { 36 | char *markdown_file; 37 | FILE *fp; 38 | size_t size; 39 | char *buf; 40 | char *html; 41 | double start, sum; 42 | int i, n; 43 | 44 | n = argc > 1 ? atoi(argv[1]) : 50; 45 | markdown_file = argc > 2 ? argv[2] : "_data.md"; 46 | 47 | fp = fopen(markdown_file,"r"); 48 | if(fp == NULL){ 49 | fprintf(stderr, "can not open %s", markdown_file); 50 | exit(1); 51 | } 52 | 53 | if(fseek(fp, 0, SEEK_END) != 0) { 54 | fprintf(stderr, "can not seek %s", markdown_file); 55 | exit(1); 56 | } 57 | if((size = ftell(fp)) < 0) { 58 | fprintf(stderr, "can not get size of %s", markdown_file); 59 | exit(1); 60 | } 61 | if(fseek(fp, 0, SEEK_SET) != 0) { 62 | fprintf(stderr, "can not seek %s", markdown_file); 63 | exit(1); 64 | } 65 | buf = malloc(sizeof(char) * size); 66 | if(buf == NULL) { 67 | fprintf(stderr, "can not allocate memory for %s", markdown_file); 68 | exit(1); 69 | } 70 | 71 | if(fread(buf, 1, size, fp) < size) { 72 | fprintf(stderr, "failed to read for %s", markdown_file); 73 | exit(1); 74 | } 75 | 76 | fclose(fp); 77 | 78 | for(i = 0; i < n; i++) { 79 | start = get_time(); 80 | html = cmark_markdown_to_html(buf, size, CMARK_OPT_UNSAFE); 81 | free(html); 82 | sum += get_time() - start; 83 | } 84 | printf("----------- cmark -----------\n"); 85 | printf("file: %s\n", markdown_file); 86 | printf("iteration: %d\n", n); 87 | printf("average: %.10f sec\n", sum / (double)n); 88 | 89 | free(buf); 90 | return 0; 91 | } 92 | -------------------------------------------------------------------------------- /_benchmark/cmark/goldmark_benchmark.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io/ioutil" 7 | "os" 8 | "strconv" 9 | "time" 10 | 11 | "github.com/enkogu/goldmark" 12 | "github.com/enkogu/goldmark/renderer/html" 13 | ) 14 | 15 | func main() { 16 | n := 50 17 | file := "_data.md" 18 | if len(os.Args) > 1 { 19 | n, _ = strconv.Atoi(os.Args[1]) 20 | } 21 | if len(os.Args) > 2 { 22 | file = os.Args[2] 23 | } 24 | source, err := ioutil.ReadFile(file) 25 | if err != nil { 26 | panic(err) 27 | } 28 | markdown := goldmark.New(goldmark.WithRendererOptions(html.WithXHTML(), html.WithUnsafe())) 29 | var out bytes.Buffer 30 | markdown.Convert([]byte(""), &out) 31 | 32 | sum := time.Duration(0) 33 | for i := 0; i < n; i++ { 34 | start := time.Now() 35 | out.Reset() 36 | if err := markdown.Convert(source, &out); err != nil { 37 | panic(err) 38 | } 39 | sum += time.Since(start) 40 | } 41 | fmt.Printf("------- goldmark -------\n") 42 | fmt.Printf("file: %s\n", file) 43 | fmt.Printf("iteration: %d\n", n) 44 | fmt.Printf("average: %.10f sec\n", float64((int64(sum)/int64(n)))/1000000000.0) 45 | } 46 | -------------------------------------------------------------------------------- /_benchmark/go/benchmark_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "io/ioutil" 6 | "testing" 7 | 8 | "github.com/enkogu/goldmark" 9 | "github.com/enkogu/goldmark/renderer/html" 10 | "github.com/enkogu/goldmark/util" 11 | gomarkdown "github.com/gomarkdown/markdown" 12 | "gitlab.com/golang-commonmark/markdown" 13 | 14 | bf2 "gopkg.in/russross/blackfriday.v2" 15 | 16 | "github.com/88250/lute" 17 | ) 18 | 19 | func BenchmarkMarkdown(b *testing.B) { 20 | b.Run("Blackfriday-v2", func(b *testing.B) { 21 | r := func(src []byte) ([]byte, error) { 22 | out := bf2.Run(src) 23 | return out, nil 24 | } 25 | doBenchmark(b, r) 26 | }) 27 | 28 | b.Run("GoldMark", func(b *testing.B) { 29 | markdown := goldmark.New( 30 | goldmark.WithRendererOptions(html.WithXHTML(), html.WithUnsafe()), 31 | ) 32 | r := func(src []byte) ([]byte, error) { 33 | var out bytes.Buffer 34 | err := markdown.Convert(src, &out) 35 | return out.Bytes(), err 36 | } 37 | doBenchmark(b, r) 38 | }) 39 | 40 | b.Run("CommonMark", func(b *testing.B) { 41 | md := markdown.New(markdown.XHTMLOutput(true)) 42 | r := func(src []byte) ([]byte, error) { 43 | var out bytes.Buffer 44 | err := md.Render(&out, src) 45 | return out.Bytes(), err 46 | } 47 | doBenchmark(b, r) 48 | }) 49 | 50 | b.Run("Lute", func(b *testing.B) { 51 | luteEngine := lute.New() 52 | luteEngine.SetGFMAutoLink(false) 53 | luteEngine.SetGFMStrikethrough(false) 54 | luteEngine.SetGFMTable(false) 55 | luteEngine.SetGFMTaskListItem(false) 56 | luteEngine.SetCodeSyntaxHighlight(false) 57 | luteEngine.SetSoftBreak2HardBreak(false) 58 | luteEngine.SetAutoSpace(false) 59 | luteEngine.SetFixTermTypo(false) 60 | r := func(src []byte) ([]byte, error) { 61 | out, err := luteEngine.MarkdownStr("Benchmark", util.BytesToReadOnlyString(src)) 62 | return util.StringToReadOnlyBytes(out), err 63 | } 64 | doBenchmark(b, r) 65 | }) 66 | 67 | b.Run("GoMarkdown", func(b *testing.B) { 68 | r := func(src []byte) ([]byte, error) { 69 | out := gomarkdown.ToHTML(src, nil, nil) 70 | return out, nil 71 | } 72 | doBenchmark(b, r) 73 | }) 74 | 75 | } 76 | 77 | // The different frameworks have different APIs. Create an adapter that 78 | // should behave the same in the memory department. 79 | func doBenchmark(b *testing.B, render func(src []byte) ([]byte, error)) { 80 | b.StopTimer() 81 | source, err := ioutil.ReadFile("_data.md") 82 | if err != nil { 83 | b.Fatal(err) 84 | } 85 | b.StartTimer() 86 | for i := 0; i < b.N; i++ { 87 | out, err := render(source) 88 | if err != nil { 89 | b.Fatal(err) 90 | } 91 | if len(out) < 100 { 92 | b.Fatal("No result") 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /_test/extra.txt: -------------------------------------------------------------------------------- 1 | 1 2 | //- - - - - - - - -// 3 | * A 4 | B 5 | //- - - - - - - - -// 6 |
    7 |
  • A 8 | B
  • 9 |
10 | //= = = = = = = = = = = = = = = = = = = = = = = =// 11 | 12 | 13 | 14 | 2 15 | //- - - - - - - - -// 16 | **test**\ 17 | test**test**\ 18 | **test**test\ 19 | test**test** 20 | //- - - - - - - - -// 21 |

test
22 | testtest
23 | testtest
24 | testtest

25 | //= = = = = = = = = = = = = = = = = = = = = = = =// 26 | 27 | 28 | 29 | 3 30 | //- - - - - - - - -// 31 | >* > 32 | > 1 33 | > 2 34 | >3 35 | //- - - - - - - - -// 36 |
37 |
    38 |
  • 39 |
    40 |
    41 |
  • 42 |
43 |

1 44 | 2 45 | 3

46 |
47 | //= = = = = = = = = = = = = = = = = = = = = = = =// 48 | 49 | 50 | 51 | 4 52 | //- - - - - - - - -// 53 | `test`a`test` 54 | //- - - - - - - - -// 55 |

testatest

56 | //= = = = = = = = = = = = = = = = = = = = = = = =// 57 | 58 | -------------------------------------------------------------------------------- /_test/options.txt: -------------------------------------------------------------------------------- 1 | 1 2 | //- - - - - - - - -// 3 | ## Title 0 4 | 5 | ## Title1 # {#id_1 .class-1} 6 | 7 | ## Title2 {#id_2} 8 | 9 | ## Title3 ## {#id_3 .class-3} 10 | 11 | ## Title4 ## {attr3=value3} 12 | 13 | ## Title5 ## {#id_5 attr5=value5} 14 | 15 | ## Title6 ## {#id_6 .class6 attr6=value6} 16 | 17 | ## Title7 ## {#id_7 attr7="value \"7"} 18 | 19 | ## Title8 {#id .className attrName=attrValue class="class1 class2"} 20 | //- - - - - - - - -// 21 |

Title 0

22 |

Title1

23 |

Title2

24 |

Title3

25 |

Title4

26 |

Title5

27 |

Title6

28 |

Title7

29 |

Title8

30 | //= = = = = = = = = = = = = = = = = = = = = = = =// 31 | 32 | 2 33 | //- - - - - - - - -// 34 | # 35 | # FOO 36 | //- - - - - - - - -// 37 |

38 |

FOO

39 | //= = = = = = = = = = = = = = = = = = = = = = = =// 40 | -------------------------------------------------------------------------------- /ast/block.go: -------------------------------------------------------------------------------- 1 | package ast 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | textm "github.com/enkogu/goldmark/text" 8 | ) 9 | 10 | // A BaseBlock struct implements the Node interface. 11 | type BaseBlock struct { 12 | BaseNode 13 | blankPreviousLines bool 14 | lines *textm.Segments 15 | } 16 | 17 | // Type implements Node.Type 18 | func (b *BaseBlock) Type() NodeType { 19 | return TypeBlock 20 | } 21 | 22 | // IsRaw implements Node.IsRaw 23 | func (b *BaseBlock) IsRaw() bool { 24 | return false 25 | } 26 | 27 | // HasBlankPreviousLines implements Node.HasBlankPreviousLines. 28 | func (b *BaseBlock) HasBlankPreviousLines() bool { 29 | return b.blankPreviousLines 30 | } 31 | 32 | // SetBlankPreviousLines implements Node.SetBlankPreviousLines. 33 | func (b *BaseBlock) SetBlankPreviousLines(v bool) { 34 | b.blankPreviousLines = v 35 | } 36 | 37 | // Lines implements Node.Lines 38 | func (b *BaseBlock) Lines() *textm.Segments { 39 | if b.lines == nil { 40 | b.lines = textm.NewSegments() 41 | } 42 | return b.lines 43 | } 44 | 45 | // SetLines implements Node.SetLines 46 | func (b *BaseBlock) SetLines(v *textm.Segments) { 47 | b.lines = v 48 | } 49 | 50 | // A Document struct is a root node of Markdown text. 51 | type Document struct { 52 | BaseBlock 53 | } 54 | 55 | // KindDocument is a NodeKind of the Document node. 56 | var KindDocument = NewNodeKind("Document") 57 | 58 | // Dump implements Node.Dump . 59 | func (n *Document) Dump(source []byte, level int) { 60 | DumpHelper(n, source, level, nil, nil) 61 | } 62 | 63 | // Type implements Node.Type . 64 | func (n *Document) Type() NodeType { 65 | return TypeDocument 66 | } 67 | 68 | // Kind implements Node.Kind. 69 | func (n *Document) Kind() NodeKind { 70 | return KindDocument 71 | } 72 | 73 | // NewDocument returns a new Document node. 74 | func NewDocument() *Document { 75 | return &Document{ 76 | BaseBlock: BaseBlock{}, 77 | } 78 | } 79 | 80 | // A TextBlock struct is a node whose lines 81 | // should be rendered without any containers. 82 | type TextBlock struct { 83 | BaseBlock 84 | } 85 | 86 | // Dump implements Node.Dump . 87 | func (n *TextBlock) Dump(source []byte, level int) { 88 | DumpHelper(n, source, level, nil, nil) 89 | } 90 | 91 | // KindTextBlock is a NodeKind of the TextBlock node. 92 | var KindTextBlock = NewNodeKind("TextBlock") 93 | 94 | // Kind implements Node.Kind. 95 | func (n *TextBlock) Kind() NodeKind { 96 | return KindTextBlock 97 | } 98 | 99 | // NewTextBlock returns a new TextBlock node. 100 | func NewTextBlock() *TextBlock { 101 | return &TextBlock{ 102 | BaseBlock: BaseBlock{}, 103 | } 104 | } 105 | 106 | // A Paragraph struct represents a paragraph of Markdown text. 107 | type Paragraph struct { 108 | BaseBlock 109 | } 110 | 111 | // Dump implements Node.Dump . 112 | func (n *Paragraph) Dump(source []byte, level int) { 113 | DumpHelper(n, source, level, nil, nil) 114 | } 115 | 116 | // KindParagraph is a NodeKind of the Paragraph node. 117 | var KindParagraph = NewNodeKind("Paragraph") 118 | 119 | // Kind implements Node.Kind. 120 | func (n *Paragraph) Kind() NodeKind { 121 | return KindParagraph 122 | } 123 | 124 | // NewParagraph returns a new Paragraph node. 125 | func NewParagraph() *Paragraph { 126 | return &Paragraph{ 127 | BaseBlock: BaseBlock{}, 128 | } 129 | } 130 | 131 | // IsParagraph returns true if the given node implements the Paragraph interface, 132 | // otherwise false. 133 | func IsParagraph(node Node) bool { 134 | _, ok := node.(*Paragraph) 135 | return ok 136 | } 137 | 138 | // A Heading struct represents headings like SetextHeading and ATXHeading. 139 | type Heading struct { 140 | BaseBlock 141 | // Level returns a level of this heading. 142 | // This value is between 1 and 6. 143 | Level int 144 | } 145 | 146 | // Dump implements Node.Dump . 147 | func (n *Heading) Dump(source []byte, level int) { 148 | m := map[string]string{ 149 | "Level": fmt.Sprintf("%d", n.Level), 150 | } 151 | DumpHelper(n, source, level, m, nil) 152 | } 153 | 154 | // KindHeading is a NodeKind of the Heading node. 155 | var KindHeading = NewNodeKind("Heading") 156 | 157 | // Kind implements Node.Kind. 158 | func (n *Heading) Kind() NodeKind { 159 | return KindHeading 160 | } 161 | 162 | // NewHeading returns a new Heading node. 163 | func NewHeading(level int) *Heading { 164 | return &Heading{ 165 | BaseBlock: BaseBlock{}, 166 | Level: level, 167 | } 168 | } 169 | 170 | // A ThematicBreak struct represents a thematic break of Markdown text. 171 | type ThematicBreak struct { 172 | BaseBlock 173 | } 174 | 175 | // Dump implements Node.Dump . 176 | func (n *ThematicBreak) Dump(source []byte, level int) { 177 | DumpHelper(n, source, level, nil, nil) 178 | } 179 | 180 | // KindThematicBreak is a NodeKind of the ThematicBreak node. 181 | var KindThematicBreak = NewNodeKind("ThematicBreak") 182 | 183 | // Kind implements Node.Kind. 184 | func (n *ThematicBreak) Kind() NodeKind { 185 | return KindThematicBreak 186 | } 187 | 188 | // NewThematicBreak returns a new ThematicBreak node. 189 | func NewThematicBreak() *ThematicBreak { 190 | return &ThematicBreak{ 191 | BaseBlock: BaseBlock{}, 192 | } 193 | } 194 | 195 | // A CodeBlock interface represents an indented code block of Markdown text. 196 | type CodeBlock struct { 197 | BaseBlock 198 | } 199 | 200 | // IsRaw implements Node.IsRaw. 201 | func (n *CodeBlock) IsRaw() bool { 202 | return true 203 | } 204 | 205 | // Dump implements Node.Dump . 206 | func (n *CodeBlock) Dump(source []byte, level int) { 207 | DumpHelper(n, source, level, nil, nil) 208 | } 209 | 210 | // KindCodeBlock is a NodeKind of the CodeBlock node. 211 | var KindCodeBlock = NewNodeKind("CodeBlock") 212 | 213 | // Kind implements Node.Kind. 214 | func (n *CodeBlock) Kind() NodeKind { 215 | return KindCodeBlock 216 | } 217 | 218 | // NewCodeBlock returns a new CodeBlock node. 219 | func NewCodeBlock() *CodeBlock { 220 | return &CodeBlock{ 221 | BaseBlock: BaseBlock{}, 222 | } 223 | } 224 | 225 | // A FencedCodeBlock struct represents a fenced code block of Markdown text. 226 | type FencedCodeBlock struct { 227 | BaseBlock 228 | // Info returns a info text of this fenced code block. 229 | Info *Text 230 | 231 | language []byte 232 | } 233 | 234 | // Language returns an language in an info string. 235 | // Language returns nil if this node does not have an info string. 236 | func (n *FencedCodeBlock) Language(source []byte) []byte { 237 | if n.language == nil && n.Info != nil { 238 | segment := n.Info.Segment 239 | info := segment.Value(source) 240 | i := 0 241 | for ; i < len(info); i++ { 242 | if info[i] == ' ' { 243 | break 244 | } 245 | } 246 | n.language = info[:i] 247 | } 248 | return n.language 249 | } 250 | 251 | // IsRaw implements Node.IsRaw. 252 | func (n *FencedCodeBlock) IsRaw() bool { 253 | return true 254 | } 255 | 256 | // Dump implements Node.Dump . 257 | func (n *FencedCodeBlock) Dump(source []byte, level int) { 258 | m := map[string]string{} 259 | if n.Info != nil { 260 | m["Info"] = fmt.Sprintf("\"%s\"", n.Info.Text(source)) 261 | } 262 | DumpHelper(n, source, level, m, nil) 263 | } 264 | 265 | // KindFencedCodeBlock is a NodeKind of the FencedCodeBlock node. 266 | var KindFencedCodeBlock = NewNodeKind("FencedCodeBlock") 267 | 268 | // Kind implements Node.Kind. 269 | func (n *FencedCodeBlock) Kind() NodeKind { 270 | return KindFencedCodeBlock 271 | } 272 | 273 | // NewFencedCodeBlock return a new FencedCodeBlock node. 274 | func NewFencedCodeBlock(info *Text) *FencedCodeBlock { 275 | return &FencedCodeBlock{ 276 | BaseBlock: BaseBlock{}, 277 | Info: info, 278 | } 279 | } 280 | 281 | // A Blockquote struct represents an blockquote block of Markdown text. 282 | type Blockquote struct { 283 | BaseBlock 284 | } 285 | 286 | // Dump implements Node.Dump . 287 | func (n *Blockquote) Dump(source []byte, level int) { 288 | DumpHelper(n, source, level, nil, nil) 289 | } 290 | 291 | // KindBlockquote is a NodeKind of the Blockquote node. 292 | var KindBlockquote = NewNodeKind("Blockquote") 293 | 294 | // Kind implements Node.Kind. 295 | func (n *Blockquote) Kind() NodeKind { 296 | return KindBlockquote 297 | } 298 | 299 | // NewBlockquote returns a new Blockquote node. 300 | func NewBlockquote() *Blockquote { 301 | return &Blockquote{ 302 | BaseBlock: BaseBlock{}, 303 | } 304 | } 305 | 306 | // A List structr represents a list of Markdown text. 307 | type List struct { 308 | BaseBlock 309 | 310 | // Marker is a markar character like '-', '+', ')' and '.'. 311 | Marker byte 312 | 313 | // IsTight is a true if this list is a 'tight' list. 314 | // See https://spec.commonmark.org/0.29/#loose for details. 315 | IsTight bool 316 | 317 | // Start is an initial number of this ordered list. 318 | // If this list is not an ordered list, Start is 0. 319 | Start int 320 | } 321 | 322 | // IsOrdered returns true if this list is an ordered list, otherwise false. 323 | func (l *List) IsOrdered() bool { 324 | return l.Marker == '.' || l.Marker == ')' 325 | } 326 | 327 | // CanContinue returns true if this list can continue with 328 | // the given mark and a list type, otherwise false. 329 | func (l *List) CanContinue(marker byte, isOrdered bool) bool { 330 | return marker == l.Marker && isOrdered == l.IsOrdered() 331 | } 332 | 333 | // Dump implements Node.Dump. 334 | func (l *List) Dump(source []byte, level int) { 335 | m := map[string]string{ 336 | "Ordered": fmt.Sprintf("%v", l.IsOrdered()), 337 | "Marker": fmt.Sprintf("%c", l.Marker), 338 | "Tight": fmt.Sprintf("%v", l.IsTight), 339 | } 340 | if l.IsOrdered() { 341 | m["Start"] = fmt.Sprintf("%d", l.Start) 342 | } 343 | DumpHelper(l, source, level, m, nil) 344 | } 345 | 346 | // KindList is a NodeKind of the List node. 347 | var KindList = NewNodeKind("List") 348 | 349 | // Kind implements Node.Kind. 350 | func (l *List) Kind() NodeKind { 351 | return KindList 352 | } 353 | 354 | // NewList returns a new List node. 355 | func NewList(marker byte) *List { 356 | return &List{ 357 | BaseBlock: BaseBlock{}, 358 | Marker: marker, 359 | IsTight: true, 360 | } 361 | } 362 | 363 | // A ListItem struct represents a list item of Markdown text. 364 | type ListItem struct { 365 | BaseBlock 366 | 367 | // Offset is an offset potision of this item. 368 | Offset int 369 | } 370 | 371 | // Dump implements Node.Dump. 372 | func (n *ListItem) Dump(source []byte, level int) { 373 | m := map[string]string{ 374 | "Offset": fmt.Sprintf("%d", n.Offset), 375 | } 376 | DumpHelper(n, source, level, m, nil) 377 | } 378 | 379 | // KindListItem is a NodeKind of the ListItem node. 380 | var KindListItem = NewNodeKind("ListItem") 381 | 382 | // Kind implements Node.Kind. 383 | func (n *ListItem) Kind() NodeKind { 384 | return KindListItem 385 | } 386 | 387 | // NewListItem returns a new ListItem node. 388 | func NewListItem(offset int) *ListItem { 389 | return &ListItem{ 390 | BaseBlock: BaseBlock{}, 391 | Offset: offset, 392 | } 393 | } 394 | 395 | // HTMLBlockType represents kinds of an html blocks. 396 | // See https://spec.commonmark.org/0.29/#html-blocks 397 | type HTMLBlockType int 398 | 399 | const ( 400 | // HTMLBlockType1 represents type 1 html blocks 401 | HTMLBlockType1 HTMLBlockType = iota + 1 402 | // HTMLBlockType2 represents type 2 html blocks 403 | HTMLBlockType2 404 | // HTMLBlockType3 represents type 3 html blocks 405 | HTMLBlockType3 406 | // HTMLBlockType4 represents type 4 html blocks 407 | HTMLBlockType4 408 | // HTMLBlockType5 represents type 5 html blocks 409 | HTMLBlockType5 410 | // HTMLBlockType6 represents type 6 html blocks 411 | HTMLBlockType6 412 | // HTMLBlockType7 represents type 7 html blocks 413 | HTMLBlockType7 414 | ) 415 | 416 | // An HTMLBlock struct represents an html block of Markdown text. 417 | type HTMLBlock struct { 418 | BaseBlock 419 | 420 | // Type is a type of this html block. 421 | HTMLBlockType HTMLBlockType 422 | 423 | // ClosureLine is a line that closes this html block. 424 | ClosureLine textm.Segment 425 | } 426 | 427 | // IsRaw implements Node.IsRaw. 428 | func (n *HTMLBlock) IsRaw() bool { 429 | return true 430 | } 431 | 432 | // HasClosure returns true if this html block has a closure line, 433 | // otherwise false. 434 | func (n *HTMLBlock) HasClosure() bool { 435 | return n.ClosureLine.Start >= 0 436 | } 437 | 438 | // Dump implements Node.Dump. 439 | func (n *HTMLBlock) Dump(source []byte, level int) { 440 | indent := strings.Repeat(" ", level) 441 | fmt.Printf("%s%s {\n", indent, "HTMLBlock") 442 | indent2 := strings.Repeat(" ", level+1) 443 | fmt.Printf("%sRawText: \"", indent2) 444 | for i := 0; i < n.Lines().Len(); i++ { 445 | s := n.Lines().At(i) 446 | fmt.Print(string(source[s.Start:s.Stop])) 447 | } 448 | fmt.Printf("\"\n") 449 | for c := n.FirstChild(); c != nil; c = c.NextSibling() { 450 | c.Dump(source, level+1) 451 | } 452 | if n.HasClosure() { 453 | cl := n.ClosureLine 454 | fmt.Printf("%sClosure: \"%s\"\n", indent2, string(cl.Value(source))) 455 | } 456 | fmt.Printf("%s}\n", indent) 457 | } 458 | 459 | // KindHTMLBlock is a NodeKind of the HTMLBlock node. 460 | var KindHTMLBlock = NewNodeKind("HTMLBlock") 461 | 462 | // Kind implements Node.Kind. 463 | func (n *HTMLBlock) Kind() NodeKind { 464 | return KindHTMLBlock 465 | } 466 | 467 | // NewHTMLBlock returns a new HTMLBlock node. 468 | func NewHTMLBlock(typ HTMLBlockType) *HTMLBlock { 469 | return &HTMLBlock{ 470 | BaseBlock: BaseBlock{}, 471 | HTMLBlockType: typ, 472 | ClosureLine: textm.NewSegment(-1, -1), 473 | } 474 | } 475 | -------------------------------------------------------------------------------- /commonmark_test.go: -------------------------------------------------------------------------------- 1 | package goldmark_test 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | "testing" 7 | 8 | . "github.com/enkogu/goldmark" 9 | "github.com/enkogu/goldmark/renderer/html" 10 | "github.com/enkogu/goldmark/testutil" 11 | ) 12 | 13 | type commonmarkSpecTestCase struct { 14 | Markdown string `json:"markdown"` 15 | HTML string `json:"html"` 16 | Example int `json:"example"` 17 | StartLine int `json:"start_line"` 18 | EndLine int `json:"end_line"` 19 | Section string `json:"section"` 20 | } 21 | 22 | func TestSpec(t *testing.T) { 23 | bs, err := ioutil.ReadFile("_test/spec.json") 24 | if err != nil { 25 | panic(err) 26 | } 27 | var testCases []commonmarkSpecTestCase 28 | if err := json.Unmarshal(bs, &testCases); err != nil { 29 | panic(err) 30 | } 31 | cases := []testutil.MarkdownTestCase{} 32 | for _, c := range testCases { 33 | cases = append(cases, testutil.MarkdownTestCase{ 34 | No: c.Example, 35 | Markdown: c.Markdown, 36 | Expected: c.HTML, 37 | }) 38 | } 39 | markdown := New(WithRendererOptions( 40 | html.WithXHTML(), 41 | html.WithUnsafe(), 42 | )) 43 | testutil.DoTestCases(markdown, cases, t) 44 | } 45 | -------------------------------------------------------------------------------- /extension/_test/definition_list.txt: -------------------------------------------------------------------------------- 1 | 1 2 | //- - - - - - - - -// 3 | Apple 4 | : Pomaceous fruit of plants of the genus Malus in 5 | the family Rosaceae. 6 | 7 | Orange 8 | : The fruit of an evergreen tree of the genus Citrus. 9 | //- - - - - - - - -// 10 |
11 |
Apple
12 |
Pomaceous fruit of plants of the genus Malus in 13 | the family Rosaceae.
14 |
Orange
15 |
The fruit of an evergreen tree of the genus Citrus.
16 |
17 | //= = = = = = = = = = = = = = = = = = = = = = = =// 18 | 19 | 20 | 21 | 2 22 | //- - - - - - - - -// 23 | Apple 24 | : Pomaceous fruit of plants of the genus Malus in 25 | the family Rosaceae. 26 | : An American computer company. 27 | 28 | Orange 29 | : The fruit of an evergreen tree of the genus Citrus. 30 | //- - - - - - - - -// 31 |
32 |
Apple
33 |
Pomaceous fruit of plants of the genus Malus in 34 | the family Rosaceae.
35 |
An American computer company.
36 |
Orange
37 |
The fruit of an evergreen tree of the genus Citrus.
38 |
39 | //= = = = = = = = = = = = = = = = = = = = = = = =// 40 | 41 | 42 | 43 | 3 44 | //- - - - - - - - -// 45 | Term 1 46 | Term 2 47 | : Definition a 48 | 49 | Term 3 50 | : Definition b 51 | //- - - - - - - - -// 52 |
53 |
Term 1
54 |
Term 2
55 |
Definition a
56 |
Term 3
57 |
Definition b
58 |
59 | //= = = = = = = = = = = = = = = = = = = = = = = =// 60 | 61 | 62 | 63 | 4 64 | //- - - - - - - - -// 65 | Apple 66 | 67 | : Pomaceous fruit of plants of the genus Malus in 68 | the family Rosaceae. 69 | 70 | Orange 71 | 72 | : The fruit of an evergreen tree of the genus Citrus. 73 | //- - - - - - - - -// 74 |
75 |
Apple
76 |
77 |

Pomaceous fruit of plants of the genus Malus in 78 | the family Rosaceae.

79 |
80 |
Orange
81 |
82 |

The fruit of an evergreen tree of the genus Citrus.

83 |
84 |
85 | //= = = = = = = = = = = = = = = = = = = = = = = =// 86 | 87 | 88 | 5 89 | //- - - - - - - - -// 90 | Term 1 91 | 92 | : This is a definition with two paragraphs. Lorem ipsum 93 | dolor sit amet, consectetuer adipiscing elit. Aliquam 94 | hendrerit mi posuere lectus. 95 | 96 | Vestibulum enim wisi, viverra nec, fringilla in, laoreet 97 | vitae, risus. 98 | 99 | : Second definition for term 1, also wrapped in a paragraph 100 | because of the blank line preceding it. 101 | 102 | Term 2 103 | 104 | : This definition has a code block, a blockquote and a list. 105 | 106 | code block. 107 | 108 | > block quote 109 | > on two lines. 110 | 111 | 1. first list item 112 | 2. second list item 113 | //- - - - - - - - -// 114 |
115 |
Term 1
116 |
117 |

This is a definition with two paragraphs. Lorem ipsum 118 | dolor sit amet, consectetuer adipiscing elit. Aliquam 119 | hendrerit mi posuere lectus.

120 |

Vestibulum enim wisi, viverra nec, fringilla in, laoreet 121 | vitae, risus.

122 |
123 |
124 |

Second definition for term 1, also wrapped in a paragraph 125 | because of the blank line preceding it.

126 |
127 |
Term 2
128 |
129 |

This definition has a code block, a blockquote and a list.

130 |
code block.
131 | 
132 |
133 |

block quote 134 | on two lines.

135 |
136 |
    137 |
  1. first list item
  2. 138 |
  3. second list item
  4. 139 |
140 |
141 |
142 | //= = = = = = = = = = = = = = = = = = = = = = = =// 143 | 144 | -------------------------------------------------------------------------------- /extension/_test/footnote.txt: -------------------------------------------------------------------------------- 1 | 1 2 | //- - - - - - - - -// 3 | That's some text with a footnote.[^1] 4 | 5 | [^1]: And that's the footnote. 6 | 7 | That's the second paragraph. 8 | //- - - - - - - - -// 9 |

That's some text with a footnote.1

10 |
11 |
12 |
    13 |
  1. 14 |

    And that's the footnote.

    15 |

    That's the second paragraph. ↩︎

    16 |
  2. 17 |
18 |
19 | //= = = = = = = = = = = = = = = = = = = = = = = =// 20 | 21 | 3 22 | //- - - - - - - - -// 23 | [^000]:0 [^]: 24 | //- - - - - - - - -// 25 | //= = = = = = = = = = = = = = = = = = = = = = = =// 26 | 27 | 4 28 | //- - - - - - - - -// 29 | This[^3] is[^1] text with footnotes[^2]. 30 | 31 | [^1]: Footnote one 32 | [^2]: Footnote two 33 | [^3]: Footnote three 34 | //- - - - - - - - -// 35 |

This1 is2 text with footnotes3.

36 |
37 |
38 |
    39 |
  1. 40 |

    Footnote three ↩︎

    41 |
  2. 42 |
  3. 43 |

    Footnote one ↩︎

    44 |
  4. 45 |
  5. 46 |

    Footnote two ↩︎

    47 |
  6. 48 |
49 |
50 | //= = = = = = = = = = = = = = = = = = = = = = = =// 51 | -------------------------------------------------------------------------------- /extension/_test/linkify.txt: -------------------------------------------------------------------------------- 1 | 1 2 | //- - - - - - - - -// 3 | www.commonmark.org 4 | //- - - - - - - - -// 5 |

www.commonmark.org

6 | //= = = = = = = = = = = = = = = = = = = = = = = =// 7 | 8 | 9 | 10 | 2 11 | //- - - - - - - - -// 12 | Visit www.commonmark.org/help for more information. 13 | //- - - - - - - - -// 14 |

Visit www.commonmark.org/help for more information.

15 | //= = = = = = = = = = = = = = = = = = = = = = = =// 16 | 17 | 18 | 19 | 3 20 | //- - - - - - - - -// 21 | www.google.com/search?q=Markup+(business) 22 | 23 | www.google.com/search?q=Markup+(business))) 24 | 25 | (www.google.com/search?q=Markup+(business)) 26 | 27 | (www.google.com/search?q=Markup+(business) 28 | //- - - - - - - - -// 29 |

www.google.com/search?q=Markup+(business)

30 |

www.google.com/search?q=Markup+(business)))

31 |

(www.google.com/search?q=Markup+(business))

32 |

(www.google.com/search?q=Markup+(business)

33 | //= = = = = = = = = = = = = = = = = = = = = = = =// 34 | 35 | 36 | 37 | 4 38 | //- - - - - - - - -// 39 | www.google.com/search?q=(business))+ok 40 | //- - - - - - - - -// 41 |

www.google.com/search?q=(business))+ok

42 | //= = = = = = = = = = = = = = = = = = = = = = = =// 43 | 44 | 45 | 46 | 5 47 | //- - - - - - - - -// 48 | www.google.com/search?q=commonmark&hl=en 49 | 50 | www.google.com/search?q=commonmark&hl; 51 | //- - - - - - - - -// 52 |

www.google.com/search?q=commonmark&hl=en

53 |

www.google.com/search?q=commonmark&hl;

54 | //= = = = = = = = = = = = = = = = = = = = = = = =// 55 | 56 | 57 | 58 | 6 59 | //- - - - - - - - -// 60 | www.commonmark.org/hewww.commonmark.org/he<lp

63 | //= = = = = = = = = = = = = = = = = = = = = = = =// 64 | 65 | 66 | 67 | 7 68 | //- - - - - - - - -// 69 | http://commonmark.org 70 | 71 | (Visit https://encrypted.google.com/search?q=Markup+(business)) 72 | 73 | Anonymous FTP is available at ftp://foo.bar.baz. 74 | //- - - - - - - - -// 75 |

http://commonmark.org

76 |

(Visit https://encrypted.google.com/search?q=Markup+(business))

77 |

Anonymous FTP is available at ftp://foo.bar.baz.

78 | //= = = = = = = = = = = = = = = = = = = = = = = =// 79 | 80 | 81 | 82 | 8 83 | //- - - - - - - - -// 84 | foo@bar.baz 85 | //- - - - - - - - -// 86 |

foo@bar.baz

87 | //= = = = = = = = = = = = = = = = = = = = = = = =// 88 | 89 | 90 | 91 | 9 92 | //- - - - - - - - -// 93 | hello@mail+xyz.example isn't valid, but hello+xyz@mail.example is. 94 | //- - - - - - - - -// 95 |

hello@mail+xyz.example isn't valid, but hello+xyz@mail.example is.

96 | //= = = = = = = = = = = = = = = = = = = = = = = =// 97 | 98 | 99 | 100 | 10 101 | //- - - - - - - - -// 102 | a.b-c_d@a.b 103 | 104 | a.b-c_d@a.b. 105 | 106 | a.b-c_d@a.b- 107 | 108 | a.b-c_d@a.b_ 109 | //- - - - - - - - -// 110 |

a.b-c_d@a.b

111 |

a.b-c_d@a.b.

112 |

a.b-c_d@a.b-

113 |

a.b-c_d@a.b_

114 | //= = = = = = = = = = = = = = = = = = = = = = = =// 115 | 116 | 117 | 118 | 11 119 | //- - - - - - - - -// 120 | https://github.com#sun,mon 121 | //- - - - - - - - -// 122 |

https://github.com#sun,mon

123 | //= = = = = = = = = = = = = = = = = = = = = = = =// 124 | 125 | 126 | 127 | 12 128 | //- - - - - - - - -// 129 | https://github.com/sunday's 130 | //- - - - - - - - -// 131 |

https://github.com/sunday's

132 | //= = = = = = = = = = = = = = = = = = = = = = = =// 133 | -------------------------------------------------------------------------------- /extension/_test/strikethrough.txt: -------------------------------------------------------------------------------- 1 | 1 2 | //- - - - - - - - -// 3 | ~~Hi~~ Hello, world! 4 | //- - - - - - - - -// 5 |

Hi Hello, world!

6 | //= = = = = = = = = = = = = = = = = = = = = = = =// 7 | 8 | 9 | 10 | 2 11 | //- - - - - - - - -// 12 | This ~~has a 13 | 14 | new paragraph~~. 15 | //- - - - - - - - -// 16 |

This ~~has a

17 |

new paragraph~~.

18 | //= = = = = = = = = = = = = = = = = = = = = = = =// 19 | -------------------------------------------------------------------------------- /extension/_test/table.txt: -------------------------------------------------------------------------------- 1 | 1 2 | //- - - - - - - - -// 3 | | foo | bar | 4 | | --- | --- | 5 | | baz | bim | 6 | //- - - - - - - - -// 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
foobar
bazbim
21 | //= = = = = = = = = = = = = = = = = = = = = = = =// 22 | 23 | 24 | 25 | 2 26 | //- - - - - - - - -// 27 | | abc | defghi | 28 | :-: | -----------: 29 | bar | baz 30 | //- - - - - - - - -// 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 |
abcdefghi
barbaz
45 | //= = = = = = = = = = = = = = = = = = = = = = = =// 46 | 47 | 48 | 49 | 3 50 | //- - - - - - - - -// 51 | | f\|oo | 52 | | ------ | 53 | | b `\|` az | 54 | | b **\|** im | 55 | //- - - - - - - - -// 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 |
f|oo
b \| az
b | im
71 | //= = = = = = = = = = = = = = = = = = = = = = = =// 72 | 73 | 74 | 75 | 4 76 | //- - - - - - - - -// 77 | | abc | def | 78 | | --- | --- | 79 | | bar | baz | 80 | > bar 81 | //- - - - - - - - -// 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 |
abcdef
barbaz
96 |
97 |

bar

98 |
99 | //= = = = = = = = = = = = = = = = = = = = = = = =// 100 | 101 | 102 | 103 | 5 104 | //- - - - - - - - -// 105 | | abc | def | 106 | | --- | --- | 107 | | bar | baz | 108 | bar 109 | 110 | bar 111 | //- - - - - - - - -// 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 |
abcdef
barbaz
bar
130 |

bar

131 | //= = = = = = = = = = = = = = = = = = = = = = = =// 132 | 133 | 134 | 135 | 6 136 | //- - - - - - - - -// 137 | | abc | def | 138 | | --- | 139 | | bar | 140 | //- - - - - - - - -// 141 |

| abc | def | 142 | | --- | 143 | | bar |

144 | //= = = = = = = = = = = = = = = = = = = = = = = =// 145 | 146 | 147 | 148 | 7 149 | //- - - - - - - - -// 150 | | abc | def | 151 | | --- | --- | 152 | | bar | 153 | | bar | baz | boo | 154 | //- - - - - - - - -// 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 |
abcdef
bar
barbaz
173 | //= = = = = = = = = = = = = = = = = = = = = = = =// 174 | 175 | 176 | 177 | 8 178 | //- - - - - - - - -// 179 | | abc | def | 180 | | --- | --- | 181 | //- - - - - - - - -// 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 |
abcdef
190 | //= = = = = = = = = = = = = = = = = = = = = = = =// 191 | -------------------------------------------------------------------------------- /extension/_test/tasklist.txt: -------------------------------------------------------------------------------- 1 | 1 2 | //- - - - - - - - -// 3 | - [ ] foo 4 | - [x] bar 5 | //- - - - - - - - -// 6 |
    7 |
  • foo
  • 8 |
  • bar
  • 9 |
10 | //= = = = = = = = = = = = = = = = = = = = = = = =// 11 | 12 | 13 | 14 | 2 15 | //- - - - - - - - -// 16 | - [x] foo 17 | - [ ] bar 18 | - [x] baz 19 | - [ ] bim 20 | //- - - - - - - - -// 21 |
    22 |
  • foo 23 |
      24 |
    • bar
    • 25 |
    • baz
    • 26 |
    27 |
  • 28 |
  • bim
  • 29 |
30 | //= = = = = = = = = = = = = = = = = = = = = = = =// 31 | -------------------------------------------------------------------------------- /extension/_test/typographer.txt: -------------------------------------------------------------------------------- 1 | 1 2 | //- - - - - - - - -// 3 | This should 'be' replaced 4 | //- - - - - - - - -// 5 |

This should ‘be’ replaced

6 | //= = = = = = = = = = = = = = = = = = = = = = = =// 7 | 8 | 2 9 | //- - - - - - - - -// 10 | This should "be" replaced 11 | //- - - - - - - - -// 12 |

This should “be” replaced

13 | //= = = = = = = = = = = = = = = = = = = = = = = =// 14 | 15 | 3 16 | //- - - - - - - - -// 17 | **--** *---* a...<< b>> 18 | //- - - - - - - - -// 19 |

a…« b»

20 | //= = = = = = = = = = = = = = = = = = = = = = = =// 21 | -------------------------------------------------------------------------------- /extension/ast/definition_list.go: -------------------------------------------------------------------------------- 1 | package ast 2 | 3 | import ( 4 | gast "github.com/enkogu/goldmark/ast" 5 | ) 6 | 7 | // A DefinitionList struct represents a definition list of Markdown 8 | // (PHPMarkdownExtra) text. 9 | type DefinitionList struct { 10 | gast.BaseBlock 11 | Offset int 12 | TemporaryParagraph *gast.Paragraph 13 | } 14 | 15 | // Dump implements Node.Dump. 16 | func (n *DefinitionList) Dump(source []byte, level int) { 17 | gast.DumpHelper(n, source, level, nil, nil) 18 | } 19 | 20 | // KindDefinitionList is a NodeKind of the DefinitionList node. 21 | var KindDefinitionList = gast.NewNodeKind("DefinitionList") 22 | 23 | // Kind implements Node.Kind. 24 | func (n *DefinitionList) Kind() gast.NodeKind { 25 | return KindDefinitionList 26 | } 27 | 28 | // NewDefinitionList returns a new DefinitionList node. 29 | func NewDefinitionList(offset int, para *gast.Paragraph) *DefinitionList { 30 | return &DefinitionList{ 31 | Offset: offset, 32 | TemporaryParagraph: para, 33 | } 34 | } 35 | 36 | // A DefinitionTerm struct represents a definition list term of Markdown 37 | // (PHPMarkdownExtra) text. 38 | type DefinitionTerm struct { 39 | gast.BaseBlock 40 | } 41 | 42 | // Dump implements Node.Dump. 43 | func (n *DefinitionTerm) Dump(source []byte, level int) { 44 | gast.DumpHelper(n, source, level, nil, nil) 45 | } 46 | 47 | // KindDefinitionTerm is a NodeKind of the DefinitionTerm node. 48 | var KindDefinitionTerm = gast.NewNodeKind("DefinitionTerm") 49 | 50 | // Kind implements Node.Kind. 51 | func (n *DefinitionTerm) Kind() gast.NodeKind { 52 | return KindDefinitionTerm 53 | } 54 | 55 | // NewDefinitionTerm returns a new DefinitionTerm node. 56 | func NewDefinitionTerm() *DefinitionTerm { 57 | return &DefinitionTerm{} 58 | } 59 | 60 | // A DefinitionDescription struct represents a definition list description of Markdown 61 | // (PHPMarkdownExtra) text. 62 | type DefinitionDescription struct { 63 | gast.BaseBlock 64 | IsTight bool 65 | } 66 | 67 | // Dump implements Node.Dump. 68 | func (n *DefinitionDescription) Dump(source []byte, level int) { 69 | gast.DumpHelper(n, source, level, nil, nil) 70 | } 71 | 72 | // KindDefinitionDescription is a NodeKind of the DefinitionDescription node. 73 | var KindDefinitionDescription = gast.NewNodeKind("DefinitionDescription") 74 | 75 | // Kind implements Node.Kind. 76 | func (n *DefinitionDescription) Kind() gast.NodeKind { 77 | return KindDefinitionDescription 78 | } 79 | 80 | // NewDefinitionDescription returns a new DefinitionDescription node. 81 | func NewDefinitionDescription() *DefinitionDescription { 82 | return &DefinitionDescription{} 83 | } 84 | -------------------------------------------------------------------------------- /extension/ast/footnote.go: -------------------------------------------------------------------------------- 1 | package ast 2 | 3 | import ( 4 | "fmt" 5 | 6 | gast "github.com/enkogu/goldmark/ast" 7 | ) 8 | 9 | // A FootnoteLink struct represents a link to a footnote of Markdown 10 | // (PHP Markdown Extra) text. 11 | type FootnoteLink struct { 12 | gast.BaseInline 13 | Index int 14 | } 15 | 16 | // Dump implements Node.Dump. 17 | func (n *FootnoteLink) Dump(source []byte, level int) { 18 | m := map[string]string{} 19 | m["Index"] = fmt.Sprintf("%v", n.Index) 20 | gast.DumpHelper(n, source, level, m, nil) 21 | } 22 | 23 | // KindFootnoteLink is a NodeKind of the FootnoteLink node. 24 | var KindFootnoteLink = gast.NewNodeKind("FootnoteLink") 25 | 26 | // Kind implements Node.Kind. 27 | func (n *FootnoteLink) Kind() gast.NodeKind { 28 | return KindFootnoteLink 29 | } 30 | 31 | // NewFootnoteLink returns a new FootnoteLink node. 32 | func NewFootnoteLink(index int) *FootnoteLink { 33 | return &FootnoteLink{ 34 | Index: index, 35 | } 36 | } 37 | 38 | // A FootnoteBackLink struct represents a link to a footnote of Markdown 39 | // (PHP Markdown Extra) text. 40 | type FootnoteBackLink struct { 41 | gast.BaseInline 42 | Index int 43 | } 44 | 45 | // Dump implements Node.Dump. 46 | func (n *FootnoteBackLink) Dump(source []byte, level int) { 47 | m := map[string]string{} 48 | m["Index"] = fmt.Sprintf("%v", n.Index) 49 | gast.DumpHelper(n, source, level, m, nil) 50 | } 51 | 52 | // KindFootnoteBackLink is a NodeKind of the FootnoteBackLink node. 53 | var KindFootnoteBackLink = gast.NewNodeKind("FootnoteBackLink") 54 | 55 | // Kind implements Node.Kind. 56 | func (n *FootnoteBackLink) Kind() gast.NodeKind { 57 | return KindFootnoteBackLink 58 | } 59 | 60 | // NewFootnoteBackLink returns a new FootnoteBackLink node. 61 | func NewFootnoteBackLink(index int) *FootnoteBackLink { 62 | return &FootnoteBackLink{ 63 | Index: index, 64 | } 65 | } 66 | 67 | // A Footnote struct represents a footnote of Markdown 68 | // (PHP Markdown Extra) text. 69 | type Footnote struct { 70 | gast.BaseBlock 71 | Ref []byte 72 | Index int 73 | } 74 | 75 | // Dump implements Node.Dump. 76 | func (n *Footnote) Dump(source []byte, level int) { 77 | m := map[string]string{} 78 | m["Index"] = fmt.Sprintf("%v", n.Index) 79 | m["Ref"] = fmt.Sprintf("%s", n.Ref) 80 | gast.DumpHelper(n, source, level, m, nil) 81 | } 82 | 83 | // KindFootnote is a NodeKind of the Footnote node. 84 | var KindFootnote = gast.NewNodeKind("Footnote") 85 | 86 | // Kind implements Node.Kind. 87 | func (n *Footnote) Kind() gast.NodeKind { 88 | return KindFootnote 89 | } 90 | 91 | // NewFootnote returns a new Footnote node. 92 | func NewFootnote(ref []byte) *Footnote { 93 | return &Footnote{ 94 | Ref: ref, 95 | Index: -1, 96 | } 97 | } 98 | 99 | // A FootnoteList struct represents footnotes of Markdown 100 | // (PHP Markdown Extra) text. 101 | type FootnoteList struct { 102 | gast.BaseBlock 103 | Count int 104 | } 105 | 106 | // Dump implements Node.Dump. 107 | func (n *FootnoteList) Dump(source []byte, level int) { 108 | m := map[string]string{} 109 | m["Count"] = fmt.Sprintf("%v", n.Count) 110 | gast.DumpHelper(n, source, level, m, nil) 111 | } 112 | 113 | // KindFootnoteList is a NodeKind of the FootnoteList node. 114 | var KindFootnoteList = gast.NewNodeKind("FootnoteList") 115 | 116 | // Kind implements Node.Kind. 117 | func (n *FootnoteList) Kind() gast.NodeKind { 118 | return KindFootnoteList 119 | } 120 | 121 | // NewFootnoteList returns a new FootnoteList node. 122 | func NewFootnoteList() *FootnoteList { 123 | return &FootnoteList{ 124 | Count: 0, 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /extension/ast/strikethrough.go: -------------------------------------------------------------------------------- 1 | // Package ast defines AST nodes that represents extension's elements 2 | package ast 3 | 4 | import ( 5 | gast "github.com/enkogu/goldmark/ast" 6 | ) 7 | 8 | // A Strikethrough struct represents a strikethrough of GFM text. 9 | type Strikethrough struct { 10 | gast.BaseInline 11 | } 12 | 13 | // Dump implements Node.Dump. 14 | func (n *Strikethrough) Dump(source []byte, level int) { 15 | gast.DumpHelper(n, source, level, nil, nil) 16 | } 17 | 18 | // KindStrikethrough is a NodeKind of the Strikethrough node. 19 | var KindStrikethrough = gast.NewNodeKind("Strikethrough") 20 | 21 | // Kind implements Node.Kind. 22 | func (n *Strikethrough) Kind() gast.NodeKind { 23 | return KindStrikethrough 24 | } 25 | 26 | // NewStrikethrough returns a new Strikethrough node. 27 | func NewStrikethrough() *Strikethrough { 28 | return &Strikethrough{} 29 | } 30 | -------------------------------------------------------------------------------- /extension/ast/table.go: -------------------------------------------------------------------------------- 1 | package ast 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | gast "github.com/enkogu/goldmark/ast" 8 | ) 9 | 10 | // Alignment is a text alignment of table cells. 11 | type Alignment int 12 | 13 | const ( 14 | // AlignLeft indicates text should be left justified. 15 | AlignLeft Alignment = iota + 1 16 | 17 | // AlignRight indicates text should be right justified. 18 | AlignRight 19 | 20 | // AlignCenter indicates text should be centered. 21 | AlignCenter 22 | 23 | // AlignNone indicates text should be aligned by default manner. 24 | AlignNone 25 | ) 26 | 27 | func (a Alignment) String() string { 28 | switch a { 29 | case AlignLeft: 30 | return "left" 31 | case AlignRight: 32 | return "right" 33 | case AlignCenter: 34 | return "center" 35 | case AlignNone: 36 | return "none" 37 | } 38 | return "" 39 | } 40 | 41 | // A Table struct represents a table of Markdown(GFM) text. 42 | type Table struct { 43 | gast.BaseBlock 44 | 45 | // Alignments returns alignments of the columns. 46 | Alignments []Alignment 47 | } 48 | 49 | // Dump implements Node.Dump 50 | func (n *Table) Dump(source []byte, level int) { 51 | gast.DumpHelper(n, source, level, nil, func(level int) { 52 | indent := strings.Repeat(" ", level) 53 | fmt.Printf("%sAlignments {\n", indent) 54 | for i, alignment := range n.Alignments { 55 | indent2 := strings.Repeat(" ", level+1) 56 | fmt.Printf("%s%s", indent2, alignment.String()) 57 | if i != len(n.Alignments)-1 { 58 | fmt.Println("") 59 | } 60 | } 61 | fmt.Printf("\n%s}\n", indent) 62 | }) 63 | } 64 | 65 | // KindTable is a NodeKind of the Table node. 66 | var KindTable = gast.NewNodeKind("Table") 67 | 68 | // Kind implements Node.Kind. 69 | func (n *Table) Kind() gast.NodeKind { 70 | return KindTable 71 | } 72 | 73 | // NewTable returns a new Table node. 74 | func NewTable() *Table { 75 | return &Table{ 76 | Alignments: []Alignment{}, 77 | } 78 | } 79 | 80 | // A TableRow struct represents a table row of Markdown(GFM) text. 81 | type TableRow struct { 82 | gast.BaseBlock 83 | Alignments []Alignment 84 | } 85 | 86 | // Dump implements Node.Dump. 87 | func (n *TableRow) Dump(source []byte, level int) { 88 | gast.DumpHelper(n, source, level, nil, nil) 89 | } 90 | 91 | // KindTableRow is a NodeKind of the TableRow node. 92 | var KindTableRow = gast.NewNodeKind("TableRow") 93 | 94 | // Kind implements Node.Kind. 95 | func (n *TableRow) Kind() gast.NodeKind { 96 | return KindTableRow 97 | } 98 | 99 | // NewTableRow returns a new TableRow node. 100 | func NewTableRow(alignments []Alignment) *TableRow { 101 | return &TableRow{} 102 | } 103 | 104 | // A TableHeader struct represents a table header of Markdown(GFM) text. 105 | type TableHeader struct { 106 | gast.BaseBlock 107 | Alignments []Alignment 108 | } 109 | 110 | // KindTableHeader is a NodeKind of the TableHeader node. 111 | var KindTableHeader = gast.NewNodeKind("TableHeader") 112 | 113 | // Kind implements Node.Kind. 114 | func (n *TableHeader) Kind() gast.NodeKind { 115 | return KindTableHeader 116 | } 117 | 118 | // Dump implements Node.Dump. 119 | func (n *TableHeader) Dump(source []byte, level int) { 120 | gast.DumpHelper(n, source, level, nil, nil) 121 | } 122 | 123 | // NewTableHeader returns a new TableHeader node. 124 | func NewTableHeader(row *TableRow) *TableHeader { 125 | n := &TableHeader{} 126 | for c := row.FirstChild(); c != nil; { 127 | next := c.NextSibling() 128 | n.AppendChild(n, c) 129 | c = next 130 | } 131 | return n 132 | } 133 | 134 | // A TableCell struct represents a table cell of a Markdown(GFM) text. 135 | type TableCell struct { 136 | gast.BaseBlock 137 | Alignment Alignment 138 | } 139 | 140 | // Dump implements Node.Dump. 141 | func (n *TableCell) Dump(source []byte, level int) { 142 | gast.DumpHelper(n, source, level, nil, nil) 143 | } 144 | 145 | // KindTableCell is a NodeKind of the TableCell node. 146 | var KindTableCell = gast.NewNodeKind("TableCell") 147 | 148 | // Kind implements Node.Kind. 149 | func (n *TableCell) Kind() gast.NodeKind { 150 | return KindTableCell 151 | } 152 | 153 | // NewTableCell returns a new TableCell node. 154 | func NewTableCell() *TableCell { 155 | return &TableCell{ 156 | Alignment: AlignNone, 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /extension/ast/tasklist.go: -------------------------------------------------------------------------------- 1 | package ast 2 | 3 | import ( 4 | "fmt" 5 | 6 | gast "github.com/enkogu/goldmark/ast" 7 | ) 8 | 9 | // A TaskCheckBox struct represents a checkbox of a task list. 10 | type TaskCheckBox struct { 11 | gast.BaseInline 12 | IsChecked bool 13 | } 14 | 15 | // Dump impelemtns Node.Dump. 16 | func (n *TaskCheckBox) Dump(source []byte, level int) { 17 | m := map[string]string{ 18 | "Checked": fmt.Sprintf("%v", n.IsChecked), 19 | } 20 | gast.DumpHelper(n, source, level, m, nil) 21 | } 22 | 23 | // KindTaskCheckBox is a NodeKind of the TaskCheckBox node. 24 | var KindTaskCheckBox = gast.NewNodeKind("TaskCheckBox") 25 | 26 | // Kind implements Node.Kind. 27 | func (n *TaskCheckBox) Kind() gast.NodeKind { 28 | return KindTaskCheckBox 29 | } 30 | 31 | // NewTaskCheckBox returns a new TaskCheckBox node. 32 | func NewTaskCheckBox(checked bool) *TaskCheckBox { 33 | return &TaskCheckBox{ 34 | IsChecked: checked, 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /extension/definition_list.go: -------------------------------------------------------------------------------- 1 | package extension 2 | 3 | import ( 4 | "github.com/enkogu/goldmark" 5 | gast "github.com/enkogu/goldmark/ast" 6 | "github.com/enkogu/goldmark/extension/ast" 7 | "github.com/enkogu/goldmark/parser" 8 | "github.com/enkogu/goldmark/renderer" 9 | "github.com/enkogu/goldmark/renderer/html" 10 | "github.com/enkogu/goldmark/text" 11 | "github.com/enkogu/goldmark/util" 12 | ) 13 | 14 | type definitionListParser struct { 15 | } 16 | 17 | var defaultDefinitionListParser = &definitionListParser{} 18 | 19 | // NewDefinitionListParser return a new parser.BlockParser that 20 | // can parse PHP Markdown Extra Definition lists. 21 | func NewDefinitionListParser() parser.BlockParser { 22 | return defaultDefinitionListParser 23 | } 24 | 25 | func (b *definitionListParser) Trigger() []byte { 26 | return []byte{':'} 27 | } 28 | 29 | func (b *definitionListParser) Open(parent gast.Node, reader text.Reader, pc parser.Context) (gast.Node, parser.State) { 30 | if _, ok := parent.(*ast.DefinitionList); ok { 31 | return nil, parser.NoChildren 32 | } 33 | line, _ := reader.PeekLine() 34 | pos := pc.BlockOffset() 35 | indent := pc.BlockIndent() 36 | if pos < 0 || line[pos] != ':' || indent != 0 { 37 | return nil, parser.NoChildren 38 | } 39 | 40 | last := parent.LastChild() 41 | // need 1 or more spaces after ':' 42 | w, _ := util.IndentWidth(line[pos+1:], pos+1) 43 | if w < 1 { 44 | return nil, parser.NoChildren 45 | } 46 | if w >= 8 { // starts with indented code 47 | w = 5 48 | } 49 | w += pos + 1 /* 1 = ':' */ 50 | 51 | para, lastIsParagraph := last.(*gast.Paragraph) 52 | var list *ast.DefinitionList 53 | status := parser.HasChildren 54 | var ok bool 55 | if lastIsParagraph { 56 | list, ok = last.PreviousSibling().(*ast.DefinitionList) 57 | if ok { // is not first item 58 | list.Offset = w 59 | list.TemporaryParagraph = para 60 | } else { // is first item 61 | list = ast.NewDefinitionList(w, para) 62 | status |= parser.RequireParagraph 63 | } 64 | } else if list, ok = last.(*ast.DefinitionList); ok { // multiple description 65 | list.Offset = w 66 | list.TemporaryParagraph = nil 67 | } else { 68 | return nil, parser.NoChildren 69 | } 70 | 71 | return list, status 72 | } 73 | 74 | func (b *definitionListParser) Continue(node gast.Node, reader text.Reader, pc parser.Context) parser.State { 75 | line, _ := reader.PeekLine() 76 | if util.IsBlank(line) { 77 | return parser.Continue | parser.HasChildren 78 | } 79 | list, _ := node.(*ast.DefinitionList) 80 | w, _ := util.IndentWidth(line, reader.LineOffset()) 81 | if w < list.Offset { 82 | return parser.Close 83 | } 84 | pos, padding := util.IndentPosition(line, reader.LineOffset(), list.Offset) 85 | reader.AdvanceAndSetPadding(pos, padding) 86 | return parser.Continue | parser.HasChildren 87 | } 88 | 89 | func (b *definitionListParser) Close(node gast.Node, reader text.Reader, pc parser.Context) { 90 | // nothing to do 91 | } 92 | 93 | func (b *definitionListParser) CanInterruptParagraph() bool { 94 | return true 95 | } 96 | 97 | func (b *definitionListParser) CanAcceptIndentedLine() bool { 98 | return false 99 | } 100 | 101 | type definitionDescriptionParser struct { 102 | } 103 | 104 | var defaultDefinitionDescriptionParser = &definitionDescriptionParser{} 105 | 106 | // NewDefinitionDescriptionParser return a new parser.BlockParser that 107 | // can parse definition description starts with ':'. 108 | func NewDefinitionDescriptionParser() parser.BlockParser { 109 | return defaultDefinitionDescriptionParser 110 | } 111 | 112 | func (b *definitionDescriptionParser) Trigger() []byte { 113 | return []byte{':'} 114 | } 115 | 116 | func (b *definitionDescriptionParser) Open(parent gast.Node, reader text.Reader, pc parser.Context) (gast.Node, parser.State) { 117 | line, _ := reader.PeekLine() 118 | pos := pc.BlockOffset() 119 | indent := pc.BlockIndent() 120 | if pos < 0 || line[pos] != ':' || indent != 0 { 121 | return nil, parser.NoChildren 122 | } 123 | list, _ := parent.(*ast.DefinitionList) 124 | if list == nil { 125 | return nil, parser.NoChildren 126 | } 127 | para := list.TemporaryParagraph 128 | list.TemporaryParagraph = nil 129 | if para != nil { 130 | lines := para.Lines() 131 | l := lines.Len() 132 | for i := 0; i < l; i++ { 133 | term := ast.NewDefinitionTerm() 134 | segment := lines.At(i) 135 | term.Lines().Append(segment.TrimRightSpace(reader.Source())) 136 | list.AppendChild(list, term) 137 | } 138 | para.Parent().RemoveChild(para.Parent(), para) 139 | } 140 | cpos, padding := util.IndentPosition(line[pos+1:], pos+1, list.Offset-pos-1) 141 | reader.AdvanceAndSetPadding(cpos, padding) 142 | 143 | return ast.NewDefinitionDescription(), parser.HasChildren 144 | } 145 | 146 | func (b *definitionDescriptionParser) Continue(node gast.Node, reader text.Reader, pc parser.Context) parser.State { 147 | // definitionListParser detects end of the description. 148 | // so this method will never be called. 149 | return parser.Continue | parser.HasChildren 150 | } 151 | 152 | func (b *definitionDescriptionParser) Close(node gast.Node, reader text.Reader, pc parser.Context) { 153 | desc := node.(*ast.DefinitionDescription) 154 | desc.IsTight = !desc.HasBlankPreviousLines() 155 | if desc.IsTight { 156 | for gc := desc.FirstChild(); gc != nil; gc = gc.NextSibling() { 157 | paragraph, ok := gc.(*gast.Paragraph) 158 | if ok { 159 | textBlock := gast.NewTextBlock() 160 | textBlock.SetLines(paragraph.Lines()) 161 | desc.ReplaceChild(desc, paragraph, textBlock) 162 | } 163 | } 164 | } 165 | } 166 | 167 | func (b *definitionDescriptionParser) CanInterruptParagraph() bool { 168 | return true 169 | } 170 | 171 | func (b *definitionDescriptionParser) CanAcceptIndentedLine() bool { 172 | return false 173 | } 174 | 175 | // DefinitionListHTMLRenderer is a renderer.NodeRenderer implementation that 176 | // renders DefinitionList nodes. 177 | type DefinitionListHTMLRenderer struct { 178 | html.Config 179 | } 180 | 181 | // NewDefinitionListHTMLRenderer returns a new DefinitionListHTMLRenderer. 182 | func NewDefinitionListHTMLRenderer(opts ...html.Option) renderer.NodeRenderer { 183 | r := &DefinitionListHTMLRenderer{ 184 | Config: html.NewConfig(), 185 | } 186 | for _, opt := range opts { 187 | opt.SetHTMLOption(&r.Config) 188 | } 189 | return r 190 | } 191 | 192 | // RegisterFuncs implements renderer.NodeRenderer.RegisterFuncs. 193 | func (r *DefinitionListHTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) { 194 | reg.Register(ast.KindDefinitionList, r.renderDefinitionList) 195 | reg.Register(ast.KindDefinitionTerm, r.renderDefinitionTerm) 196 | reg.Register(ast.KindDefinitionDescription, r.renderDefinitionDescription) 197 | } 198 | 199 | func (r *DefinitionListHTMLRenderer) renderDefinitionList(w util.BufWriter, source []byte, n gast.Node, entering bool) (gast.WalkStatus, error) { 200 | if entering { 201 | _, _ = w.WriteString("
\n") 202 | } else { 203 | _, _ = w.WriteString("
\n") 204 | } 205 | return gast.WalkContinue, nil 206 | } 207 | 208 | func (r *DefinitionListHTMLRenderer) renderDefinitionTerm(w util.BufWriter, source []byte, n gast.Node, entering bool) (gast.WalkStatus, error) { 209 | if entering { 210 | _, _ = w.WriteString("
") 211 | } else { 212 | _, _ = w.WriteString("
\n") 213 | } 214 | return gast.WalkContinue, nil 215 | } 216 | 217 | func (r *DefinitionListHTMLRenderer) renderDefinitionDescription(w util.BufWriter, source []byte, node gast.Node, entering bool) (gast.WalkStatus, error) { 218 | if entering { 219 | n := node.(*ast.DefinitionDescription) 220 | if n.IsTight { 221 | _, _ = w.WriteString("
") 222 | } else { 223 | _, _ = w.WriteString("
\n") 224 | } 225 | } else { 226 | _, _ = w.WriteString("
\n") 227 | } 228 | return gast.WalkContinue, nil 229 | } 230 | 231 | type definitionList struct { 232 | } 233 | 234 | // DefinitionList is an extension that allow you to use PHP Markdown Extra Definition lists. 235 | var DefinitionList = &definitionList{} 236 | 237 | func (e *definitionList) Extend(m goldmark.Markdown) { 238 | m.Parser().AddOptions(parser.WithBlockParsers( 239 | util.Prioritized(NewDefinitionListParser(), 101), 240 | util.Prioritized(NewDefinitionDescriptionParser(), 102), 241 | )) 242 | m.Renderer().AddOptions(renderer.WithNodeRenderers( 243 | util.Prioritized(NewDefinitionListHTMLRenderer(), 500), 244 | )) 245 | } 246 | -------------------------------------------------------------------------------- /extension/definition_list_test.go: -------------------------------------------------------------------------------- 1 | package extension 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/enkogu/goldmark" 7 | "github.com/enkogu/goldmark/renderer/html" 8 | "github.com/enkogu/goldmark/testutil" 9 | ) 10 | 11 | func TestDefinitionList(t *testing.T) { 12 | markdown := goldmark.New( 13 | goldmark.WithRendererOptions( 14 | html.WithUnsafe(), 15 | ), 16 | goldmark.WithExtensions( 17 | DefinitionList, 18 | ), 19 | ) 20 | testutil.DoTestCaseFile(markdown, "_test/definition_list.txt", t) 21 | } 22 | -------------------------------------------------------------------------------- /extension/footnote.go: -------------------------------------------------------------------------------- 1 | package extension 2 | 3 | import ( 4 | "bytes" 5 | "strconv" 6 | 7 | "github.com/enkogu/goldmark" 8 | gast "github.com/enkogu/goldmark/ast" 9 | "github.com/enkogu/goldmark/extension/ast" 10 | "github.com/enkogu/goldmark/parser" 11 | "github.com/enkogu/goldmark/renderer" 12 | "github.com/enkogu/goldmark/renderer/html" 13 | "github.com/enkogu/goldmark/text" 14 | "github.com/enkogu/goldmark/util" 15 | ) 16 | 17 | var footnoteListKey = parser.NewContextKey() 18 | 19 | type footnoteBlockParser struct { 20 | } 21 | 22 | var defaultFootnoteBlockParser = &footnoteBlockParser{} 23 | 24 | // NewFootnoteBlockParser returns a new parser.BlockParser that can parse 25 | // footnotes of the Markdown(PHP Markdown Extra) text. 26 | func NewFootnoteBlockParser() parser.BlockParser { 27 | return defaultFootnoteBlockParser 28 | } 29 | 30 | func (b *footnoteBlockParser) Trigger() []byte { 31 | return []byte{'['} 32 | } 33 | 34 | func (b *footnoteBlockParser) Open(parent gast.Node, reader text.Reader, pc parser.Context) (gast.Node, parser.State) { 35 | line, segment := reader.PeekLine() 36 | pos := pc.BlockOffset() 37 | if pos < 0 || line[pos] != '[' { 38 | return nil, parser.NoChildren 39 | } 40 | pos++ 41 | if pos > len(line)-1 || line[pos] != '^' { 42 | return nil, parser.NoChildren 43 | } 44 | open := pos + 1 45 | closes := 0 46 | closure := util.FindClosure(line[pos+1:], '[', ']', false, false) 47 | closes = pos + 1 + closure 48 | next := closes + 1 49 | if closure > -1 { 50 | if next >= len(line) || line[next] != ':' { 51 | return nil, parser.NoChildren 52 | } 53 | } else { 54 | return nil, parser.NoChildren 55 | } 56 | padding := segment.Padding 57 | label := reader.Value(text.NewSegment(segment.Start+open-padding, segment.Start+closes-padding)) 58 | if util.IsBlank(label) { 59 | return nil, parser.NoChildren 60 | } 61 | item := ast.NewFootnote(label) 62 | 63 | pos = next + 1 - padding 64 | if pos >= len(line) { 65 | reader.Advance(pos) 66 | return item, parser.NoChildren 67 | } 68 | reader.AdvanceAndSetPadding(pos, padding) 69 | return item, parser.HasChildren 70 | } 71 | 72 | func (b *footnoteBlockParser) Continue(node gast.Node, reader text.Reader, pc parser.Context) parser.State { 73 | line, _ := reader.PeekLine() 74 | if util.IsBlank(line) { 75 | return parser.Continue | parser.HasChildren 76 | } 77 | childpos, padding := util.IndentPosition(line, reader.LineOffset(), 4) 78 | if childpos < 0 { 79 | return parser.Close 80 | } 81 | reader.AdvanceAndSetPadding(childpos, padding) 82 | return parser.Continue | parser.HasChildren 83 | } 84 | 85 | func (b *footnoteBlockParser) Close(node gast.Node, reader text.Reader, pc parser.Context) { 86 | var list *ast.FootnoteList 87 | if tlist := pc.Get(footnoteListKey); tlist != nil { 88 | list = tlist.(*ast.FootnoteList) 89 | } else { 90 | list = ast.NewFootnoteList() 91 | pc.Set(footnoteListKey, list) 92 | node.Parent().InsertBefore(node.Parent(), node, list) 93 | } 94 | node.Parent().RemoveChild(node.Parent(), node) 95 | list.AppendChild(list, node) 96 | } 97 | 98 | func (b *footnoteBlockParser) CanInterruptParagraph() bool { 99 | return true 100 | } 101 | 102 | func (b *footnoteBlockParser) CanAcceptIndentedLine() bool { 103 | return false 104 | } 105 | 106 | type footnoteParser struct { 107 | } 108 | 109 | var defaultFootnoteParser = &footnoteParser{} 110 | 111 | // NewFootnoteParser returns a new parser.InlineParser that can parse 112 | // footnote links of the Markdown(PHP Markdown Extra) text. 113 | func NewFootnoteParser() parser.InlineParser { 114 | return defaultFootnoteParser 115 | } 116 | 117 | func (s *footnoteParser) Trigger() []byte { 118 | return []byte{'['} 119 | } 120 | 121 | func (s *footnoteParser) Parse(parent gast.Node, block text.Reader, pc parser.Context) gast.Node { 122 | line, segment := block.PeekLine() 123 | pos := 1 124 | if pos >= len(line) || line[pos] != '^' { 125 | return nil 126 | } 127 | pos++ 128 | if pos >= len(line) { 129 | return nil 130 | } 131 | open := pos 132 | closure := util.FindClosure(line[pos:], '[', ']', false, false) 133 | if closure < 0 { 134 | return nil 135 | } 136 | closes := pos + closure 137 | value := block.Value(text.NewSegment(segment.Start+open, segment.Start+closes)) 138 | block.Advance(closes + 1) 139 | 140 | var list *ast.FootnoteList 141 | if tlist := pc.Get(footnoteListKey); tlist != nil { 142 | list = tlist.(*ast.FootnoteList) 143 | } 144 | if list == nil { 145 | return nil 146 | } 147 | index := 0 148 | for def := list.FirstChild(); def != nil; def = def.NextSibling() { 149 | d := def.(*ast.Footnote) 150 | if bytes.Equal(d.Ref, value) { 151 | if d.Index < 0 { 152 | list.Count += 1 153 | d.Index = list.Count 154 | } 155 | index = d.Index 156 | break 157 | } 158 | } 159 | if index == 0 { 160 | return nil 161 | } 162 | 163 | return ast.NewFootnoteLink(index) 164 | } 165 | 166 | type footnoteASTTransformer struct { 167 | } 168 | 169 | var defaultFootnoteASTTransformer = &footnoteASTTransformer{} 170 | 171 | // NewFootnoteASTTransformer returns a new parser.ASTTransformer that 172 | // insert a footnote list to the last of the document. 173 | func NewFootnoteASTTransformer() parser.ASTTransformer { 174 | return defaultFootnoteASTTransformer 175 | } 176 | 177 | func (a *footnoteASTTransformer) Transform(node *gast.Document, reader text.Reader, pc parser.Context) { 178 | var list *ast.FootnoteList 179 | if tlist := pc.Get(footnoteListKey); tlist != nil { 180 | list = tlist.(*ast.FootnoteList) 181 | } else { 182 | return 183 | } 184 | pc.Set(footnoteListKey, nil) 185 | for footnote := list.FirstChild(); footnote != nil; { 186 | var container gast.Node = footnote 187 | next := footnote.NextSibling() 188 | if fc := container.LastChild(); fc != nil && gast.IsParagraph(fc) { 189 | container = fc 190 | } 191 | index := footnote.(*ast.Footnote).Index 192 | if index < 0 { 193 | list.RemoveChild(list, footnote) 194 | } else { 195 | container.AppendChild(container, ast.NewFootnoteBackLink(index)) 196 | } 197 | footnote = next 198 | } 199 | list.SortChildren(func(n1, n2 gast.Node) int { 200 | if n1.(*ast.Footnote).Index < n2.(*ast.Footnote).Index { 201 | return -1 202 | } 203 | return 1 204 | }) 205 | if list.Count <= 0 { 206 | list.Parent().RemoveChild(list.Parent(), list) 207 | return 208 | } 209 | 210 | node.AppendChild(node, list) 211 | } 212 | 213 | // FootnoteHTMLRenderer is a renderer.NodeRenderer implementation that 214 | // renders FootnoteLink nodes. 215 | type FootnoteHTMLRenderer struct { 216 | html.Config 217 | } 218 | 219 | // NewFootnoteHTMLRenderer returns a new FootnoteHTMLRenderer. 220 | func NewFootnoteHTMLRenderer(opts ...html.Option) renderer.NodeRenderer { 221 | r := &FootnoteHTMLRenderer{ 222 | Config: html.NewConfig(), 223 | } 224 | for _, opt := range opts { 225 | opt.SetHTMLOption(&r.Config) 226 | } 227 | return r 228 | } 229 | 230 | // RegisterFuncs implements renderer.NodeRenderer.RegisterFuncs. 231 | func (r *FootnoteHTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) { 232 | reg.Register(ast.KindFootnoteLink, r.renderFootnoteLink) 233 | reg.Register(ast.KindFootnoteBackLink, r.renderFootnoteBackLink) 234 | reg.Register(ast.KindFootnote, r.renderFootnote) 235 | reg.Register(ast.KindFootnoteList, r.renderFootnoteList) 236 | } 237 | 238 | func (r *FootnoteHTMLRenderer) renderFootnoteLink(w util.BufWriter, source []byte, node gast.Node, entering bool) (gast.WalkStatus, error) { 239 | if entering { 240 | n := node.(*ast.FootnoteLink) 241 | is := strconv.Itoa(n.Index) 242 | _, _ = w.WriteString(``) 247 | _, _ = w.WriteString(is) 248 | _, _ = w.WriteString(``) 249 | } 250 | return gast.WalkContinue, nil 251 | } 252 | 253 | func (r *FootnoteHTMLRenderer) renderFootnoteBackLink(w util.BufWriter, source []byte, node gast.Node, entering bool) (gast.WalkStatus, error) { 254 | if entering { 255 | n := node.(*ast.FootnoteBackLink) 256 | is := strconv.Itoa(n.Index) 257 | _, _ = w.WriteString(` `) 260 | _, _ = w.WriteString("↩︎") 261 | _, _ = w.WriteString(``) 262 | } 263 | return gast.WalkContinue, nil 264 | } 265 | 266 | func (r *FootnoteHTMLRenderer) renderFootnote(w util.BufWriter, source []byte, node gast.Node, entering bool) (gast.WalkStatus, error) { 267 | n := node.(*ast.Footnote) 268 | is := strconv.Itoa(n.Index) 269 | if entering { 270 | _, _ = w.WriteString(`
  • `) 273 | _, _ = w.WriteString("\n") 274 | } else { 275 | _, _ = w.WriteString("
  • \n") 276 | } 277 | return gast.WalkContinue, nil 278 | } 279 | 280 | func (r *FootnoteHTMLRenderer) renderFootnoteList(w util.BufWriter, source []byte, node gast.Node, entering bool) (gast.WalkStatus, error) { 281 | tag := "section" 282 | if r.Config.XHTML { 283 | tag = "div" 284 | } 285 | if entering { 286 | _, _ = w.WriteString("<") 287 | _, _ = w.WriteString(tag) 288 | _, _ = w.WriteString(` class="footnotes" role="doc-endnotes">`) 289 | if r.Config.XHTML { 290 | _, _ = w.WriteString("\n
    \n") 291 | } else { 292 | _, _ = w.WriteString("\n
    \n") 293 | } 294 | _, _ = w.WriteString("
      \n") 295 | } else { 296 | _, _ = w.WriteString("
    \n") 297 | _, _ = w.WriteString("\n") 300 | } 301 | return gast.WalkContinue, nil 302 | } 303 | 304 | type footnote struct { 305 | } 306 | 307 | // Footnote is an extension that allow you to use PHP Markdown Extra Footnotes. 308 | var Footnote = &footnote{} 309 | 310 | func (e *footnote) Extend(m goldmark.Markdown) { 311 | m.Parser().AddOptions( 312 | parser.WithBlockParsers( 313 | util.Prioritized(NewFootnoteBlockParser(), 999), 314 | ), 315 | parser.WithInlineParsers( 316 | util.Prioritized(NewFootnoteParser(), 101), 317 | ), 318 | parser.WithASTTransformers( 319 | util.Prioritized(NewFootnoteASTTransformer(), 999), 320 | ), 321 | ) 322 | m.Renderer().AddOptions(renderer.WithNodeRenderers( 323 | util.Prioritized(NewFootnoteHTMLRenderer(), 500), 324 | )) 325 | } 326 | -------------------------------------------------------------------------------- /extension/footnote_test.go: -------------------------------------------------------------------------------- 1 | package extension 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/enkogu/goldmark" 7 | "github.com/enkogu/goldmark/renderer/html" 8 | "github.com/enkogu/goldmark/testutil" 9 | ) 10 | 11 | func TestFootnote(t *testing.T) { 12 | markdown := goldmark.New( 13 | goldmark.WithRendererOptions( 14 | html.WithUnsafe(), 15 | ), 16 | goldmark.WithExtensions( 17 | Footnote, 18 | ), 19 | ) 20 | testutil.DoTestCaseFile(markdown, "_test/footnote.txt", t) 21 | } 22 | -------------------------------------------------------------------------------- /extension/gfm.go: -------------------------------------------------------------------------------- 1 | package extension 2 | 3 | import ( 4 | "github.com/enkogu/goldmark" 5 | ) 6 | 7 | type gfm struct { 8 | } 9 | 10 | // GFM is an extension that provides Github Flavored markdown functionalities. 11 | var GFM = &gfm{} 12 | 13 | func (e *gfm) Extend(m goldmark.Markdown) { 14 | Linkify.Extend(m) 15 | Table.Extend(m) 16 | Strikethrough.Extend(m) 17 | TaskList.Extend(m) 18 | } 19 | -------------------------------------------------------------------------------- /extension/linkify.go: -------------------------------------------------------------------------------- 1 | package extension 2 | 3 | import ( 4 | "bytes" 5 | "regexp" 6 | 7 | "github.com/enkogu/goldmark" 8 | "github.com/enkogu/goldmark/ast" 9 | "github.com/enkogu/goldmark/parser" 10 | "github.com/enkogu/goldmark/text" 11 | "github.com/enkogu/goldmark/util" 12 | ) 13 | 14 | var wwwURLRegxp = regexp.MustCompile(`^www\.[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b(?:[-a-zA-Z0-9@:%_\+.~#?&//=\(\);,'"]*)`) 15 | 16 | var urlRegexp = regexp.MustCompile(`^(?:http|https|ftp):\/\/(?:www\.)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=\(\);,'"]*)`) 17 | 18 | type linkifyParser struct { 19 | } 20 | 21 | var defaultLinkifyParser = &linkifyParser{} 22 | 23 | // NewLinkifyParser return a new InlineParser can parse 24 | // text that seems like a URL. 25 | func NewLinkifyParser() parser.InlineParser { 26 | return defaultLinkifyParser 27 | } 28 | 29 | func (s *linkifyParser) Trigger() []byte { 30 | // ' ' indicates any white spaces and a line head 31 | return []byte{' ', '*', '_', '~', '('} 32 | } 33 | 34 | var protoHTTP = []byte("http:") 35 | var protoHTTPS = []byte("https:") 36 | var protoFTP = []byte("ftp:") 37 | var domainWWW = []byte("www.") 38 | 39 | func (s *linkifyParser) Parse(parent ast.Node, block text.Reader, pc parser.Context) ast.Node { 40 | line, segment := block.PeekLine() 41 | consumes := 0 42 | start := segment.Start 43 | c := line[0] 44 | // advance if current position is not a line head. 45 | if c == ' ' || c == '*' || c == '_' || c == '~' || c == '(' { 46 | consumes++ 47 | start++ 48 | line = line[1:] 49 | } 50 | 51 | var m []int 52 | var protocol []byte 53 | var typ ast.AutoLinkType = ast.AutoLinkURL 54 | if bytes.HasPrefix(line, protoHTTP) || bytes.HasPrefix(line, protoHTTPS) || bytes.HasPrefix(line, protoFTP) { 55 | m = urlRegexp.FindSubmatchIndex(line) 56 | } 57 | if m == nil && bytes.HasPrefix(line, domainWWW) { 58 | m = wwwURLRegxp.FindSubmatchIndex(line) 59 | protocol = []byte("http") 60 | } 61 | if m != nil { 62 | lastChar := line[m[1]-1] 63 | if lastChar == '.' { 64 | m[1]-- 65 | } else if lastChar == ')' { 66 | closing := 0 67 | for i := m[1] - 1; i >= m[0]; i-- { 68 | if line[i] == ')' { 69 | closing++ 70 | } else if line[i] == '(' { 71 | closing-- 72 | } 73 | } 74 | if closing > 0 { 75 | m[1] -= closing 76 | } 77 | } else if lastChar == ';' { 78 | i := m[1] - 2 79 | for ; i >= m[0]; i-- { 80 | if util.IsAlphaNumeric(line[i]) { 81 | continue 82 | } 83 | break 84 | } 85 | if i != m[1]-2 { 86 | if line[i] == '&' { 87 | m[1] -= m[1] - i 88 | } 89 | } 90 | } 91 | } 92 | if m == nil { 93 | typ = ast.AutoLinkEmail 94 | stop := util.FindEmailIndex(line) 95 | if stop < 0 { 96 | return nil 97 | } 98 | at := bytes.IndexByte(line, '@') 99 | m = []int{0, stop, at, stop - 1} 100 | if m == nil || bytes.IndexByte(line[m[2]:m[3]], '.') < 0 { 101 | return nil 102 | } 103 | lastChar := line[m[1]-1] 104 | if lastChar == '.' { 105 | m[1]-- 106 | } 107 | if m[1] < len(line) { 108 | nextChar := line[m[1]] 109 | if nextChar == '-' || nextChar == '_' { 110 | return nil 111 | } 112 | } 113 | } 114 | if m == nil { 115 | return nil 116 | } 117 | if consumes != 0 { 118 | s := segment.WithStop(segment.Start + 1) 119 | ast.MergeOrAppendTextSegment(parent, s) 120 | } 121 | consumes += m[1] 122 | block.Advance(consumes) 123 | n := ast.NewTextSegment(text.NewSegment(start, start+m[1])) 124 | link := ast.NewAutoLink(typ, n) 125 | link.Protocol = protocol 126 | return link 127 | } 128 | 129 | func (s *linkifyParser) CloseBlock(parent ast.Node, pc parser.Context) { 130 | // nothing to do 131 | } 132 | 133 | type linkify struct { 134 | } 135 | 136 | type linkifyASTTransformer struct { 137 | } 138 | 139 | var defaultLinkifyASTTransformer = &linkifyASTTransformer{} 140 | 141 | // NewLinkifyASTTransformer returns a new parser.ASTTransformer that 142 | // is related to AutoLink. 143 | func NewLinkifyASTTransformer() parser.ASTTransformer { 144 | return defaultLinkifyASTTransformer 145 | } 146 | 147 | func (a *linkifyASTTransformer) Transform(node *ast.Document, reader text.Reader, pc parser.Context) { 148 | var autoLinks []*ast.AutoLink 149 | _ = ast.Walk(node, func(n ast.Node, entering bool) (ast.WalkStatus, error) { 150 | if entering { 151 | if autoLink, ok := n.(*ast.AutoLink); ok { 152 | autoLinks = append(autoLinks, autoLink) 153 | } 154 | } 155 | return ast.WalkContinue, nil 156 | 157 | }) 158 | for _, autoLink := range autoLinks { 159 | nested := false 160 | for p := autoLink.Parent(); p != nil; p = p.Parent() { 161 | if _, ok := p.(*ast.Link); ok { 162 | nested = true 163 | break 164 | } 165 | } 166 | if nested { 167 | parent := autoLink.Parent() 168 | parent.ReplaceChild(parent, autoLink, ast.NewString(autoLink.Label(reader.Source()))) 169 | } 170 | } 171 | } 172 | 173 | // Linkify is an extension that allow you to parse text that seems like a URL. 174 | var Linkify = &linkify{} 175 | 176 | func (e *linkify) Extend(m goldmark.Markdown) { 177 | m.Parser().AddOptions( 178 | parser.WithInlineParsers( 179 | util.Prioritized(NewLinkifyParser(), 999), 180 | ), 181 | parser.WithASTTransformers( 182 | util.Prioritized(NewLinkifyASTTransformer(), 999), 183 | ), 184 | ) 185 | } 186 | -------------------------------------------------------------------------------- /extension/linkify_test.go: -------------------------------------------------------------------------------- 1 | package extension 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/enkogu/goldmark" 7 | "github.com/enkogu/goldmark/renderer/html" 8 | "github.com/enkogu/goldmark/testutil" 9 | ) 10 | 11 | func TestLinkify(t *testing.T) { 12 | markdown := goldmark.New( 13 | goldmark.WithRendererOptions( 14 | html.WithUnsafe(), 15 | ), 16 | goldmark.WithExtensions( 17 | Linkify, 18 | ), 19 | ) 20 | testutil.DoTestCaseFile(markdown, "_test/linkify.txt", t) 21 | } 22 | -------------------------------------------------------------------------------- /extension/strikethrough.go: -------------------------------------------------------------------------------- 1 | package extension 2 | 3 | import ( 4 | "github.com/enkogu/goldmark" 5 | gast "github.com/enkogu/goldmark/ast" 6 | "github.com/enkogu/goldmark/extension/ast" 7 | "github.com/enkogu/goldmark/parser" 8 | "github.com/enkogu/goldmark/renderer" 9 | "github.com/enkogu/goldmark/renderer/html" 10 | "github.com/enkogu/goldmark/text" 11 | "github.com/enkogu/goldmark/util" 12 | ) 13 | 14 | type strikethroughDelimiterProcessor struct { 15 | } 16 | 17 | func (p *strikethroughDelimiterProcessor) IsDelimiter(b byte) bool { 18 | return b == '~' 19 | } 20 | 21 | func (p *strikethroughDelimiterProcessor) CanOpenCloser(opener, closer *parser.Delimiter) bool { 22 | return opener.Char == closer.Char 23 | } 24 | 25 | func (p *strikethroughDelimiterProcessor) OnMatch(consumes int) gast.Node { 26 | return ast.NewStrikethrough() 27 | } 28 | 29 | var defaultStrikethroughDelimiterProcessor = &strikethroughDelimiterProcessor{} 30 | 31 | type strikethroughParser struct { 32 | } 33 | 34 | var defaultStrikethroughParser = &strikethroughParser{} 35 | 36 | // NewStrikethroughParser return a new InlineParser that parses 37 | // strikethrough expressions. 38 | func NewStrikethroughParser() parser.InlineParser { 39 | return defaultStrikethroughParser 40 | } 41 | 42 | func (s *strikethroughParser) Trigger() []byte { 43 | return []byte{'~'} 44 | } 45 | 46 | func (s *strikethroughParser) Parse(parent gast.Node, block text.Reader, pc parser.Context) gast.Node { 47 | before := block.PrecendingCharacter() 48 | line, segment := block.PeekLine() 49 | node := parser.ScanDelimiter(line, before, 2, defaultStrikethroughDelimiterProcessor) 50 | if node == nil { 51 | return nil 52 | } 53 | node.Segment = segment.WithStop(segment.Start + node.OriginalLength) 54 | block.Advance(node.OriginalLength) 55 | pc.PushDelimiter(node) 56 | return node 57 | } 58 | 59 | func (s *strikethroughParser) CloseBlock(parent gast.Node, pc parser.Context) { 60 | // nothing to do 61 | } 62 | 63 | // StrikethroughHTMLRenderer is a renderer.NodeRenderer implementation that 64 | // renders Strikethrough nodes. 65 | type StrikethroughHTMLRenderer struct { 66 | html.Config 67 | } 68 | 69 | // NewStrikethroughHTMLRenderer returns a new StrikethroughHTMLRenderer. 70 | func NewStrikethroughHTMLRenderer(opts ...html.Option) renderer.NodeRenderer { 71 | r := &StrikethroughHTMLRenderer{ 72 | Config: html.NewConfig(), 73 | } 74 | for _, opt := range opts { 75 | opt.SetHTMLOption(&r.Config) 76 | } 77 | return r 78 | } 79 | 80 | // RegisterFuncs implements renderer.NodeRenderer.RegisterFuncs. 81 | func (r *StrikethroughHTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) { 82 | reg.Register(ast.KindStrikethrough, r.renderStrikethrough) 83 | } 84 | 85 | func (r *StrikethroughHTMLRenderer) renderStrikethrough(w util.BufWriter, source []byte, n gast.Node, entering bool) (gast.WalkStatus, error) { 86 | if entering { 87 | w.WriteString("") 88 | } else { 89 | w.WriteString("") 90 | } 91 | return gast.WalkContinue, nil 92 | } 93 | 94 | type strikethrough struct { 95 | } 96 | 97 | // Strikethrough is an extension that allow you to use strikethrough expression like '~~text~~' . 98 | var Strikethrough = &strikethrough{} 99 | 100 | func (e *strikethrough) Extend(m goldmark.Markdown) { 101 | m.Parser().AddOptions(parser.WithInlineParsers( 102 | util.Prioritized(NewStrikethroughParser(), 500), 103 | )) 104 | m.Renderer().AddOptions(renderer.WithNodeRenderers( 105 | util.Prioritized(NewStrikethroughHTMLRenderer(), 500), 106 | )) 107 | } 108 | -------------------------------------------------------------------------------- /extension/strikethrough_test.go: -------------------------------------------------------------------------------- 1 | package extension 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/enkogu/goldmark" 7 | "github.com/enkogu/goldmark/renderer/html" 8 | "github.com/enkogu/goldmark/testutil" 9 | ) 10 | 11 | func TestStrikethrough(t *testing.T) { 12 | markdown := goldmark.New( 13 | goldmark.WithRendererOptions( 14 | html.WithUnsafe(), 15 | ), 16 | goldmark.WithExtensions( 17 | Strikethrough, 18 | ), 19 | ) 20 | testutil.DoTestCaseFile(markdown, "_test/strikethrough.txt", t) 21 | } 22 | -------------------------------------------------------------------------------- /extension/table.go: -------------------------------------------------------------------------------- 1 | package extension 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "regexp" 7 | 8 | "github.com/enkogu/goldmark" 9 | gast "github.com/enkogu/goldmark/ast" 10 | "github.com/enkogu/goldmark/extension/ast" 11 | "github.com/enkogu/goldmark/parser" 12 | "github.com/enkogu/goldmark/renderer" 13 | "github.com/enkogu/goldmark/renderer/html" 14 | "github.com/enkogu/goldmark/text" 15 | "github.com/enkogu/goldmark/util" 16 | ) 17 | 18 | var tableDelimRegexp = regexp.MustCompile(`^[\s\-\|\:]+$`) 19 | var tableDelimLeft = regexp.MustCompile(`^\s*\:\-+\s*$`) 20 | var tableDelimRight = regexp.MustCompile(`^\s*\-+\:\s*$`) 21 | var tableDelimCenter = regexp.MustCompile(`^\s*\:\-+\:\s*$`) 22 | var tableDelimNone = regexp.MustCompile(`^\s*\-+\s*$`) 23 | 24 | type tableParagraphTransformer struct { 25 | } 26 | 27 | var defaultTableParagraphTransformer = &tableParagraphTransformer{} 28 | 29 | // NewTableParagraphTransformer returns a new ParagraphTransformer 30 | // that can transform pargraphs into tables. 31 | func NewTableParagraphTransformer() parser.ParagraphTransformer { 32 | return defaultTableParagraphTransformer 33 | } 34 | 35 | func (b *tableParagraphTransformer) Transform(node *gast.Paragraph, reader text.Reader, pc parser.Context) { 36 | lines := node.Lines() 37 | if lines.Len() < 2 { 38 | return 39 | } 40 | alignments := b.parseDelimiter(lines.At(1), reader) 41 | if alignments == nil { 42 | return 43 | } 44 | header := b.parseRow(lines.At(0), alignments, true, reader) 45 | if header == nil || len(alignments) != header.ChildCount() { 46 | return 47 | } 48 | table := ast.NewTable() 49 | table.Alignments = alignments 50 | table.AppendChild(table, ast.NewTableHeader(header)) 51 | for i := 2; i < lines.Len(); i++ { 52 | table.AppendChild(table, b.parseRow(lines.At(i), alignments, false, reader)) 53 | } 54 | node.Parent().InsertBefore(node.Parent(), node, table) 55 | node.Parent().RemoveChild(node.Parent(), node) 56 | } 57 | 58 | func (b *tableParagraphTransformer) parseRow(segment text.Segment, alignments []ast.Alignment, isHeader bool, reader text.Reader) *ast.TableRow { 59 | source := reader.Source() 60 | line := segment.Value(source) 61 | pos := 0 62 | pos += util.TrimLeftSpaceLength(line) 63 | limit := len(line) 64 | limit -= util.TrimRightSpaceLength(line) 65 | row := ast.NewTableRow(alignments) 66 | if len(line) > 0 && line[pos] == '|' { 67 | pos++ 68 | } 69 | if len(line) > 0 && line[limit-1] == '|' { 70 | limit-- 71 | } 72 | i := 0 73 | for ; pos < limit; i++ { 74 | alignment := ast.AlignNone 75 | if i >= len(alignments) { 76 | if !isHeader { 77 | return row 78 | } 79 | } else { 80 | alignment = alignments[i] 81 | } 82 | closure := util.FindClosure(line[pos:], byte(0), '|', true, false) 83 | if closure < 0 { 84 | closure = len(line[pos:]) 85 | } 86 | node := ast.NewTableCell() 87 | seg := text.NewSegment(segment.Start+pos, segment.Start+pos+closure) 88 | seg = seg.TrimLeftSpace(source) 89 | seg = seg.TrimRightSpace(source) 90 | node.Lines().Append(seg) 91 | node.Alignment = alignment 92 | row.AppendChild(row, node) 93 | pos += closure + 1 94 | } 95 | for ; i < len(alignments); i++ { 96 | row.AppendChild(row, ast.NewTableCell()) 97 | } 98 | return row 99 | } 100 | 101 | func (b *tableParagraphTransformer) parseDelimiter(segment text.Segment, reader text.Reader) []ast.Alignment { 102 | line := segment.Value(reader.Source()) 103 | if !tableDelimRegexp.Match(line) { 104 | return nil 105 | } 106 | cols := bytes.Split(line, []byte{'|'}) 107 | if util.IsBlank(cols[0]) { 108 | cols = cols[1:] 109 | } 110 | if len(cols) > 0 && util.IsBlank(cols[len(cols)-1]) { 111 | cols = cols[:len(cols)-1] 112 | } 113 | 114 | var alignments []ast.Alignment 115 | for _, col := range cols { 116 | if tableDelimLeft.Match(col) { 117 | alignments = append(alignments, ast.AlignLeft) 118 | } else if tableDelimRight.Match(col) { 119 | alignments = append(alignments, ast.AlignRight) 120 | } else if tableDelimCenter.Match(col) { 121 | alignments = append(alignments, ast.AlignCenter) 122 | } else if tableDelimNone.Match(col) { 123 | alignments = append(alignments, ast.AlignNone) 124 | } else { 125 | return nil 126 | } 127 | } 128 | return alignments 129 | } 130 | 131 | // TableHTMLRenderer is a renderer.NodeRenderer implementation that 132 | // renders Table nodes. 133 | type TableHTMLRenderer struct { 134 | html.Config 135 | } 136 | 137 | // NewTableHTMLRenderer returns a new TableHTMLRenderer. 138 | func NewTableHTMLRenderer(opts ...html.Option) renderer.NodeRenderer { 139 | r := &TableHTMLRenderer{ 140 | Config: html.NewConfig(), 141 | } 142 | for _, opt := range opts { 143 | opt.SetHTMLOption(&r.Config) 144 | } 145 | return r 146 | } 147 | 148 | // RegisterFuncs implements renderer.NodeRenderer.RegisterFuncs. 149 | func (r *TableHTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) { 150 | reg.Register(ast.KindTable, r.renderTable) 151 | reg.Register(ast.KindTableHeader, r.renderTableHeader) 152 | reg.Register(ast.KindTableRow, r.renderTableRow) 153 | reg.Register(ast.KindTableCell, r.renderTableCell) 154 | } 155 | 156 | func (r *TableHTMLRenderer) renderTable(w util.BufWriter, source []byte, n gast.Node, entering bool) (gast.WalkStatus, error) { 157 | if entering { 158 | _, _ = w.WriteString("\n") 159 | } else { 160 | _, _ = w.WriteString("
    \n") 161 | } 162 | return gast.WalkContinue, nil 163 | } 164 | 165 | func (r *TableHTMLRenderer) renderTableHeader(w util.BufWriter, source []byte, n gast.Node, entering bool) (gast.WalkStatus, error) { 166 | if entering { 167 | _, _ = w.WriteString("\n") 168 | _, _ = w.WriteString("\n") 169 | } else { 170 | _, _ = w.WriteString("\n") 171 | _, _ = w.WriteString("\n") 172 | if n.NextSibling() != nil { 173 | _, _ = w.WriteString("\n") 174 | } 175 | } 176 | return gast.WalkContinue, nil 177 | } 178 | 179 | func (r *TableHTMLRenderer) renderTableRow(w util.BufWriter, source []byte, n gast.Node, entering bool) (gast.WalkStatus, error) { 180 | if entering { 181 | _, _ = w.WriteString("\n") 182 | } else { 183 | _, _ = w.WriteString("\n") 184 | if n.Parent().LastChild() == n { 185 | _, _ = w.WriteString("\n") 186 | } 187 | } 188 | return gast.WalkContinue, nil 189 | } 190 | 191 | func (r *TableHTMLRenderer) renderTableCell(w util.BufWriter, source []byte, node gast.Node, entering bool) (gast.WalkStatus, error) { 192 | n := node.(*ast.TableCell) 193 | tag := "td" 194 | if n.Parent().Kind() == ast.KindTableHeader { 195 | tag = "th" 196 | } 197 | if entering { 198 | align := "" 199 | if n.Alignment != ast.AlignNone { 200 | align = fmt.Sprintf(` align="%s"`, n.Alignment.String()) 201 | } 202 | fmt.Fprintf(w, "<%s%s>", tag, align) 203 | } else { 204 | fmt.Fprintf(w, "\n", tag) 205 | } 206 | return gast.WalkContinue, nil 207 | } 208 | 209 | type table struct { 210 | } 211 | 212 | // Table is an extension that allow you to use GFM tables . 213 | var Table = &table{} 214 | 215 | func (e *table) Extend(m goldmark.Markdown) { 216 | m.Parser().AddOptions(parser.WithParagraphTransformers( 217 | util.Prioritized(NewTableParagraphTransformer(), 200), 218 | )) 219 | m.Renderer().AddOptions(renderer.WithNodeRenderers( 220 | util.Prioritized(NewTableHTMLRenderer(), 500), 221 | )) 222 | } 223 | -------------------------------------------------------------------------------- /extension/table_test.go: -------------------------------------------------------------------------------- 1 | package extension 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/enkogu/goldmark" 7 | "github.com/enkogu/goldmark/renderer/html" 8 | "github.com/enkogu/goldmark/testutil" 9 | ) 10 | 11 | func TestTable(t *testing.T) { 12 | markdown := goldmark.New( 13 | goldmark.WithRendererOptions( 14 | html.WithUnsafe(), 15 | ), 16 | goldmark.WithExtensions( 17 | Table, 18 | ), 19 | ) 20 | testutil.DoTestCaseFile(markdown, "_test/table.txt", t) 21 | } 22 | -------------------------------------------------------------------------------- /extension/tasklist.go: -------------------------------------------------------------------------------- 1 | package extension 2 | 3 | import ( 4 | "regexp" 5 | 6 | "github.com/enkogu/goldmark" 7 | gast "github.com/enkogu/goldmark/ast" 8 | "github.com/enkogu/goldmark/extension/ast" 9 | "github.com/enkogu/goldmark/parser" 10 | "github.com/enkogu/goldmark/renderer" 11 | "github.com/enkogu/goldmark/renderer/html" 12 | "github.com/enkogu/goldmark/text" 13 | "github.com/enkogu/goldmark/util" 14 | ) 15 | 16 | var taskListRegexp = regexp.MustCompile(`^\[([\sxX])\]\s*`) 17 | 18 | type taskCheckBoxParser struct { 19 | } 20 | 21 | var defaultTaskCheckBoxParser = &taskCheckBoxParser{} 22 | 23 | // NewTaskCheckBoxParser returns a new InlineParser that can parse 24 | // checkboxes in list items. 25 | // This parser must take precedence over the parser.LinkParser. 26 | func NewTaskCheckBoxParser() parser.InlineParser { 27 | return defaultTaskCheckBoxParser 28 | } 29 | 30 | func (s *taskCheckBoxParser) Trigger() []byte { 31 | return []byte{'['} 32 | } 33 | 34 | func (s *taskCheckBoxParser) Parse(parent gast.Node, block text.Reader, pc parser.Context) gast.Node { 35 | // Given AST structure must be like 36 | // - List 37 | // - ListItem : parent.Parent 38 | // - TextBlock : parent 39 | // (current line) 40 | if parent.Parent() == nil || parent.Parent().FirstChild() != parent { 41 | return nil 42 | } 43 | 44 | if _, ok := parent.Parent().(*gast.ListItem); !ok { 45 | return nil 46 | } 47 | line, _ := block.PeekLine() 48 | m := taskListRegexp.FindSubmatchIndex(line) 49 | if m == nil { 50 | return nil 51 | } 52 | value := line[m[2]:m[3]][0] 53 | block.Advance(m[1]) 54 | checked := value == 'x' || value == 'X' 55 | return ast.NewTaskCheckBox(checked) 56 | } 57 | 58 | func (s *taskCheckBoxParser) CloseBlock(parent gast.Node, pc parser.Context) { 59 | // nothing to do 60 | } 61 | 62 | // TaskCheckBoxHTMLRenderer is a renderer.NodeRenderer implementation that 63 | // renders checkboxes in list items. 64 | type TaskCheckBoxHTMLRenderer struct { 65 | html.Config 66 | } 67 | 68 | // NewTaskCheckBoxHTMLRenderer returns a new TaskCheckBoxHTMLRenderer. 69 | func NewTaskCheckBoxHTMLRenderer(opts ...html.Option) renderer.NodeRenderer { 70 | r := &TaskCheckBoxHTMLRenderer{ 71 | Config: html.NewConfig(), 72 | } 73 | for _, opt := range opts { 74 | opt.SetHTMLOption(&r.Config) 75 | } 76 | return r 77 | } 78 | 79 | // RegisterFuncs implements renderer.NodeRenderer.RegisterFuncs. 80 | func (r *TaskCheckBoxHTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) { 81 | reg.Register(ast.KindTaskCheckBox, r.renderTaskCheckBox) 82 | } 83 | 84 | func (r *TaskCheckBoxHTMLRenderer) renderTaskCheckBox(w util.BufWriter, source []byte, node gast.Node, entering bool) (gast.WalkStatus, error) { 85 | if !entering { 86 | return gast.WalkContinue, nil 87 | } 88 | n := node.(*ast.TaskCheckBox) 89 | 90 | if n.IsChecked { 91 | w.WriteString(`") 97 | } else { 98 | w.WriteString(">") 99 | } 100 | return gast.WalkContinue, nil 101 | } 102 | 103 | type taskList struct { 104 | } 105 | 106 | // TaskList is an extension that allow you to use GFM task lists. 107 | var TaskList = &taskList{} 108 | 109 | func (e *taskList) Extend(m goldmark.Markdown) { 110 | m.Parser().AddOptions(parser.WithInlineParsers( 111 | util.Prioritized(NewTaskCheckBoxParser(), 0), 112 | )) 113 | m.Renderer().AddOptions(renderer.WithNodeRenderers( 114 | util.Prioritized(NewTaskCheckBoxHTMLRenderer(), 500), 115 | )) 116 | } 117 | -------------------------------------------------------------------------------- /extension/tasklist_test.go: -------------------------------------------------------------------------------- 1 | package extension 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/enkogu/goldmark" 7 | "github.com/enkogu/goldmark/renderer/html" 8 | "github.com/enkogu/goldmark/testutil" 9 | ) 10 | 11 | func TestTaskList(t *testing.T) { 12 | markdown := goldmark.New( 13 | goldmark.WithRendererOptions( 14 | html.WithUnsafe(), 15 | ), 16 | goldmark.WithExtensions( 17 | TaskList, 18 | ), 19 | ) 20 | testutil.DoTestCaseFile(markdown, "_test/tasklist.txt", t) 21 | } 22 | -------------------------------------------------------------------------------- /extension/typographer.go: -------------------------------------------------------------------------------- 1 | package extension 2 | 3 | import ( 4 | "github.com/enkogu/goldmark" 5 | gast "github.com/enkogu/goldmark/ast" 6 | "github.com/enkogu/goldmark/parser" 7 | "github.com/enkogu/goldmark/text" 8 | "github.com/enkogu/goldmark/util" 9 | ) 10 | 11 | // TypographicPunctuation is a key of the punctuations that can be replaced with 12 | // typographic entities. 13 | type TypographicPunctuation int 14 | 15 | const ( 16 | // LeftSingleQuote is ' 17 | LeftSingleQuote TypographicPunctuation = iota + 1 18 | // RightSingleQuote is ' 19 | RightSingleQuote 20 | // LeftDoubleQuote is " 21 | LeftDoubleQuote 22 | // RightDoubleQuote is " 23 | RightDoubleQuote 24 | // EnDash is -- 25 | EnDash 26 | // EmDash is --- 27 | EmDash 28 | // Ellipsis is ... 29 | Ellipsis 30 | // LeftAngleQuote is << 31 | LeftAngleQuote 32 | // RightAngleQuote is >> 33 | RightAngleQuote 34 | 35 | typographicPunctuationMax 36 | ) 37 | 38 | // An TypographerConfig struct is a data structure that holds configuration of the 39 | // Typographer extension. 40 | type TypographerConfig struct { 41 | Substitutions [][]byte 42 | } 43 | 44 | func newDefaultSubstitutions() [][]byte { 45 | replacements := make([][]byte, typographicPunctuationMax) 46 | replacements[LeftSingleQuote] = []byte("‘") 47 | replacements[RightSingleQuote] = []byte("’") 48 | replacements[LeftDoubleQuote] = []byte("“") 49 | replacements[RightDoubleQuote] = []byte("”") 50 | replacements[EnDash] = []byte("–") 51 | replacements[EmDash] = []byte("—") 52 | replacements[Ellipsis] = []byte("…") 53 | replacements[LeftAngleQuote] = []byte("«") 54 | replacements[RightAngleQuote] = []byte("»") 55 | 56 | return replacements 57 | } 58 | 59 | // SetOption implements SetOptioner. 60 | func (b *TypographerConfig) SetOption(name parser.OptionName, value interface{}) { 61 | switch name { 62 | case optTypographicSubstitutions: 63 | b.Substitutions = value.([][]byte) 64 | } 65 | } 66 | 67 | // A TypographerOption interface sets options for the TypographerParser. 68 | type TypographerOption interface { 69 | parser.Option 70 | SetTypographerOption(*TypographerConfig) 71 | } 72 | 73 | const optTypographicSubstitutions parser.OptionName = "TypographicSubstitutions" 74 | 75 | // TypographicSubstitutions is a list of the substitutions for the Typographer extension. 76 | type TypographicSubstitutions map[TypographicPunctuation][]byte 77 | 78 | type withTypographicSubstitutions struct { 79 | value [][]byte 80 | } 81 | 82 | func (o *withTypographicSubstitutions) SetParserOption(c *parser.Config) { 83 | c.Options[optTypographicSubstitutions] = o.value 84 | } 85 | 86 | func (o *withTypographicSubstitutions) SetTypographerOption(p *TypographerConfig) { 87 | p.Substitutions = o.value 88 | } 89 | 90 | // WithTypographicSubstitutions is a functional otpion that specify replacement text 91 | // for punctuations. 92 | func WithTypographicSubstitutions(values map[TypographicPunctuation][]byte) TypographerOption { 93 | replacements := newDefaultSubstitutions() 94 | for k, v := range values { 95 | replacements[k] = v 96 | } 97 | 98 | return &withTypographicSubstitutions{replacements} 99 | } 100 | 101 | type typographerDelimiterProcessor struct { 102 | } 103 | 104 | func (p *typographerDelimiterProcessor) IsDelimiter(b byte) bool { 105 | return b == '\'' || b == '"' 106 | } 107 | 108 | func (p *typographerDelimiterProcessor) CanOpenCloser(opener, closer *parser.Delimiter) bool { 109 | return opener.Char == closer.Char 110 | } 111 | 112 | func (p *typographerDelimiterProcessor) OnMatch(consumes int) gast.Node { 113 | return nil 114 | } 115 | 116 | var defaultTypographerDelimiterProcessor = &typographerDelimiterProcessor{} 117 | 118 | type typographerParser struct { 119 | TypographerConfig 120 | } 121 | 122 | // NewTypographerParser return a new InlineParser that parses 123 | // typographer expressions. 124 | func NewTypographerParser(opts ...TypographerOption) parser.InlineParser { 125 | p := &typographerParser{ 126 | TypographerConfig: TypographerConfig{ 127 | Substitutions: newDefaultSubstitutions(), 128 | }, 129 | } 130 | for _, o := range opts { 131 | o.SetTypographerOption(&p.TypographerConfig) 132 | } 133 | return p 134 | } 135 | 136 | func (s *typographerParser) Trigger() []byte { 137 | return []byte{'\'', '"', '-', '.', '<', '>'} 138 | } 139 | 140 | func (s *typographerParser) Parse(parent gast.Node, block text.Reader, pc parser.Context) gast.Node { 141 | before := block.PrecendingCharacter() 142 | line, _ := block.PeekLine() 143 | c := line[0] 144 | if len(line) > 2 { 145 | if c == '-' { 146 | if s.Substitutions[EmDash] != nil && line[1] == '-' && line[2] == '-' { // --- 147 | node := gast.NewString(s.Substitutions[EmDash]) 148 | node.SetCode(true) 149 | block.Advance(3) 150 | return node 151 | } 152 | } else if c == '.' { 153 | if s.Substitutions[Ellipsis] != nil && line[1] == '.' && line[2] == '.' { // ... 154 | node := gast.NewString(s.Substitutions[Ellipsis]) 155 | node.SetCode(true) 156 | block.Advance(3) 157 | return node 158 | } 159 | return nil 160 | } 161 | } 162 | if len(line) > 1 { 163 | if c == '<' { 164 | if s.Substitutions[LeftAngleQuote] != nil && line[1] == '<' { // << 165 | node := gast.NewString(s.Substitutions[LeftAngleQuote]) 166 | node.SetCode(true) 167 | block.Advance(2) 168 | return node 169 | } 170 | return nil 171 | } else if c == '>' { 172 | if s.Substitutions[RightAngleQuote] != nil && line[1] == '>' { // >> 173 | node := gast.NewString(s.Substitutions[RightAngleQuote]) 174 | node.SetCode(true) 175 | block.Advance(2) 176 | return node 177 | } 178 | return nil 179 | } else if s.Substitutions[EnDash] != nil && c == '-' && line[1] == '-' { // -- 180 | node := gast.NewString(s.Substitutions[EnDash]) 181 | node.SetCode(true) 182 | block.Advance(2) 183 | return node 184 | } 185 | } 186 | if c == '\'' || c == '"' { 187 | d := parser.ScanDelimiter(line, before, 1, defaultTypographerDelimiterProcessor) 188 | if d == nil { 189 | return nil 190 | } 191 | if c == '\'' { 192 | if s.Substitutions[LeftSingleQuote] != nil && d.CanOpen && !d.CanClose { 193 | node := gast.NewString(s.Substitutions[LeftSingleQuote]) 194 | node.SetCode(true) 195 | block.Advance(1) 196 | return node 197 | } 198 | if s.Substitutions[RightSingleQuote] != nil && d.CanClose && !d.CanOpen { 199 | node := gast.NewString(s.Substitutions[RightSingleQuote]) 200 | node.SetCode(true) 201 | block.Advance(1) 202 | return node 203 | } 204 | } 205 | if c == '"' { 206 | if s.Substitutions[LeftDoubleQuote] != nil && d.CanOpen && !d.CanClose { 207 | node := gast.NewString(s.Substitutions[LeftDoubleQuote]) 208 | node.SetCode(true) 209 | block.Advance(1) 210 | return node 211 | } 212 | if s.Substitutions[RightDoubleQuote] != nil && d.CanClose && !d.CanOpen { 213 | node := gast.NewString(s.Substitutions[RightDoubleQuote]) 214 | node.SetCode(true) 215 | block.Advance(1) 216 | return node 217 | } 218 | } 219 | } 220 | return nil 221 | } 222 | 223 | func (s *typographerParser) CloseBlock(parent gast.Node, pc parser.Context) { 224 | // nothing to do 225 | } 226 | 227 | type typographer struct { 228 | options []TypographerOption 229 | } 230 | 231 | // Typographer is an extension that repalace punctuations with typographic entities. 232 | var Typographer = &typographer{} 233 | 234 | // NewTypographer returns a new Entender that repalace punctuations with typographic entities. 235 | func NewTypographer(opts ...TypographerOption) goldmark.Extender { 236 | return &typographer{ 237 | options: opts, 238 | } 239 | } 240 | 241 | func (e *typographer) Extend(m goldmark.Markdown) { 242 | m.Parser().AddOptions(parser.WithInlineParsers( 243 | util.Prioritized(NewTypographerParser(e.options...), 9999), 244 | )) 245 | } 246 | -------------------------------------------------------------------------------- /extension/typographer_test.go: -------------------------------------------------------------------------------- 1 | package extension 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/enkogu/goldmark" 7 | "github.com/enkogu/goldmark/renderer/html" 8 | "github.com/enkogu/goldmark/testutil" 9 | ) 10 | 11 | func TestTypographer(t *testing.T) { 12 | markdown := goldmark.New( 13 | goldmark.WithRendererOptions( 14 | html.WithUnsafe(), 15 | ), 16 | goldmark.WithExtensions( 17 | Typographer, 18 | ), 19 | ) 20 | testutil.DoTestCaseFile(markdown, "_test/typographer.txt", t) 21 | } 22 | -------------------------------------------------------------------------------- /extra_test.go: -------------------------------------------------------------------------------- 1 | package goldmark_test 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | . "github.com/enkogu/goldmark" 8 | "github.com/enkogu/goldmark/ast" 9 | "github.com/enkogu/goldmark/parser" 10 | "github.com/enkogu/goldmark/renderer/html" 11 | "github.com/enkogu/goldmark/testutil" 12 | ) 13 | 14 | func TestExtras(t *testing.T) { 15 | markdown := New(WithRendererOptions( 16 | html.WithXHTML(), 17 | html.WithUnsafe(), 18 | )) 19 | testutil.DoTestCaseFile(markdown, "_test/extra.txt", t) 20 | } 21 | 22 | func TestEndsWithNonSpaceCharacters(t *testing.T) { 23 | markdown := New(WithRendererOptions( 24 | html.WithXHTML(), 25 | html.WithUnsafe(), 26 | )) 27 | source := []byte("```\na\n```") 28 | var b bytes.Buffer 29 | err := markdown.Convert(source, &b) 30 | if err != nil { 31 | t.Error(err.Error()) 32 | } 33 | if b.String() != "
    a\n
    \n" { 34 | t.Errorf("%s \n---------\n %s", source, b.String()) 35 | } 36 | } 37 | 38 | func TestWindowsNewLine(t *testing.T) { 39 | markdown := New(WithRendererOptions( 40 | html.WithXHTML(), 41 | )) 42 | source := []byte("a \r\nb\n") 43 | var b bytes.Buffer 44 | err := markdown.Convert(source, &b) 45 | if err != nil { 46 | t.Error(err.Error()) 47 | } 48 | if b.String() != "

    a
    \nb

    \n" { 49 | t.Errorf("%s\n---------\n%s", source, b.String()) 50 | } 51 | 52 | source = []byte("a\\\r\nb\r\n") 53 | var b2 bytes.Buffer 54 | err = markdown.Convert(source, &b2) 55 | if err != nil { 56 | t.Error(err.Error()) 57 | } 58 | if b2.String() != "

    a
    \nb

    \n" { 59 | t.Errorf("\n%s\n---------\n%s", source, b2.String()) 60 | } 61 | } 62 | 63 | type myIDs struct { 64 | } 65 | 66 | func (s *myIDs) Generate(value []byte, kind ast.NodeKind) []byte { 67 | return []byte("my-id") 68 | } 69 | 70 | func (s *myIDs) Put(value []byte) { 71 | } 72 | 73 | func TestAutogeneratedIDs(t *testing.T) { 74 | ctx := parser.NewContext(parser.WithIDs(&myIDs{})) 75 | markdown := New(WithParserOptions(parser.WithAutoHeadingID())) 76 | source := []byte("# Title1\n## Title2") 77 | var b bytes.Buffer 78 | err := markdown.Convert(source, &b, parser.WithContext(ctx)) 79 | if err != nil { 80 | t.Error(err.Error()) 81 | } 82 | if b.String() != `

    Title1

    83 |

    Title2

    84 | ` { 85 | t.Errorf("%s\n---------\n%s", source, b.String()) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /fuzz/fuzz.go: -------------------------------------------------------------------------------- 1 | package fuzz 2 | 3 | import ( 4 | "bytes" 5 | 6 | "github.com/enkogu/goldmark" 7 | "github.com/enkogu/goldmark/extension" 8 | "github.com/enkogu/goldmark/parser" 9 | "github.com/enkogu/goldmark/renderer/html" 10 | ) 11 | 12 | // Fuzz runs automated fuzzing against goldmark. 13 | func Fuzz(data []byte) int { 14 | markdown := goldmark.New( 15 | goldmark.WithParserOptions( 16 | parser.WithAutoHeadingID(), 17 | parser.WithAttribute(), 18 | ), 19 | goldmark.WithRendererOptions( 20 | html.WithUnsafe(), 21 | html.WithXHTML(), 22 | ), 23 | goldmark.WithExtensions( 24 | extension.DefinitionList, 25 | extension.Footnote, 26 | extension.GFM, 27 | extension.Linkify, 28 | extension.Table, 29 | extension.TaskList, 30 | extension.Typographer, 31 | ), 32 | ) 33 | var b bytes.Buffer 34 | if err := markdown.Convert(data, &b); err != nil { 35 | return 0 36 | } 37 | 38 | return 1 39 | } 40 | -------------------------------------------------------------------------------- /fuzz/fuzz_test.go: -------------------------------------------------------------------------------- 1 | package fuzz 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io/ioutil" 7 | "testing" 8 | 9 | "github.com/enkogu/goldmark" 10 | "github.com/enkogu/goldmark/extension" 11 | "github.com/enkogu/goldmark/parser" 12 | "github.com/enkogu/goldmark/renderer/html" 13 | "github.com/enkogu/goldmark/util" 14 | ) 15 | 16 | var _ = fmt.Printf 17 | 18 | func TestFuzz(t *testing.T) { 19 | crasher := "6dff3d03167cb144d4e2891edac76ee740a77bc7" 20 | data, err := ioutil.ReadFile("crashers/" + crasher) 21 | if err != nil { 22 | return 23 | } 24 | fmt.Printf("%s\n", util.VisualizeSpaces(data)) 25 | fmt.Println("||||||||||||||||||||||") 26 | markdown := goldmark.New( 27 | goldmark.WithParserOptions( 28 | parser.WithAutoHeadingID(), 29 | parser.WithAttribute(), 30 | ), 31 | goldmark.WithRendererOptions( 32 | html.WithUnsafe(), 33 | html.WithXHTML(), 34 | ), 35 | goldmark.WithExtensions( 36 | extension.DefinitionList, 37 | extension.Footnote, 38 | extension.GFM, 39 | extension.Typographer, 40 | extension.Linkify, 41 | extension.Table, 42 | extension.TaskList, 43 | ), 44 | ) 45 | var b bytes.Buffer 46 | if err := markdown.Convert(data, &b); err != nil { 47 | panic(err) 48 | } 49 | fmt.Println(b.String()) 50 | } 51 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/enkogu/goldmark 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/88250/lute v0.0.0-20191201130105-0e9e9801157c 7 | github.com/gomarkdown/markdown v0.0.0-20191207194928-fbea82c4bb03 8 | gitlab.com/golang-commonmark/markdown v0.0.0-20191127184510-91b5b3c99c19 9 | ) 10 | -------------------------------------------------------------------------------- /markdown.go: -------------------------------------------------------------------------------- 1 | // Package goldmark implements functions to convert markdown text to a desired format. 2 | package goldmark 3 | 4 | import ( 5 | "io" 6 | 7 | "github.com/enkogu/goldmark/parser" 8 | "github.com/enkogu/goldmark/renderer" 9 | "github.com/enkogu/goldmark/renderer/html" 10 | "github.com/enkogu/goldmark/text" 11 | "github.com/enkogu/goldmark/util" 12 | ) 13 | 14 | // DefaultParser returns a new Parser that is configured by default values. 15 | func DefaultParser() parser.Parser { 16 | return parser.NewParser(parser.WithBlockParsers(parser.DefaultBlockParsers()...), 17 | parser.WithInlineParsers(parser.DefaultInlineParsers()...), 18 | parser.WithParagraphTransformers(parser.DefaultParagraphTransformers()...), 19 | ) 20 | } 21 | 22 | // DefaultRenderer returns a new Renderer that is configured by default values. 23 | func DefaultRenderer() renderer.Renderer { 24 | return renderer.NewRenderer(renderer.WithNodeRenderers(util.Prioritized(html.NewRenderer(), 1000))) 25 | } 26 | 27 | var defaultMarkdown = New() 28 | 29 | // Convert interprets a UTF-8 bytes source in Markdown and 30 | // write rendered contents to a writer w. 31 | func Convert(source []byte, w io.Writer, opts ...parser.ParseOption) error { 32 | return defaultMarkdown.Convert(source, w, opts...) 33 | } 34 | 35 | // A Markdown interface offers functions to convert Markdown text to 36 | // a desired format. 37 | type Markdown interface { 38 | // Convert interprets a UTF-8 bytes source in Markdown and write rendered 39 | // contents to a writer w. 40 | Convert(source []byte, writer io.Writer, opts ...parser.ParseOption) error 41 | 42 | // Parser returns a Parser that will be used for conversion. 43 | Parser() parser.Parser 44 | 45 | // SetParser sets a Parser to this object. 46 | SetParser(parser.Parser) 47 | 48 | // Parser returns a Renderer that will be used for conversion. 49 | Renderer() renderer.Renderer 50 | 51 | // SetRenderer sets a Renderer to this object. 52 | SetRenderer(renderer.Renderer) 53 | } 54 | 55 | // Option is a functional option type for Markdown objects. 56 | type Option func(*markdown) 57 | 58 | // WithExtensions adds extensions. 59 | func WithExtensions(ext ...Extender) Option { 60 | return func(m *markdown) { 61 | m.extensions = append(m.extensions, ext...) 62 | } 63 | } 64 | 65 | // WithParser allows you to override the default parser. 66 | func WithParser(p parser.Parser) Option { 67 | return func(m *markdown) { 68 | m.parser = p 69 | } 70 | } 71 | 72 | // WithParserOptions applies options for the parser. 73 | func WithParserOptions(opts ...parser.Option) Option { 74 | return func(m *markdown) { 75 | m.parser.AddOptions(opts...) 76 | } 77 | } 78 | 79 | // WithRenderer allows you to override the default renderer. 80 | func WithRenderer(r renderer.Renderer) Option { 81 | return func(m *markdown) { 82 | m.renderer = r 83 | } 84 | } 85 | 86 | // WithRendererOptions applies options for the renderer. 87 | func WithRendererOptions(opts ...renderer.Option) Option { 88 | return func(m *markdown) { 89 | m.renderer.AddOptions(opts...) 90 | } 91 | } 92 | 93 | type markdown struct { 94 | parser parser.Parser 95 | renderer renderer.Renderer 96 | extensions []Extender 97 | } 98 | 99 | // New returns a new Markdown with given options. 100 | func New(options ...Option) Markdown { 101 | md := &markdown{ 102 | parser: DefaultParser(), 103 | renderer: DefaultRenderer(), 104 | extensions: []Extender{}, 105 | } 106 | for _, opt := range options { 107 | opt(md) 108 | } 109 | for _, e := range md.extensions { 110 | e.Extend(md) 111 | } 112 | return md 113 | } 114 | 115 | func (m *markdown) Convert(source []byte, writer io.Writer, opts ...parser.ParseOption) error { 116 | reader := text.NewReader(source) 117 | doc := m.parser.Parse(reader, opts...) 118 | return m.renderer.Render(writer, source, doc) 119 | } 120 | 121 | func (m *markdown) Parser() parser.Parser { 122 | return m.parser 123 | } 124 | 125 | func (m *markdown) SetParser(v parser.Parser) { 126 | m.parser = v 127 | } 128 | 129 | func (m *markdown) Renderer() renderer.Renderer { 130 | return m.renderer 131 | } 132 | 133 | func (m *markdown) SetRenderer(v renderer.Renderer) { 134 | m.renderer = v 135 | } 136 | 137 | // An Extender interface is used for extending Markdown. 138 | type Extender interface { 139 | // Extend extends the Markdown. 140 | Extend(Markdown) 141 | } 142 | -------------------------------------------------------------------------------- /options_test.go: -------------------------------------------------------------------------------- 1 | package goldmark_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/enkogu/goldmark" 7 | "github.com/enkogu/goldmark/parser" 8 | "github.com/enkogu/goldmark/testutil" 9 | ) 10 | 11 | func TestAttributeAndAutoHeadingID(t *testing.T) { 12 | markdown := New( 13 | WithParserOptions( 14 | parser.WithAttribute(), 15 | parser.WithAutoHeadingID(), 16 | ), 17 | ) 18 | testutil.DoTestCaseFile(markdown, "_test/options.txt", t) 19 | } 20 | -------------------------------------------------------------------------------- /parser/attribute.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "strconv" 7 | 8 | "github.com/enkogu/goldmark/text" 9 | "github.com/enkogu/goldmark/util" 10 | ) 11 | 12 | var attrNameID = []byte("id") 13 | var attrNameClass = []byte("class") 14 | 15 | // An Attribute is an attribute of the markdown elements 16 | type Attribute struct { 17 | Name []byte 18 | Value interface{} 19 | } 20 | 21 | // An Attributes is a collection of attributes. 22 | type Attributes []Attribute 23 | 24 | // Find returns a (value, true) if an attribute correspond with given name is found, otherwise (nil, false). 25 | func (as Attributes) Find(name []byte) (interface{}, bool) { 26 | for _, a := range as { 27 | if bytes.Equal(a.Name, name) { 28 | return a.Value, true 29 | } 30 | } 31 | return nil, false 32 | } 33 | 34 | func (as Attributes) findUpdate(name []byte, cb func(v interface{}) interface{}) bool { 35 | for i, a := range as { 36 | if bytes.Equal(a.Name, name) { 37 | as[i].Value = cb(a.Value) 38 | return true 39 | } 40 | } 41 | return false 42 | } 43 | 44 | // ParseAttributes parses attributes into a map. 45 | // ParseAttributes returns a parsed attributes and true if could parse 46 | // attributes, otherwise nil and false. 47 | func ParseAttributes(reader text.Reader) (Attributes, bool) { 48 | savedLine, savedPosition := reader.Position() 49 | reader.SkipSpaces() 50 | if reader.Peek() != '{' { 51 | reader.SetPosition(savedLine, savedPosition) 52 | return nil, false 53 | } 54 | reader.Advance(1) 55 | attrs := Attributes{} 56 | for { 57 | if reader.Peek() == '}' { 58 | reader.Advance(1) 59 | return attrs, true 60 | } 61 | attr, ok := parseAttribute(reader) 62 | if !ok { 63 | reader.SetPosition(savedLine, savedPosition) 64 | return nil, false 65 | } 66 | if bytes.Equal(attr.Name, attrNameClass) { 67 | if !attrs.findUpdate(attrNameClass, func(v interface{}) interface{} { 68 | ret := make([]byte, 0, len(v.([]byte))+1+len(attr.Value.([]byte))) 69 | ret = append(ret, v.([]byte)...) 70 | return append(append(ret, ' '), attr.Value.([]byte)...) 71 | }) { 72 | attrs = append(attrs, attr) 73 | } 74 | } else { 75 | attrs = append(attrs, attr) 76 | } 77 | reader.SkipSpaces() 78 | if reader.Peek() == ',' { 79 | reader.Advance(1) 80 | reader.SkipSpaces() 81 | } 82 | } 83 | } 84 | 85 | func parseAttribute(reader text.Reader) (Attribute, bool) { 86 | reader.SkipSpaces() 87 | c := reader.Peek() 88 | if c == '#' || c == '.' { 89 | reader.Advance(1) 90 | line, _ := reader.PeekLine() 91 | i := 0 92 | for ; i < len(line) && !util.IsSpace(line[i]) && (!util.IsPunct(line[i]) || line[i] == '_' || line[i] == '-'); i++ { 93 | } 94 | name := attrNameClass 95 | if c == '#' { 96 | name = attrNameID 97 | } 98 | reader.Advance(i) 99 | return Attribute{Name: name, Value: line[0:i]}, true 100 | } 101 | line, _ := reader.PeekLine() 102 | if len(line) == 0 { 103 | return Attribute{}, false 104 | } 105 | c = line[0] 106 | if !((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || 107 | c == '_' || c == ':') { 108 | return Attribute{}, false 109 | } 110 | i := 0 111 | for ; i < len(line); i++ { 112 | c = line[i] 113 | if !((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || 114 | (c >= '0' && c <= '9') || 115 | c == '_' || c == ':' || c == '.' || c == '-') { 116 | break 117 | } 118 | } 119 | name := line[:i] 120 | reader.Advance(i) 121 | reader.SkipSpaces() 122 | c = reader.Peek() 123 | if c != '=' { 124 | return Attribute{}, false 125 | } 126 | reader.Advance(1) 127 | reader.SkipSpaces() 128 | value, ok := parseAttributeValue(reader) 129 | if !ok { 130 | return Attribute{}, false 131 | } 132 | return Attribute{Name: name, Value: value}, true 133 | } 134 | 135 | func parseAttributeValue(reader text.Reader) (interface{}, bool) { 136 | reader.SkipSpaces() 137 | c := reader.Peek() 138 | var value interface{} 139 | ok := false 140 | switch c { 141 | case text.EOF: 142 | return Attribute{}, false 143 | case '{': 144 | value, ok = ParseAttributes(reader) 145 | case '[': 146 | value, ok = parseAttributeArray(reader) 147 | case '"': 148 | value, ok = parseAttributeString(reader) 149 | default: 150 | if c == '-' || c == '+' || util.IsNumeric(c) { 151 | value, ok = parseAttributeNumber(reader) 152 | } else { 153 | value, ok = parseAttributeOthers(reader) 154 | } 155 | } 156 | if !ok { 157 | return nil, false 158 | } 159 | return value, true 160 | } 161 | 162 | func parseAttributeArray(reader text.Reader) ([]interface{}, bool) { 163 | reader.Advance(1) // skip [ 164 | ret := []interface{}{} 165 | for i := 0; ; i++ { 166 | c := reader.Peek() 167 | comma := false 168 | if i != 0 && c == ',' { 169 | reader.Advance(1) 170 | comma = true 171 | } 172 | if c == ']' { 173 | if !comma { 174 | reader.Advance(1) 175 | return ret, true 176 | } 177 | return nil, false 178 | } 179 | reader.SkipSpaces() 180 | value, ok := parseAttributeValue(reader) 181 | if !ok { 182 | return nil, false 183 | } 184 | ret = append(ret, value) 185 | reader.SkipSpaces() 186 | } 187 | } 188 | 189 | func parseAttributeString(reader text.Reader) ([]byte, bool) { 190 | reader.Advance(1) // skip " 191 | line, _ := reader.PeekLine() 192 | i := 0 193 | l := len(line) 194 | var buf bytes.Buffer 195 | for i < l { 196 | c := line[i] 197 | if c == '\\' && i != l-1 { 198 | n := line[i+1] 199 | switch n { 200 | case '"', '/', '\\': 201 | buf.WriteByte(n) 202 | i += 2 203 | case 'b': 204 | buf.WriteString("\b") 205 | i += 2 206 | case 'f': 207 | buf.WriteString("\f") 208 | i += 2 209 | case 'n': 210 | buf.WriteString("\n") 211 | i += 2 212 | case 'r': 213 | buf.WriteString("\r") 214 | i += 2 215 | case 't': 216 | buf.WriteString("\t") 217 | i += 2 218 | default: 219 | buf.WriteByte('\\') 220 | i++ 221 | } 222 | continue 223 | } 224 | if c == '"' { 225 | reader.Advance(i + 1) 226 | return buf.Bytes(), true 227 | } 228 | buf.WriteByte(c) 229 | i++ 230 | } 231 | return nil, false 232 | } 233 | 234 | func scanAttributeDecimal(reader text.Reader, w io.ByteWriter) { 235 | for { 236 | c := reader.Peek() 237 | if util.IsNumeric(c) { 238 | w.WriteByte(c) 239 | } else { 240 | return 241 | } 242 | reader.Advance(1) 243 | } 244 | } 245 | 246 | func parseAttributeNumber(reader text.Reader) (float64, bool) { 247 | sign := 1 248 | c := reader.Peek() 249 | if c == '-' { 250 | sign = -1 251 | reader.Advance(1) 252 | } else if c == '+' { 253 | reader.Advance(1) 254 | } 255 | var buf bytes.Buffer 256 | if !util.IsNumeric(reader.Peek()) { 257 | return 0, false 258 | } 259 | scanAttributeDecimal(reader, &buf) 260 | if buf.Len() == 0 { 261 | return 0, false 262 | } 263 | c = reader.Peek() 264 | if c == '.' { 265 | buf.WriteByte(c) 266 | reader.Advance(1) 267 | scanAttributeDecimal(reader, &buf) 268 | } 269 | c = reader.Peek() 270 | if c == 'e' || c == 'E' { 271 | buf.WriteByte(c) 272 | reader.Advance(1) 273 | c = reader.Peek() 274 | if c == '-' || c == '+' { 275 | buf.WriteByte(c) 276 | reader.Advance(1) 277 | } 278 | scanAttributeDecimal(reader, &buf) 279 | } 280 | f, err := strconv.ParseFloat(buf.String(), 10) 281 | if err != nil { 282 | return 0, false 283 | } 284 | return float64(sign) * f, true 285 | } 286 | 287 | var bytesTrue = []byte("true") 288 | var bytesFalse = []byte("false") 289 | var bytesNull = []byte("null") 290 | 291 | func parseAttributeOthers(reader text.Reader) (interface{}, bool) { 292 | line, _ := reader.PeekLine() 293 | c := line[0] 294 | if !((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || 295 | c == '_' || c == ':') { 296 | return nil, false 297 | } 298 | i := 0 299 | for ; i < len(line); i++ { 300 | c := line[i] 301 | if !((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || 302 | (c >= '0' && c <= '9') || 303 | c == '_' || c == ':' || c == '.' || c == '-') { 304 | break 305 | } 306 | } 307 | value := line[:i] 308 | reader.Advance(i) 309 | if bytes.Equal(value, bytesTrue) { 310 | return true, true 311 | } 312 | if bytes.Equal(value, bytesFalse) { 313 | return false, true 314 | } 315 | if bytes.Equal(value, bytesNull) { 316 | return nil, true 317 | } 318 | return value, true 319 | } 320 | -------------------------------------------------------------------------------- /parser/atx_heading.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "github.com/enkogu/goldmark/ast" 5 | "github.com/enkogu/goldmark/text" 6 | "github.com/enkogu/goldmark/util" 7 | ) 8 | 9 | // A HeadingConfig struct is a data structure that holds configuration of the renderers related to headings. 10 | type HeadingConfig struct { 11 | AutoHeadingID bool 12 | Attribute bool 13 | } 14 | 15 | // SetOption implements SetOptioner. 16 | func (b *HeadingConfig) SetOption(name OptionName, value interface{}) { 17 | switch name { 18 | case optAutoHeadingID: 19 | b.AutoHeadingID = true 20 | case optAttribute: 21 | b.Attribute = true 22 | } 23 | } 24 | 25 | // A HeadingOption interface sets options for heading parsers. 26 | type HeadingOption interface { 27 | Option 28 | SetHeadingOption(*HeadingConfig) 29 | } 30 | 31 | // AutoHeadingID is an option name that enables auto IDs for headings. 32 | const optAutoHeadingID OptionName = "AutoHeadingID" 33 | 34 | type withAutoHeadingID struct { 35 | } 36 | 37 | func (o *withAutoHeadingID) SetParserOption(c *Config) { 38 | c.Options[optAutoHeadingID] = true 39 | } 40 | 41 | func (o *withAutoHeadingID) SetHeadingOption(p *HeadingConfig) { 42 | p.AutoHeadingID = true 43 | } 44 | 45 | // WithAutoHeadingID is a functional option that enables custom heading ids and 46 | // auto generated heading ids. 47 | func WithAutoHeadingID() HeadingOption { 48 | return &withAutoHeadingID{} 49 | } 50 | 51 | type withHeadingAttribute struct { 52 | Option 53 | } 54 | 55 | func (o *withHeadingAttribute) SetHeadingOption(p *HeadingConfig) { 56 | p.Attribute = true 57 | } 58 | 59 | // WithHeadingAttribute is a functional option that enables custom heading attributes. 60 | func WithHeadingAttribute() HeadingOption { 61 | return &withHeadingAttribute{WithAttribute()} 62 | } 63 | 64 | type atxHeadingParser struct { 65 | HeadingConfig 66 | } 67 | 68 | // NewATXHeadingParser return a new BlockParser that can parse ATX headings. 69 | func NewATXHeadingParser(opts ...HeadingOption) BlockParser { 70 | p := &atxHeadingParser{} 71 | for _, o := range opts { 72 | o.SetHeadingOption(&p.HeadingConfig) 73 | } 74 | return p 75 | } 76 | 77 | func (b *atxHeadingParser) Trigger() []byte { 78 | return []byte{'#'} 79 | } 80 | 81 | func (b *atxHeadingParser) Open(parent ast.Node, reader text.Reader, pc Context) (ast.Node, State) { 82 | line, segment := reader.PeekLine() 83 | pos := pc.BlockOffset() 84 | if pos < 0 { 85 | return nil, NoChildren 86 | } 87 | i := pos 88 | for ; i < len(line) && line[i] == '#'; i++ { 89 | } 90 | level := i - pos 91 | if i == pos || level > 6 { 92 | return nil, NoChildren 93 | } 94 | l := util.TrimLeftSpaceLength(line[i:]) 95 | if l == 0 { 96 | return nil, NoChildren 97 | } 98 | start := i + l 99 | if start >= len(line) { 100 | start = len(line) - 1 101 | } 102 | origstart := start 103 | stop := len(line) - util.TrimRightSpaceLength(line) 104 | 105 | node := ast.NewHeading(level) 106 | parsed := false 107 | if b.Attribute { // handles special case like ### heading ### {#id} 108 | start-- 109 | closureClose := -1 110 | closureOpen := -1 111 | for j := start; j < stop; { 112 | c := line[j] 113 | if util.IsEscapedPunctuation(line, j) { 114 | j += 2 115 | } else if util.IsSpace(c) && j < stop-1 && line[j+1] == '#' { 116 | closureOpen = j + 1 117 | k := j + 1 118 | for ; k < stop && line[k] == '#'; k++ { 119 | } 120 | closureClose = k 121 | break 122 | } else { 123 | j++ 124 | } 125 | } 126 | if closureClose > 0 { 127 | reader.Advance(closureClose) 128 | attrs, ok := ParseAttributes(reader) 129 | parsed = ok 130 | if parsed { 131 | for _, attr := range attrs { 132 | node.SetAttribute(attr.Name, attr.Value) 133 | } 134 | node.Lines().Append(text.NewSegment(segment.Start+start+1-segment.Padding, segment.Start+closureOpen-segment.Padding)) 135 | } 136 | } 137 | } 138 | if !parsed { 139 | start = origstart 140 | stop := len(line) - util.TrimRightSpaceLength(line) 141 | if stop <= start { // empty headings like '##[space]' 142 | stop = start 143 | } else { 144 | i = stop - 1 145 | for ; line[i] == '#' && i >= start; i-- { 146 | } 147 | if i != stop-1 && !util.IsSpace(line[i]) { 148 | i = stop - 1 149 | } 150 | i++ 151 | stop = i 152 | } 153 | 154 | if len(util.TrimRight(line[start:stop], []byte{'#'})) != 0 { // empty heading like '### ###' 155 | node.Lines().Append(text.NewSegment(segment.Start+start-segment.Padding, segment.Start+stop-segment.Padding)) 156 | } 157 | } 158 | return node, NoChildren 159 | } 160 | 161 | func (b *atxHeadingParser) Continue(node ast.Node, reader text.Reader, pc Context) State { 162 | return Close 163 | } 164 | 165 | func (b *atxHeadingParser) Close(node ast.Node, reader text.Reader, pc Context) { 166 | if b.Attribute { 167 | _, ok := node.AttributeString("id") 168 | if !ok { 169 | parseLastLineAttributes(node, reader, pc) 170 | } 171 | } 172 | 173 | if b.AutoHeadingID { 174 | _, ok := node.AttributeString("id") 175 | if !ok { 176 | generateAutoHeadingID(node.(*ast.Heading), reader, pc) 177 | } 178 | } 179 | } 180 | 181 | func (b *atxHeadingParser) CanInterruptParagraph() bool { 182 | return true 183 | } 184 | 185 | func (b *atxHeadingParser) CanAcceptIndentedLine() bool { 186 | return false 187 | } 188 | 189 | func generateAutoHeadingID(node *ast.Heading, reader text.Reader, pc Context) { 190 | var line []byte 191 | lastIndex := node.Lines().Len() - 1 192 | if lastIndex > -1 { 193 | lastLine := node.Lines().At(lastIndex) 194 | line = lastLine.Value(reader.Source()) 195 | } 196 | headingID := pc.IDs().Generate(line, ast.KindHeading) 197 | node.SetAttribute(attrNameID, headingID) 198 | } 199 | 200 | func parseLastLineAttributes(node ast.Node, reader text.Reader, pc Context) { 201 | lastIndex := node.Lines().Len() - 1 202 | if lastIndex < 0 { // empty headings 203 | return 204 | } 205 | lastLine := node.Lines().At(lastIndex) 206 | line := lastLine.Value(reader.Source()) 207 | lr := text.NewReader(line) 208 | var attrs Attributes 209 | var ok bool 210 | var start text.Segment 211 | var sl int 212 | var end text.Segment 213 | for { 214 | c := lr.Peek() 215 | if c == text.EOF { 216 | break 217 | } 218 | if c == '\\' { 219 | lr.Advance(1) 220 | if lr.Peek() == '{' { 221 | lr.Advance(1) 222 | } 223 | continue 224 | } 225 | if c == '{' { 226 | sl, start = lr.Position() 227 | attrs, ok = ParseAttributes(lr) 228 | _, end = lr.Position() 229 | lr.SetPosition(sl, start) 230 | } 231 | lr.Advance(1) 232 | } 233 | if ok && util.IsBlank(line[end.Stop:]) { 234 | for _, attr := range attrs { 235 | node.SetAttribute(attr.Name, attr.Value) 236 | } 237 | lastLine.Stop = lastLine.Start + start.Start 238 | node.Lines().Set(lastIndex, lastLine) 239 | } 240 | } 241 | -------------------------------------------------------------------------------- /parser/auto_link.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "github.com/enkogu/goldmark/ast" 5 | "github.com/enkogu/goldmark/text" 6 | "github.com/enkogu/goldmark/util" 7 | ) 8 | 9 | type autoLinkParser struct { 10 | } 11 | 12 | var defaultAutoLinkParser = &autoLinkParser{} 13 | 14 | // NewAutoLinkParser returns a new InlineParser that parses autolinks 15 | // surrounded by '<' and '>' . 16 | func NewAutoLinkParser() InlineParser { 17 | return defaultAutoLinkParser 18 | } 19 | 20 | func (s *autoLinkParser) Trigger() []byte { 21 | return []byte{'<'} 22 | } 23 | 24 | func (s *autoLinkParser) Parse(parent ast.Node, block text.Reader, pc Context) ast.Node { 25 | line, segment := block.PeekLine() 26 | stop := util.FindEmailIndex(line[1:]) 27 | typ := ast.AutoLinkType(ast.AutoLinkEmail) 28 | if stop < 0 { 29 | stop = util.FindURLIndex(line[1:]) 30 | typ = ast.AutoLinkURL 31 | } 32 | if stop < 0 { 33 | return nil 34 | } 35 | stop++ 36 | if stop >= len(line) || line[stop] != '>' { 37 | return nil 38 | } 39 | value := ast.NewTextSegment(text.NewSegment(segment.Start+1, segment.Start+stop)) 40 | block.Advance(stop + 1) 41 | return ast.NewAutoLink(typ, value) 42 | } 43 | -------------------------------------------------------------------------------- /parser/blockquote.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "github.com/enkogu/goldmark/ast" 5 | "github.com/enkogu/goldmark/text" 6 | "github.com/enkogu/goldmark/util" 7 | ) 8 | 9 | type blockquoteParser struct { 10 | } 11 | 12 | var defaultBlockquoteParser = &blockquoteParser{} 13 | 14 | // NewBlockquoteParser returns a new BlockParser that 15 | // parses blockquotes. 16 | func NewBlockquoteParser() BlockParser { 17 | return defaultBlockquoteParser 18 | } 19 | 20 | func (b *blockquoteParser) process(reader text.Reader) bool { 21 | line, _ := reader.PeekLine() 22 | w, pos := util.IndentWidth(line, reader.LineOffset()) 23 | if w > 3 || pos >= len(line) || line[pos] != '>' { 24 | return false 25 | } 26 | pos++ 27 | if pos >= len(line) || line[pos] == '\n' { 28 | reader.Advance(pos) 29 | return true 30 | } 31 | if line[pos] == ' ' || line[pos] == '\t' { 32 | pos++ 33 | } 34 | reader.Advance(pos) 35 | if line[pos-1] == '\t' { 36 | reader.SetPadding(2) 37 | } 38 | return true 39 | } 40 | 41 | func (b *blockquoteParser) Trigger() []byte { 42 | return []byte{'>'} 43 | } 44 | 45 | func (b *blockquoteParser) Open(parent ast.Node, reader text.Reader, pc Context) (ast.Node, State) { 46 | if b.process(reader) { 47 | return ast.NewBlockquote(), HasChildren 48 | } 49 | return nil, NoChildren 50 | } 51 | 52 | func (b *blockquoteParser) Continue(node ast.Node, reader text.Reader, pc Context) State { 53 | if b.process(reader) { 54 | return Continue | HasChildren 55 | } 56 | return Close 57 | } 58 | 59 | func (b *blockquoteParser) Close(node ast.Node, reader text.Reader, pc Context) { 60 | // nothing to do 61 | } 62 | 63 | func (b *blockquoteParser) CanInterruptParagraph() bool { 64 | return true 65 | } 66 | 67 | func (b *blockquoteParser) CanAcceptIndentedLine() bool { 68 | return false 69 | } 70 | -------------------------------------------------------------------------------- /parser/code_block.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "github.com/enkogu/goldmark/ast" 5 | "github.com/enkogu/goldmark/text" 6 | "github.com/enkogu/goldmark/util" 7 | ) 8 | 9 | type codeBlockParser struct { 10 | } 11 | 12 | // CodeBlockParser is a BlockParser implementation that parses indented code blocks. 13 | var defaultCodeBlockParser = &codeBlockParser{} 14 | 15 | // NewCodeBlockParser returns a new BlockParser that 16 | // parses code blocks. 17 | func NewCodeBlockParser() BlockParser { 18 | return defaultCodeBlockParser 19 | } 20 | 21 | func (b *codeBlockParser) Trigger() []byte { 22 | return nil 23 | } 24 | 25 | func (b *codeBlockParser) Open(parent ast.Node, reader text.Reader, pc Context) (ast.Node, State) { 26 | line, segment := reader.PeekLine() 27 | pos, padding := util.IndentPosition(line, reader.LineOffset(), 4) 28 | if pos < 0 || util.IsBlank(line) { 29 | return nil, NoChildren 30 | } 31 | node := ast.NewCodeBlock() 32 | reader.AdvanceAndSetPadding(pos, padding) 33 | _, segment = reader.PeekLine() 34 | node.Lines().Append(segment) 35 | reader.Advance(segment.Len() - 1) 36 | return node, NoChildren 37 | 38 | } 39 | 40 | func (b *codeBlockParser) Continue(node ast.Node, reader text.Reader, pc Context) State { 41 | line, segment := reader.PeekLine() 42 | if util.IsBlank(line) { 43 | node.Lines().Append(segment.TrimLeftSpaceWidth(4, reader.Source())) 44 | return Continue | NoChildren 45 | } 46 | pos, padding := util.IndentPosition(line, reader.LineOffset(), 4) 47 | if pos < 0 { 48 | return Close 49 | } 50 | reader.AdvanceAndSetPadding(pos, padding) 51 | _, segment = reader.PeekLine() 52 | node.Lines().Append(segment) 53 | reader.Advance(segment.Len() - 1) 54 | return Continue | NoChildren 55 | } 56 | 57 | func (b *codeBlockParser) Close(node ast.Node, reader text.Reader, pc Context) { 58 | // trim trailing blank lines 59 | lines := node.Lines() 60 | length := lines.Len() - 1 61 | source := reader.Source() 62 | for length >= 0 { 63 | line := lines.At(length) 64 | if util.IsBlank(line.Value(source)) { 65 | length-- 66 | } else { 67 | break 68 | } 69 | } 70 | lines.SetSliced(0, length+1) 71 | } 72 | 73 | func (b *codeBlockParser) CanInterruptParagraph() bool { 74 | return false 75 | } 76 | 77 | func (b *codeBlockParser) CanAcceptIndentedLine() bool { 78 | return true 79 | } 80 | -------------------------------------------------------------------------------- /parser/code_span.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "github.com/enkogu/goldmark/ast" 5 | "github.com/enkogu/goldmark/text" 6 | "github.com/enkogu/goldmark/util" 7 | ) 8 | 9 | type codeSpanParser struct { 10 | } 11 | 12 | var defaultCodeSpanParser = &codeSpanParser{} 13 | 14 | // NewCodeSpanParser return a new InlineParser that parses inline codes 15 | // surrounded by '`' . 16 | func NewCodeSpanParser() InlineParser { 17 | return defaultCodeSpanParser 18 | } 19 | 20 | func (s *codeSpanParser) Trigger() []byte { 21 | return []byte{'`'} 22 | } 23 | 24 | func (s *codeSpanParser) Parse(parent ast.Node, block text.Reader, pc Context) ast.Node { 25 | line, startSegment := block.PeekLine() 26 | opener := 0 27 | for ; opener < len(line) && line[opener] == '`'; opener++ { 28 | } 29 | block.Advance(opener) 30 | l, pos := block.Position() 31 | node := ast.NewCodeSpan() 32 | for { 33 | line, segment := block.PeekLine() 34 | if line == nil { 35 | block.SetPosition(l, pos) 36 | return ast.NewTextSegment(startSegment.WithStop(startSegment.Start + opener)) 37 | } 38 | for i := 0; i < len(line); i++ { 39 | c := line[i] 40 | if c == '`' { 41 | oldi := i 42 | for ; i < len(line) && line[i] == '`'; i++ { 43 | } 44 | closure := i - oldi 45 | if closure == opener && (i >= len(line) || line[i] != '`') { 46 | segment = segment.WithStop(segment.Start + i - closure) 47 | if !segment.IsEmpty() { 48 | node.AppendChild(node, ast.NewRawTextSegment(segment)) 49 | } 50 | block.Advance(i) 51 | goto end 52 | } 53 | } 54 | } 55 | if !util.IsBlank(line) { 56 | node.AppendChild(node, ast.NewRawTextSegment(segment)) 57 | } 58 | block.AdvanceLine() 59 | } 60 | end: 61 | if !node.IsBlank(block.Source()) { 62 | // trim first halfspace and last halfspace 63 | segment := node.FirstChild().(*ast.Text).Segment 64 | shouldTrimmed := true 65 | if !(!segment.IsEmpty() && block.Source()[segment.Start] == ' ') { 66 | shouldTrimmed = false 67 | } 68 | segment = node.LastChild().(*ast.Text).Segment 69 | if !(!segment.IsEmpty() && block.Source()[segment.Stop-1] == ' ') { 70 | shouldTrimmed = false 71 | } 72 | if shouldTrimmed { 73 | t := node.FirstChild().(*ast.Text) 74 | segment := t.Segment 75 | t.Segment = segment.WithStart(segment.Start + 1) 76 | t = node.LastChild().(*ast.Text) 77 | segment = node.LastChild().(*ast.Text).Segment 78 | t.Segment = segment.WithStop(segment.Stop - 1) 79 | } 80 | 81 | } 82 | return node 83 | } 84 | -------------------------------------------------------------------------------- /parser/delimiter.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "unicode" 7 | 8 | "github.com/enkogu/goldmark/ast" 9 | "github.com/enkogu/goldmark/text" 10 | "github.com/enkogu/goldmark/util" 11 | ) 12 | 13 | // A DelimiterProcessor interface provides a set of functions about 14 | // Deliiter nodes. 15 | type DelimiterProcessor interface { 16 | // IsDelimiter returns true if given character is a delimiter, otherwise false. 17 | IsDelimiter(byte) bool 18 | 19 | // CanOpenCloser returns true if given opener can close given closer, otherwise false. 20 | CanOpenCloser(opener, closer *Delimiter) bool 21 | 22 | // OnMatch will be called when new matched delimiter found. 23 | // OnMatch should return a new Node correspond to the matched delimiter. 24 | OnMatch(consumes int) ast.Node 25 | } 26 | 27 | // A Delimiter struct represents a delimiter like '*' of the Markdown text. 28 | type Delimiter struct { 29 | ast.BaseInline 30 | 31 | Segment text.Segment 32 | 33 | // CanOpen is set true if this delimiter can open a span for a new node. 34 | // See https://spec.commonmark.org/0.29/#can-open-emphasis for details. 35 | CanOpen bool 36 | 37 | // CanClose is set true if this delimiter can close a span for a new node. 38 | // See https://spec.commonmark.org/0.29/#can-open-emphasis for details. 39 | CanClose bool 40 | 41 | // Length is a remaining length of this delmiter. 42 | Length int 43 | 44 | // OriginalLength is a original length of this delimiter. 45 | OriginalLength int 46 | 47 | // Char is a character of this delimiter. 48 | Char byte 49 | 50 | // PreviousDelimiter is a previous sibling delimiter node of this delimiter. 51 | PreviousDelimiter *Delimiter 52 | 53 | // NextDelimiter is a next sibling delimiter node of this delimiter. 54 | NextDelimiter *Delimiter 55 | 56 | // Processor is a DelimiterProcessor associated with this delimiter. 57 | Processor DelimiterProcessor 58 | } 59 | 60 | // Inline implements Inline.Inline. 61 | func (d *Delimiter) Inline() {} 62 | 63 | // Dump implements Node.Dump. 64 | func (d *Delimiter) Dump(source []byte, level int) { 65 | fmt.Printf("%sDelimiter: \"%s\"\n", strings.Repeat(" ", level), string(d.Text(source))) 66 | } 67 | 68 | var kindDelimiter = ast.NewNodeKind("Delimiter") 69 | 70 | // Kind implements Node.Kind 71 | func (d *Delimiter) Kind() ast.NodeKind { 72 | return kindDelimiter 73 | } 74 | 75 | // Text implements Node.Text 76 | func (d *Delimiter) Text(source []byte) []byte { 77 | return d.Segment.Value(source) 78 | } 79 | 80 | // ConsumeCharacters consumes delimiters. 81 | func (d *Delimiter) ConsumeCharacters(n int) { 82 | d.Length -= n 83 | d.Segment = d.Segment.WithStop(d.Segment.Start + d.Length) 84 | } 85 | 86 | // CalcComsumption calculates how many characters should be used for opening 87 | // a new span correspond to given closer. 88 | func (d *Delimiter) CalcComsumption(closer *Delimiter) int { 89 | if (d.CanClose || closer.CanOpen) && (d.OriginalLength+closer.OriginalLength)%3 == 0 && closer.OriginalLength%3 != 0 { 90 | return 0 91 | } 92 | if d.Length >= 2 && closer.Length >= 2 { 93 | return 2 94 | } 95 | return 1 96 | } 97 | 98 | // NewDelimiter returns a new Delimiter node. 99 | func NewDelimiter(canOpen, canClose bool, length int, char byte, processor DelimiterProcessor) *Delimiter { 100 | c := &Delimiter{ 101 | BaseInline: ast.BaseInline{}, 102 | CanOpen: canOpen, 103 | CanClose: canClose, 104 | Length: length, 105 | OriginalLength: length, 106 | Char: char, 107 | PreviousDelimiter: nil, 108 | NextDelimiter: nil, 109 | Processor: processor, 110 | } 111 | return c 112 | } 113 | 114 | // ScanDelimiter scans a delimiter by given DelimiterProcessor. 115 | func ScanDelimiter(line []byte, before rune, min int, processor DelimiterProcessor) *Delimiter { 116 | i := 0 117 | c := line[i] 118 | j := i 119 | if !processor.IsDelimiter(c) { 120 | return nil 121 | } 122 | for ; j < len(line) && c == line[j]; j++ { 123 | } 124 | if (j - i) >= min { 125 | after := rune(' ') 126 | if j != len(line) { 127 | after = util.ToRune(line, j) 128 | } 129 | 130 | isLeft, isRight, canOpen, canClose := false, false, false, false 131 | beforeIsPunctuation := unicode.IsPunct(before) 132 | beforeIsWhitespace := unicode.IsSpace(before) 133 | afterIsPunctuation := unicode.IsPunct(after) 134 | afterIsWhitespace := unicode.IsSpace(after) 135 | 136 | isLeft = !afterIsWhitespace && 137 | (!afterIsPunctuation || beforeIsWhitespace || beforeIsPunctuation) 138 | isRight = !beforeIsWhitespace && 139 | (!beforeIsPunctuation || afterIsWhitespace || afterIsPunctuation) 140 | 141 | if line[i] == '_' { 142 | canOpen = isLeft && (!isRight || beforeIsPunctuation) 143 | canClose = isRight && (!isLeft || afterIsPunctuation) 144 | } else { 145 | canOpen = isLeft 146 | canClose = isRight 147 | } 148 | return NewDelimiter(canOpen, canClose, j-i, c, processor) 149 | } 150 | return nil 151 | } 152 | 153 | // ProcessDelimiters processes the delimiter list in the context. 154 | // Processing will be stop when reaching the bottom. 155 | // 156 | // If you implement an inline parser that can have other inline nodes as 157 | // children, you should call this function when nesting span has closed. 158 | func ProcessDelimiters(bottom ast.Node, pc Context) { 159 | if pc.LastDelimiter() == nil { 160 | return 161 | } 162 | var closer *Delimiter 163 | if bottom != nil { 164 | for c := pc.LastDelimiter().PreviousSibling(); c != nil; { 165 | if d, ok := c.(*Delimiter); ok { 166 | closer = d 167 | } 168 | prev := c.PreviousSibling() 169 | if prev == bottom { 170 | break 171 | } 172 | c = prev 173 | } 174 | } else { 175 | closer = pc.FirstDelimiter() 176 | } 177 | if closer == nil { 178 | pc.ClearDelimiters(bottom) 179 | return 180 | } 181 | for closer != nil { 182 | if !closer.CanClose { 183 | closer = closer.NextDelimiter 184 | continue 185 | } 186 | consume := 0 187 | found := false 188 | maybeOpener := false 189 | var opener *Delimiter 190 | for opener = closer.PreviousDelimiter; opener != nil; opener = opener.PreviousDelimiter { 191 | if opener.CanOpen && opener.Processor.CanOpenCloser(opener, closer) { 192 | maybeOpener = true 193 | consume = opener.CalcComsumption(closer) 194 | if consume > 0 { 195 | found = true 196 | break 197 | } 198 | } 199 | } 200 | if !found { 201 | if !maybeOpener && !closer.CanOpen { 202 | pc.RemoveDelimiter(closer) 203 | } 204 | closer = closer.NextDelimiter 205 | continue 206 | } 207 | opener.ConsumeCharacters(consume) 208 | closer.ConsumeCharacters(consume) 209 | 210 | node := opener.Processor.OnMatch(consume) 211 | 212 | parent := opener.Parent() 213 | child := opener.NextSibling() 214 | 215 | for child != nil && child != closer { 216 | next := child.NextSibling() 217 | node.AppendChild(node, child) 218 | child = next 219 | } 220 | parent.InsertAfter(parent, opener, node) 221 | 222 | for c := opener.NextDelimiter; c != nil && c != closer; { 223 | next := c.NextDelimiter 224 | pc.RemoveDelimiter(c) 225 | c = next 226 | } 227 | 228 | if opener.Length == 0 { 229 | pc.RemoveDelimiter(opener) 230 | } 231 | 232 | if closer.Length == 0 { 233 | next := closer.NextDelimiter 234 | pc.RemoveDelimiter(closer) 235 | closer = next 236 | } 237 | } 238 | pc.ClearDelimiters(bottom) 239 | } 240 | -------------------------------------------------------------------------------- /parser/emphasis.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "github.com/enkogu/goldmark/ast" 5 | "github.com/enkogu/goldmark/text" 6 | ) 7 | 8 | type emphasisDelimiterProcessor struct { 9 | } 10 | 11 | func (p *emphasisDelimiterProcessor) IsDelimiter(b byte) bool { 12 | return b == '*' || b == '_' 13 | } 14 | 15 | func (p *emphasisDelimiterProcessor) CanOpenCloser(opener, closer *Delimiter) bool { 16 | return opener.Char == closer.Char 17 | } 18 | 19 | func (p *emphasisDelimiterProcessor) OnMatch(consumes int) ast.Node { 20 | return ast.NewEmphasis(consumes) 21 | } 22 | 23 | var defaultEmphasisDelimiterProcessor = &emphasisDelimiterProcessor{} 24 | 25 | type emphasisParser struct { 26 | } 27 | 28 | var defaultEmphasisParser = &emphasisParser{} 29 | 30 | // NewEmphasisParser return a new InlineParser that parses emphasises. 31 | func NewEmphasisParser() InlineParser { 32 | return defaultEmphasisParser 33 | } 34 | 35 | func (s *emphasisParser) Trigger() []byte { 36 | return []byte{'*', '_'} 37 | } 38 | 39 | func (s *emphasisParser) Parse(parent ast.Node, block text.Reader, pc Context) ast.Node { 40 | before := block.PrecendingCharacter() 41 | line, segment := block.PeekLine() 42 | node := ScanDelimiter(line, before, 1, defaultEmphasisDelimiterProcessor) 43 | if node == nil { 44 | return nil 45 | } 46 | node.Segment = segment.WithStop(segment.Start + node.OriginalLength) 47 | block.Advance(node.OriginalLength) 48 | pc.PushDelimiter(node) 49 | return node 50 | } 51 | -------------------------------------------------------------------------------- /parser/fcode_block.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "bytes" 5 | 6 | "github.com/enkogu/goldmark/ast" 7 | "github.com/enkogu/goldmark/text" 8 | "github.com/enkogu/goldmark/util" 9 | ) 10 | 11 | type fencedCodeBlockParser struct { 12 | } 13 | 14 | var defaultFencedCodeBlockParser = &fencedCodeBlockParser{} 15 | 16 | // NewFencedCodeBlockParser returns a new BlockParser that 17 | // parses fenced code blocks. 18 | func NewFencedCodeBlockParser() BlockParser { 19 | return defaultFencedCodeBlockParser 20 | } 21 | 22 | type fenceData struct { 23 | char byte 24 | indent int 25 | length int 26 | node ast.Node 27 | } 28 | 29 | var fencedCodeBlockInfoKey = NewContextKey() 30 | 31 | func (b *fencedCodeBlockParser) Trigger() []byte { 32 | return []byte{'~', '`'} 33 | } 34 | 35 | func (b *fencedCodeBlockParser) Open(parent ast.Node, reader text.Reader, pc Context) (ast.Node, State) { 36 | line, segment := reader.PeekLine() 37 | pos := pc.BlockOffset() 38 | if pos < 0 || (line[pos] != '`' && line[pos] != '~') { 39 | return nil, NoChildren 40 | } 41 | findent := pos 42 | fenceChar := line[pos] 43 | i := pos 44 | for ; i < len(line) && line[i] == fenceChar; i++ { 45 | } 46 | oFenceLength := i - pos 47 | if oFenceLength < 3 { 48 | return nil, NoChildren 49 | } 50 | var info *ast.Text 51 | if i < len(line)-1 { 52 | rest := line[i:] 53 | left := util.TrimLeftSpaceLength(rest) 54 | right := util.TrimRightSpaceLength(rest) 55 | if left < len(rest)-right { 56 | infoStart, infoStop := segment.Start-segment.Padding+i+left, segment.Stop-right 57 | value := rest[left : len(rest)-right] 58 | if fenceChar == '`' && bytes.IndexByte(value, '`') > -1 { 59 | return nil, NoChildren 60 | } else if infoStart != infoStop { 61 | info = ast.NewTextSegment(text.NewSegment(infoStart, infoStop)) 62 | } 63 | } 64 | } 65 | node := ast.NewFencedCodeBlock(info) 66 | pc.Set(fencedCodeBlockInfoKey, &fenceData{fenceChar, findent, oFenceLength, node}) 67 | return node, NoChildren 68 | 69 | } 70 | 71 | func (b *fencedCodeBlockParser) Continue(node ast.Node, reader text.Reader, pc Context) State { 72 | line, segment := reader.PeekLine() 73 | fdata := pc.Get(fencedCodeBlockInfoKey).(*fenceData) 74 | w, pos := util.IndentWidth(line, reader.LineOffset()) 75 | if w < 4 { 76 | i := pos 77 | for ; i < len(line) && line[i] == fdata.char; i++ { 78 | } 79 | length := i - pos 80 | if length >= fdata.length && util.IsBlank(line[i:]) { 81 | newline := 1 82 | if line[len(line)-1] != '\n' { 83 | newline = 0 84 | } 85 | reader.Advance(segment.Stop - segment.Start - newline - segment.Padding) 86 | return Close 87 | } 88 | } 89 | pos, padding := util.DedentPositionPadding(line, reader.LineOffset(), segment.Padding, fdata.indent) 90 | 91 | seg := text.NewSegmentPadding(segment.Start+pos, segment.Stop, padding) 92 | node.Lines().Append(seg) 93 | reader.AdvanceAndSetPadding(segment.Stop-segment.Start-pos-1, padding) 94 | return Continue | NoChildren 95 | } 96 | 97 | func (b *fencedCodeBlockParser) Close(node ast.Node, reader text.Reader, pc Context) { 98 | fdata := pc.Get(fencedCodeBlockInfoKey).(*fenceData) 99 | if fdata.node == node { 100 | pc.Set(fencedCodeBlockInfoKey, nil) 101 | } 102 | } 103 | 104 | func (b *fencedCodeBlockParser) CanInterruptParagraph() bool { 105 | return true 106 | } 107 | 108 | func (b *fencedCodeBlockParser) CanAcceptIndentedLine() bool { 109 | return false 110 | } 111 | -------------------------------------------------------------------------------- /parser/html_block.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "bytes" 5 | "regexp" 6 | "strings" 7 | 8 | "github.com/enkogu/goldmark/ast" 9 | "github.com/enkogu/goldmark/text" 10 | "github.com/enkogu/goldmark/util" 11 | ) 12 | 13 | var allowedBlockTags = map[string]bool{ 14 | "address": true, 15 | "article": true, 16 | "aside": true, 17 | "base": true, 18 | "basefont": true, 19 | "blockquote": true, 20 | "body": true, 21 | "caption": true, 22 | "center": true, 23 | "col": true, 24 | "colgroup": true, 25 | "dd": true, 26 | "details": true, 27 | "dialog": true, 28 | "dir": true, 29 | "div": true, 30 | "dl": true, 31 | "dt": true, 32 | "fieldset": true, 33 | "figcaption": true, 34 | "figure": true, 35 | "footer": true, 36 | "form": true, 37 | "frame": true, 38 | "frameset": true, 39 | "h1": true, 40 | "h2": true, 41 | "h3": true, 42 | "h4": true, 43 | "h5": true, 44 | "h6": true, 45 | "head": true, 46 | "header": true, 47 | "hr": true, 48 | "html": true, 49 | "iframe": true, 50 | "legend": true, 51 | "li": true, 52 | "link": true, 53 | "main": true, 54 | "menu": true, 55 | "menuitem": true, 56 | "meta": true, 57 | "nav": true, 58 | "noframes": true, 59 | "ol": true, 60 | "optgroup": true, 61 | "option": true, 62 | "p": true, 63 | "param": true, 64 | "section": true, 65 | "source": true, 66 | "summary": true, 67 | "table": true, 68 | "tbody": true, 69 | "td": true, 70 | "tfoot": true, 71 | "th": true, 72 | "thead": true, 73 | "title": true, 74 | "tr": true, 75 | "track": true, 76 | "ul": true, 77 | } 78 | 79 | var htmlBlockType1OpenRegexp = regexp.MustCompile(`(?i)^[ ]{0,3}<(script|pre|style)(?:\s.*|>.*|/>.*|)\n?$`) 80 | var htmlBlockType1CloseRegexp = regexp.MustCompile(`(?i)^[ ]{0,3}(?:[^ ].*|).*`) 81 | 82 | var htmlBlockType2OpenRegexp = regexp.MustCompile(`^[ ]{0,3}'} 84 | 85 | var htmlBlockType3OpenRegexp = regexp.MustCompile(`^[ ]{0,3}<\?`) 86 | var htmlBlockType3Close = []byte{'?', '>'} 87 | 88 | var htmlBlockType4OpenRegexp = regexp.MustCompile(`^[ ]{0,3}'} 90 | 91 | var htmlBlockType5OpenRegexp = regexp.MustCompile(`^[ ]{0,3}<\!\[CDATA\[`) 92 | var htmlBlockType5Close = []byte{']', ']', '>'} 93 | 94 | var htmlBlockType6Regexp = regexp.MustCompile(`^[ ]{0,3}.*|/>.*|)\n?$`) 95 | 96 | var htmlBlockType7Regexp = regexp.MustCompile(`^[ ]{0,3}<(/)?([a-zA-Z0-9]+)(` + attributePattern + `*)(:?>|/>)\s*\n?$`) 97 | 98 | type htmlBlockParser struct { 99 | } 100 | 101 | var defaultHTMLBlockParser = &htmlBlockParser{} 102 | 103 | // NewHTMLBlockParser return a new BlockParser that can parse html 104 | // blocks. 105 | func NewHTMLBlockParser() BlockParser { 106 | return defaultHTMLBlockParser 107 | } 108 | 109 | func (b *htmlBlockParser) Trigger() []byte { 110 | return []byte{'<'} 111 | } 112 | 113 | func (b *htmlBlockParser) Open(parent ast.Node, reader text.Reader, pc Context) (ast.Node, State) { 114 | var node *ast.HTMLBlock 115 | line, segment := reader.PeekLine() 116 | last := pc.LastOpenedBlock().Node 117 | if pos := pc.BlockOffset(); pos < 0 || line[pos] != '<' { 118 | return nil, NoChildren 119 | } 120 | 121 | tagName := "" 122 | if m := htmlBlockType1OpenRegexp.FindSubmatchIndex(line); m != nil { 123 | tagName = string(line[m[2]:m[3]]) 124 | node = ast.NewHTMLBlock(ast.HTMLBlockType1) 125 | } else if htmlBlockType2OpenRegexp.Match(line) { 126 | node = ast.NewHTMLBlock(ast.HTMLBlockType2) 127 | } else if htmlBlockType3OpenRegexp.Match(line) { 128 | node = ast.NewHTMLBlock(ast.HTMLBlockType3) 129 | } else if htmlBlockType4OpenRegexp.Match(line) { 130 | node = ast.NewHTMLBlock(ast.HTMLBlockType4) 131 | } else if htmlBlockType5OpenRegexp.Match(line) { 132 | node = ast.NewHTMLBlock(ast.HTMLBlockType5) 133 | } else if match := htmlBlockType7Regexp.FindSubmatchIndex(line); match != nil { 134 | isCloseTag := match[2] > -1 && bytes.Equal(line[match[2]:match[3]], []byte("/")) 135 | hasAttr := match[6] != match[7] 136 | tagName = strings.ToLower(string(line[match[4]:match[5]])) 137 | _, ok := allowedBlockTags[tagName] 138 | if ok { 139 | node = ast.NewHTMLBlock(ast.HTMLBlockType6) 140 | } else if tagName != "script" && tagName != "style" && tagName != "pre" && !ast.IsParagraph(last) && !(isCloseTag && hasAttr) { // type 7 can not interrupt paragraph 141 | node = ast.NewHTMLBlock(ast.HTMLBlockType7) 142 | } 143 | } 144 | if node == nil { 145 | if match := htmlBlockType6Regexp.FindSubmatchIndex(line); match != nil { 146 | tagName = string(line[match[2]:match[3]]) 147 | _, ok := allowedBlockTags[strings.ToLower(tagName)] 148 | if ok { 149 | node = ast.NewHTMLBlock(ast.HTMLBlockType6) 150 | } 151 | } 152 | } 153 | if node != nil { 154 | reader.Advance(segment.Len() - 1) 155 | node.Lines().Append(segment) 156 | return node, NoChildren 157 | } 158 | return nil, NoChildren 159 | } 160 | 161 | func (b *htmlBlockParser) Continue(node ast.Node, reader text.Reader, pc Context) State { 162 | htmlBlock := node.(*ast.HTMLBlock) 163 | lines := htmlBlock.Lines() 164 | line, segment := reader.PeekLine() 165 | var closurePattern []byte 166 | 167 | switch htmlBlock.HTMLBlockType { 168 | case ast.HTMLBlockType1: 169 | if lines.Len() == 1 { 170 | firstLine := lines.At(0) 171 | if htmlBlockType1CloseRegexp.Match(firstLine.Value(reader.Source())) { 172 | return Close 173 | } 174 | } 175 | if htmlBlockType1CloseRegexp.Match(line) { 176 | htmlBlock.ClosureLine = segment 177 | reader.Advance(segment.Len() - 1) 178 | return Close 179 | } 180 | case ast.HTMLBlockType2: 181 | closurePattern = htmlBlockType2Close 182 | fallthrough 183 | case ast.HTMLBlockType3: 184 | if closurePattern == nil { 185 | closurePattern = htmlBlockType3Close 186 | } 187 | fallthrough 188 | case ast.HTMLBlockType4: 189 | if closurePattern == nil { 190 | closurePattern = htmlBlockType4Close 191 | } 192 | fallthrough 193 | case ast.HTMLBlockType5: 194 | if closurePattern == nil { 195 | closurePattern = htmlBlockType5Close 196 | } 197 | 198 | if lines.Len() == 1 { 199 | firstLine := lines.At(0) 200 | if bytes.Contains(firstLine.Value(reader.Source()), closurePattern) { 201 | return Close 202 | } 203 | } 204 | if bytes.Contains(line, closurePattern) { 205 | htmlBlock.ClosureLine = segment 206 | reader.Advance(segment.Len() - 1) 207 | return Close 208 | } 209 | 210 | case ast.HTMLBlockType6, ast.HTMLBlockType7: 211 | if util.IsBlank(line) { 212 | return Close 213 | } 214 | } 215 | node.Lines().Append(segment) 216 | reader.Advance(segment.Len() - 1) 217 | return Continue | NoChildren 218 | } 219 | 220 | func (b *htmlBlockParser) Close(node ast.Node, reader text.Reader, pc Context) { 221 | // nothing to do 222 | } 223 | 224 | func (b *htmlBlockParser) CanInterruptParagraph() bool { 225 | return true 226 | } 227 | 228 | func (b *htmlBlockParser) CanAcceptIndentedLine() bool { 229 | return false 230 | } 231 | -------------------------------------------------------------------------------- /parser/link.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | "strings" 7 | 8 | "github.com/enkogu/goldmark/ast" 9 | "github.com/enkogu/goldmark/text" 10 | "github.com/enkogu/goldmark/util" 11 | ) 12 | 13 | var linkLabelStateKey = NewContextKey() 14 | 15 | type linkLabelState struct { 16 | ast.BaseInline 17 | 18 | Segment text.Segment 19 | 20 | IsImage bool 21 | 22 | Prev *linkLabelState 23 | 24 | Next *linkLabelState 25 | 26 | First *linkLabelState 27 | 28 | Last *linkLabelState 29 | } 30 | 31 | func newLinkLabelState(segment text.Segment, isImage bool) *linkLabelState { 32 | return &linkLabelState{ 33 | Segment: segment, 34 | IsImage: isImage, 35 | } 36 | } 37 | 38 | func (s *linkLabelState) Text(source []byte) []byte { 39 | return s.Segment.Value(source) 40 | } 41 | 42 | func (s *linkLabelState) Dump(source []byte, level int) { 43 | fmt.Printf("%slinkLabelState: \"%s\"\n", strings.Repeat(" ", level), s.Text(source)) 44 | } 45 | 46 | var kindLinkLabelState = ast.NewNodeKind("LinkLabelState") 47 | 48 | func (s *linkLabelState) Kind() ast.NodeKind { 49 | return kindLinkLabelState 50 | } 51 | 52 | func pushLinkLabelState(pc Context, v *linkLabelState) { 53 | tlist := pc.Get(linkLabelStateKey) 54 | var list *linkLabelState 55 | if tlist == nil { 56 | list = v 57 | v.First = v 58 | v.Last = v 59 | pc.Set(linkLabelStateKey, list) 60 | } else { 61 | list = tlist.(*linkLabelState) 62 | l := list.Last 63 | list.Last = v 64 | l.Next = v 65 | v.Prev = l 66 | } 67 | } 68 | 69 | func removeLinkLabelState(pc Context, d *linkLabelState) { 70 | tlist := pc.Get(linkLabelStateKey) 71 | var list *linkLabelState 72 | if tlist == nil { 73 | return 74 | } 75 | list = tlist.(*linkLabelState) 76 | 77 | if d.Prev == nil { 78 | list = d.Next 79 | if list != nil { 80 | list.First = d 81 | list.Last = d.Last 82 | list.Prev = nil 83 | pc.Set(linkLabelStateKey, list) 84 | } else { 85 | pc.Set(linkLabelStateKey, nil) 86 | } 87 | } else { 88 | d.Prev.Next = d.Next 89 | if d.Next != nil { 90 | d.Next.Prev = d.Prev 91 | } 92 | } 93 | if list != nil && d.Next == nil { 94 | list.Last = d.Prev 95 | } 96 | d.Next = nil 97 | d.Prev = nil 98 | d.First = nil 99 | d.Last = nil 100 | } 101 | 102 | type linkParser struct { 103 | } 104 | 105 | var defaultLinkParser = &linkParser{} 106 | 107 | // NewLinkParser return a new InlineParser that parses links. 108 | func NewLinkParser() InlineParser { 109 | return defaultLinkParser 110 | } 111 | 112 | func (s *linkParser) Trigger() []byte { 113 | return []byte{'!', '[', ']'} 114 | } 115 | 116 | var linkDestinationRegexp = regexp.MustCompile(`\s*([^\s].+)`) 117 | var linkTitleRegexp = regexp.MustCompile(`\s+(\)|["'\(].+)`) 118 | var linkBottom = NewContextKey() 119 | 120 | func (s *linkParser) Parse(parent ast.Node, block text.Reader, pc Context) ast.Node { 121 | line, segment := block.PeekLine() 122 | if line[0] == '!' { 123 | if len(line) > 1 && line[1] == '[' { 124 | block.Advance(1) 125 | pc.Set(linkBottom, pc.LastDelimiter()) 126 | return processLinkLabelOpen(block, segment.Start+1, true, pc) 127 | } 128 | return nil 129 | } 130 | if line[0] == '[' { 131 | pc.Set(linkBottom, pc.LastDelimiter()) 132 | return processLinkLabelOpen(block, segment.Start, false, pc) 133 | } 134 | 135 | // line[0] == ']' 136 | tlist := pc.Get(linkLabelStateKey) 137 | if tlist == nil { 138 | return nil 139 | } 140 | last := tlist.(*linkLabelState).Last 141 | if last == nil { 142 | return nil 143 | } 144 | block.Advance(1) 145 | removeLinkLabelState(pc, last) 146 | if s.containsLink(last) { // a link in a link text is not allowed 147 | ast.MergeOrReplaceTextSegment(last.Parent(), last, last.Segment) 148 | return nil 149 | } 150 | labelValue := block.Value(text.NewSegment(last.Segment.Start+1, segment.Start)) 151 | if util.IsBlank(labelValue) && !last.IsImage { 152 | ast.MergeOrReplaceTextSegment(last.Parent(), last, last.Segment) 153 | return nil 154 | } 155 | 156 | c := block.Peek() 157 | l, pos := block.Position() 158 | var link *ast.Link 159 | var hasValue bool 160 | if c == '(' { // normal link 161 | link = s.parseLink(parent, last, block, pc) 162 | } else if c == '[' { // reference link 163 | link, hasValue = s.parseReferenceLink(parent, last, block, pc) 164 | if link == nil && hasValue { 165 | ast.MergeOrReplaceTextSegment(last.Parent(), last, last.Segment) 166 | return nil 167 | } 168 | } 169 | 170 | if link == nil { 171 | // maybe shortcut reference link 172 | block.SetPosition(l, pos) 173 | ssegment := text.NewSegment(last.Segment.Stop, segment.Start) 174 | maybeReference := block.Value(ssegment) 175 | ref, ok := pc.Reference(util.ToLinkReference(maybeReference)) 176 | if !ok { 177 | ast.MergeOrReplaceTextSegment(last.Parent(), last, last.Segment) 178 | return nil 179 | } 180 | link = ast.NewLink() 181 | s.processLinkLabel(parent, link, last, pc) 182 | link.Title = ref.Title() 183 | link.Destination = ref.Destination() 184 | } 185 | if last.IsImage { 186 | last.Parent().RemoveChild(last.Parent(), last) 187 | return ast.NewImage(link) 188 | } 189 | last.Parent().RemoveChild(last.Parent(), last) 190 | return link 191 | } 192 | 193 | func (s *linkParser) containsLink(last *linkLabelState) bool { 194 | if last.IsImage { 195 | return false 196 | } 197 | var c ast.Node 198 | for c = last; c != nil; c = c.NextSibling() { 199 | if _, ok := c.(*ast.Link); ok { 200 | return true 201 | } 202 | } 203 | return false 204 | } 205 | 206 | func processLinkLabelOpen(block text.Reader, pos int, isImage bool, pc Context) *linkLabelState { 207 | start := pos 208 | if isImage { 209 | start-- 210 | } 211 | state := newLinkLabelState(text.NewSegment(start, pos+1), isImage) 212 | pushLinkLabelState(pc, state) 213 | block.Advance(1) 214 | return state 215 | } 216 | 217 | func (s *linkParser) processLinkLabel(parent ast.Node, link *ast.Link, last *linkLabelState, pc Context) { 218 | var bottom ast.Node 219 | if v := pc.Get(linkBottom); v != nil { 220 | bottom = v.(ast.Node) 221 | } 222 | pc.Set(linkBottom, nil) 223 | ProcessDelimiters(bottom, pc) 224 | for c := last.NextSibling(); c != nil; { 225 | next := c.NextSibling() 226 | parent.RemoveChild(parent, c) 227 | link.AppendChild(link, c) 228 | c = next 229 | } 230 | } 231 | 232 | func (s *linkParser) parseReferenceLink(parent ast.Node, last *linkLabelState, block text.Reader, pc Context) (*ast.Link, bool) { 233 | _, orgpos := block.Position() 234 | block.Advance(1) // skip '[' 235 | line, segment := block.PeekLine() 236 | endIndex := util.FindClosure(line, '[', ']', false, true) 237 | if endIndex < 0 { 238 | return nil, false 239 | } 240 | 241 | block.Advance(endIndex + 1) 242 | ssegment := segment.WithStop(segment.Start + endIndex) 243 | maybeReference := block.Value(ssegment) 244 | if util.IsBlank(maybeReference) { // collapsed reference link 245 | ssegment = text.NewSegment(last.Segment.Stop, orgpos.Start-1) 246 | maybeReference = block.Value(ssegment) 247 | } 248 | 249 | ref, ok := pc.Reference(util.ToLinkReference(maybeReference)) 250 | if !ok { 251 | return nil, true 252 | } 253 | 254 | link := ast.NewLink() 255 | s.processLinkLabel(parent, link, last, pc) 256 | link.Title = ref.Title() 257 | link.Destination = ref.Destination() 258 | return link, true 259 | } 260 | 261 | func (s *linkParser) parseLink(parent ast.Node, last *linkLabelState, block text.Reader, pc Context) *ast.Link { 262 | block.Advance(1) // skip '(' 263 | block.SkipSpaces() 264 | var title []byte 265 | var destination []byte 266 | var ok bool 267 | if block.Peek() == ')' { // empty link like '[link]()' 268 | block.Advance(1) 269 | } else { 270 | destination, ok = parseLinkDestination(block) 271 | if !ok { 272 | return nil 273 | } 274 | block.SkipSpaces() 275 | if block.Peek() == ')' { 276 | block.Advance(1) 277 | } else { 278 | title, ok = parseLinkTitle(block) 279 | if !ok { 280 | return nil 281 | } 282 | block.SkipSpaces() 283 | if block.Peek() == ')' { 284 | block.Advance(1) 285 | } else { 286 | return nil 287 | } 288 | } 289 | } 290 | 291 | link := ast.NewLink() 292 | s.processLinkLabel(parent, link, last, pc) 293 | link.Destination = destination 294 | link.Title = title 295 | return link 296 | } 297 | 298 | func parseLinkDestination(block text.Reader) ([]byte, bool) { 299 | block.SkipSpaces() 300 | line, _ := block.PeekLine() 301 | buf := []byte{} 302 | if block.Peek() == '<' { 303 | i := 1 304 | for i < len(line) { 305 | c := line[i] 306 | if c == '\\' && i < len(line)-1 && util.IsPunct(line[i+1]) { 307 | buf = append(buf, '\\', line[i+1]) 308 | i += 2 309 | continue 310 | } else if c == '>' { 311 | block.Advance(i + 1) 312 | return line[1:i], true 313 | } 314 | buf = append(buf, c) 315 | i++ 316 | } 317 | return nil, false 318 | } 319 | opened := 0 320 | i := 0 321 | for i < len(line) { 322 | c := line[i] 323 | if c == '\\' && i < len(line)-1 && util.IsPunct(line[i+1]) { 324 | buf = append(buf, '\\', line[i+1]) 325 | i += 2 326 | continue 327 | } else if c == '(' { 328 | opened++ 329 | } else if c == ')' { 330 | opened-- 331 | if opened < 0 { 332 | break 333 | } 334 | } else if util.IsSpace(c) { 335 | break 336 | } 337 | buf = append(buf, c) 338 | i++ 339 | } 340 | block.Advance(i) 341 | return line[:i], len(line[:i]) != 0 342 | } 343 | 344 | func parseLinkTitle(block text.Reader) ([]byte, bool) { 345 | block.SkipSpaces() 346 | opener := block.Peek() 347 | if opener != '"' && opener != '\'' && opener != '(' { 348 | return nil, false 349 | } 350 | closer := opener 351 | if opener == '(' { 352 | closer = ')' 353 | } 354 | line, _ := block.PeekLine() 355 | pos := util.FindClosure(line[1:], opener, closer, false, true) 356 | if pos < 0 { 357 | return nil, false 358 | } 359 | pos += 2 // opener + closer 360 | block.Advance(pos) 361 | return line[1 : pos-1], true 362 | } 363 | 364 | func (s *linkParser) CloseBlock(parent ast.Node, block text.Reader, pc Context) { 365 | tlist := pc.Get(linkLabelStateKey) 366 | if tlist == nil { 367 | return 368 | } 369 | for s := tlist.(*linkLabelState); s != nil; { 370 | next := s.Next 371 | removeLinkLabelState(pc, s) 372 | s.Parent().ReplaceChild(s.Parent(), s, ast.NewTextSegment(s.Segment)) 373 | s = next 374 | } 375 | } 376 | -------------------------------------------------------------------------------- /parser/link_ref.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "github.com/enkogu/goldmark/ast" 5 | "github.com/enkogu/goldmark/text" 6 | "github.com/enkogu/goldmark/util" 7 | ) 8 | 9 | type linkReferenceParagraphTransformer struct { 10 | } 11 | 12 | // LinkReferenceParagraphTransformer is a ParagraphTransformer implementation 13 | // that parses and extracts link reference from paragraphs. 14 | var LinkReferenceParagraphTransformer = &linkReferenceParagraphTransformer{} 15 | 16 | func (p *linkReferenceParagraphTransformer) Transform(node *ast.Paragraph, reader text.Reader, pc Context) { 17 | lines := node.Lines() 18 | block := text.NewBlockReader(reader.Source(), lines) 19 | removes := [][2]int{} 20 | for { 21 | start, end := parseLinkReferenceDefinition(block, pc) 22 | if start > -1 { 23 | if start == end { 24 | end++ 25 | } 26 | removes = append(removes, [2]int{start, end}) 27 | continue 28 | } 29 | break 30 | } 31 | 32 | offset := 0 33 | for _, remove := range removes { 34 | if lines.Len() == 0 { 35 | break 36 | } 37 | s := lines.Sliced(remove[1]-offset, lines.Len()) 38 | lines.SetSliced(0, remove[0]-offset) 39 | lines.AppendAll(s) 40 | offset = remove[1] 41 | } 42 | 43 | if lines.Len() == 0 { 44 | t := ast.NewTextBlock() 45 | t.SetBlankPreviousLines(node.HasBlankPreviousLines()) 46 | node.Parent().ReplaceChild(node.Parent(), node, t) 47 | return 48 | } 49 | 50 | node.SetLines(lines) 51 | } 52 | 53 | func parseLinkReferenceDefinition(block text.Reader, pc Context) (int, int) { 54 | block.SkipSpaces() 55 | line, segment := block.PeekLine() 56 | if line == nil { 57 | return -1, -1 58 | } 59 | startLine, _ := block.Position() 60 | width, pos := util.IndentWidth(line, 0) 61 | if width > 3 { 62 | return -1, -1 63 | } 64 | if width != 0 { 65 | pos++ 66 | } 67 | if line[pos] != '[' { 68 | return -1, -1 69 | } 70 | open := segment.Start + pos + 1 71 | closes := -1 72 | block.Advance(pos + 1) 73 | for { 74 | line, segment = block.PeekLine() 75 | if line == nil { 76 | return -1, -1 77 | } 78 | closure := util.FindClosure(line, '[', ']', false, false) 79 | if closure > -1 { 80 | closes = segment.Start + closure 81 | next := closure + 1 82 | if next >= len(line) || line[next] != ':' { 83 | return -1, -1 84 | } 85 | block.Advance(next + 1) 86 | break 87 | } 88 | block.AdvanceLine() 89 | } 90 | if closes < 0 { 91 | return -1, -1 92 | } 93 | label := block.Value(text.NewSegment(open, closes)) 94 | if util.IsBlank(label) { 95 | return -1, -1 96 | } 97 | block.SkipSpaces() 98 | destination, ok := parseLinkDestination(block) 99 | if !ok { 100 | return -1, -1 101 | } 102 | line, segment = block.PeekLine() 103 | isNewLine := line == nil || util.IsBlank(line) 104 | 105 | endLine, _ := block.Position() 106 | _, spaces, _ := block.SkipSpaces() 107 | opener := block.Peek() 108 | if opener != '"' && opener != '\'' && opener != '(' { 109 | if !isNewLine { 110 | return -1, -1 111 | } 112 | ref := NewReference(label, destination, nil) 113 | pc.AddReference(ref) 114 | return startLine, endLine + 1 115 | } 116 | if spaces == 0 { 117 | return -1, -1 118 | } 119 | block.Advance(1) 120 | open = -1 121 | closes = -1 122 | closer := opener 123 | if opener == '(' { 124 | closer = ')' 125 | } 126 | for { 127 | line, segment = block.PeekLine() 128 | if line == nil { 129 | return -1, -1 130 | } 131 | if open < 0 { 132 | open = segment.Start 133 | } 134 | closure := util.FindClosure(line, opener, closer, false, true) 135 | if closure > -1 { 136 | closes = segment.Start + closure 137 | block.Advance(closure + 1) 138 | break 139 | } 140 | block.AdvanceLine() 141 | } 142 | if closes < 0 { 143 | return -1, -1 144 | } 145 | 146 | line, segment = block.PeekLine() 147 | if line != nil && !util.IsBlank(line) { 148 | if !isNewLine { 149 | return -1, -1 150 | } 151 | title := block.Value(text.NewSegment(open, closes)) 152 | ref := NewReference(label, destination, title) 153 | pc.AddReference(ref) 154 | return startLine, endLine 155 | } 156 | 157 | title := block.Value(text.NewSegment(open, closes)) 158 | 159 | endLine, _ = block.Position() 160 | ref := NewReference(label, destination, title) 161 | pc.AddReference(ref) 162 | return startLine, endLine + 1 163 | } 164 | -------------------------------------------------------------------------------- /parser/list.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "strconv" 5 | 6 | "github.com/enkogu/goldmark/ast" 7 | "github.com/enkogu/goldmark/text" 8 | "github.com/enkogu/goldmark/util" 9 | ) 10 | 11 | type listItemType int 12 | 13 | const ( 14 | notList listItemType = iota 15 | bulletList 16 | orderedList 17 | ) 18 | 19 | // Same as 20 | // `^(([ ]*)([\-\*\+]))(\s+.*)?\n?$`.FindSubmatchIndex or 21 | // `^(([ ]*)(\d{1,9}[\.\)]))(\s+.*)?\n?$`.FindSubmatchIndex 22 | func parseListItem(line []byte) ([6]int, listItemType) { 23 | i := 0 24 | l := len(line) 25 | ret := [6]int{} 26 | for ; i < l && line[i] == ' '; i++ { 27 | c := line[i] 28 | if c == '\t' { 29 | return ret, notList 30 | } 31 | } 32 | if i > 3 { 33 | return ret, notList 34 | } 35 | ret[0] = 0 36 | ret[1] = i 37 | ret[2] = i 38 | var typ listItemType 39 | if i < l && (line[i] == '-' || line[i] == '*' || line[i] == '+') { 40 | i++ 41 | ret[3] = i 42 | typ = bulletList 43 | } else if i < l { 44 | for ; i < l && util.IsNumeric(line[i]); i++ { 45 | } 46 | ret[3] = i 47 | if ret[3] == ret[2] || ret[3]-ret[2] > 9 { 48 | return ret, notList 49 | } 50 | if i < l && (line[i] == '.' || line[i] == ')') { 51 | i++ 52 | ret[3] = i 53 | } else { 54 | return ret, notList 55 | } 56 | typ = orderedList 57 | } else { 58 | return ret, notList 59 | } 60 | if i < l && line[i] != '\n' { 61 | w, _ := util.IndentWidth(line[i:], 0) 62 | if w == 0 { 63 | return ret, notList 64 | } 65 | } 66 | if i >= l { 67 | ret[4] = -1 68 | ret[5] = -1 69 | return ret, typ 70 | } 71 | ret[4] = i 72 | ret[5] = len(line) 73 | if line[ret[5]-1] == '\n' && line[i] != '\n' { 74 | ret[5]-- 75 | } 76 | return ret, typ 77 | } 78 | 79 | func matchesListItem(source []byte, strict bool) ([6]int, listItemType) { 80 | m, typ := parseListItem(source) 81 | if typ != notList && (!strict || strict && m[1] < 4) { 82 | return m, typ 83 | } 84 | return m, notList 85 | } 86 | 87 | func calcListOffset(source []byte, match [6]int) int { 88 | offset := 0 89 | if match[4] < 0 || util.IsBlank(source[match[4]:]) { // list item starts with a blank line 90 | offset = 1 91 | } else { 92 | offset, _ = util.IndentWidth(source[match[4]:], match[4]) 93 | if offset > 4 { // offseted codeblock 94 | offset = 1 95 | } 96 | } 97 | return offset 98 | } 99 | 100 | func lastOffset(node ast.Node) int { 101 | lastChild := node.LastChild() 102 | if lastChild != nil { 103 | return lastChild.(*ast.ListItem).Offset 104 | } 105 | return 0 106 | } 107 | 108 | type listParser struct { 109 | } 110 | 111 | var defaultListParser = &listParser{} 112 | 113 | // NewListParser returns a new BlockParser that 114 | // parses lists. 115 | // This parser must take precedence over the ListItemParser. 116 | func NewListParser() BlockParser { 117 | return defaultListParser 118 | } 119 | 120 | func (b *listParser) Trigger() []byte { 121 | return []byte{'-', '+', '*', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'} 122 | } 123 | 124 | func (b *listParser) Open(parent ast.Node, reader text.Reader, pc Context) (ast.Node, State) { 125 | last := pc.LastOpenedBlock().Node 126 | if _, lok := last.(*ast.List); lok || pc.Get(skipListParser) != nil { 127 | pc.Set(skipListParser, nil) 128 | return nil, NoChildren 129 | } 130 | line, _ := reader.PeekLine() 131 | match, typ := matchesListItem(line, true) 132 | if typ == notList { 133 | return nil, NoChildren 134 | } 135 | start := -1 136 | if typ == orderedList { 137 | number := line[match[2] : match[3]-1] 138 | start, _ = strconv.Atoi(string(number)) 139 | } 140 | 141 | if ast.IsParagraph(last) && last.Parent() == parent { 142 | // we allow only lists starting with 1 to interrupt paragraphs. 143 | if typ == orderedList && start != 1 { 144 | return nil, NoChildren 145 | } 146 | //an empty list item cannot interrupt a paragraph: 147 | if match[5]-match[4] == 1 { 148 | return nil, NoChildren 149 | } 150 | } 151 | 152 | marker := line[match[3]-1] 153 | node := ast.NewList(marker) 154 | if start > -1 { 155 | node.Start = start 156 | } 157 | return node, HasChildren 158 | } 159 | 160 | func (b *listParser) Continue(node ast.Node, reader text.Reader, pc Context) State { 161 | list := node.(*ast.List) 162 | line, _ := reader.PeekLine() 163 | if util.IsBlank(line) { 164 | // A list item can begin with at most one blank line 165 | if node.ChildCount() == 1 && node.LastChild().ChildCount() == 0 { 166 | return Close 167 | } 168 | return Continue | HasChildren 169 | } 170 | // Thematic Breaks take precedence over lists 171 | if isThematicBreak(line, reader.LineOffset()) { 172 | isHeading := false 173 | last := pc.LastOpenedBlock().Node 174 | if ast.IsParagraph(last) { 175 | c, ok := matchesSetextHeadingBar(line) 176 | if ok && c == '-' { 177 | isHeading = true 178 | } 179 | } 180 | if !isHeading { 181 | return Close 182 | } 183 | } 184 | 185 | // "offset" means a width that bar indicates. 186 | // - aaaaaaaa 187 | // |----| 188 | // 189 | // If the indent is less than the last offset like 190 | // - a 191 | // - b <--- current line 192 | // it maybe a new child of the list. 193 | offset := lastOffset(node) 194 | indent, _ := util.IndentWidth(line, reader.LineOffset()) 195 | 196 | if indent < offset { 197 | if indent < 4 { 198 | match, typ := matchesListItem(line, false) // may have a leading spaces more than 3 199 | if typ != notList && match[1]-offset < 4 { 200 | marker := line[match[3]-1] 201 | if !list.CanContinue(marker, typ == orderedList) { 202 | return Close 203 | } 204 | return Continue | HasChildren 205 | } 206 | } 207 | return Close 208 | } 209 | return Continue | HasChildren 210 | } 211 | 212 | func (b *listParser) Close(node ast.Node, reader text.Reader, pc Context) { 213 | list := node.(*ast.List) 214 | 215 | for c := node.FirstChild(); c != nil && list.IsTight; c = c.NextSibling() { 216 | if c.FirstChild() != nil && c.FirstChild() != c.LastChild() { 217 | for c1 := c.FirstChild().NextSibling(); c1 != nil; c1 = c1.NextSibling() { 218 | if bl, ok := c1.(ast.Node); ok && bl.HasBlankPreviousLines() { 219 | list.IsTight = false 220 | break 221 | } 222 | } 223 | } 224 | if c != node.FirstChild() { 225 | if bl, ok := c.(ast.Node); ok && bl.HasBlankPreviousLines() { 226 | list.IsTight = false 227 | } 228 | } 229 | } 230 | 231 | if list.IsTight { 232 | for child := node.FirstChild(); child != nil; child = child.NextSibling() { 233 | for gc := child.FirstChild(); gc != nil; gc = gc.NextSibling() { 234 | paragraph, ok := gc.(*ast.Paragraph) 235 | if ok { 236 | textBlock := ast.NewTextBlock() 237 | textBlock.SetLines(paragraph.Lines()) 238 | child.ReplaceChild(child, paragraph, textBlock) 239 | } 240 | } 241 | } 242 | } 243 | } 244 | 245 | func (b *listParser) CanInterruptParagraph() bool { 246 | return true 247 | } 248 | 249 | func (b *listParser) CanAcceptIndentedLine() bool { 250 | return false 251 | } 252 | -------------------------------------------------------------------------------- /parser/list_item.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "github.com/enkogu/goldmark/ast" 5 | "github.com/enkogu/goldmark/text" 6 | "github.com/enkogu/goldmark/util" 7 | ) 8 | 9 | type listItemParser struct { 10 | } 11 | 12 | var defaultListItemParser = &listItemParser{} 13 | 14 | // NewListItemParser returns a new BlockParser that 15 | // parses list items. 16 | func NewListItemParser() BlockParser { 17 | return defaultListItemParser 18 | } 19 | 20 | var skipListParser = NewContextKey() 21 | var skipListParserValue interface{} = true 22 | 23 | func (b *listItemParser) Trigger() []byte { 24 | return []byte{'-', '+', '*', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'} 25 | } 26 | 27 | func (b *listItemParser) Open(parent ast.Node, reader text.Reader, pc Context) (ast.Node, State) { 28 | list, lok := parent.(*ast.List) 29 | if !lok { // list item must be a child of a list 30 | return nil, NoChildren 31 | } 32 | offset := lastOffset(list) 33 | line, _ := reader.PeekLine() 34 | match, typ := matchesListItem(line, false) 35 | if typ == notList { 36 | return nil, NoChildren 37 | } 38 | if match[1]-offset > 3 { 39 | return nil, NoChildren 40 | } 41 | itemOffset := calcListOffset(line, match) 42 | node := ast.NewListItem(match[3] + itemOffset) 43 | if match[4] < 0 || match[5]-match[4] == 1 { 44 | return node, NoChildren 45 | } 46 | 47 | pos, padding := util.IndentPosition(line[match[4]:], match[4], itemOffset) 48 | child := match[3] + pos 49 | reader.AdvanceAndSetPadding(child, padding) 50 | return node, HasChildren 51 | } 52 | 53 | func (b *listItemParser) Continue(node ast.Node, reader text.Reader, pc Context) State { 54 | line, _ := reader.PeekLine() 55 | if util.IsBlank(line) { 56 | return Continue | HasChildren 57 | } 58 | 59 | indent, _ := util.IndentWidth(line, reader.LineOffset()) 60 | offset := lastOffset(node.Parent()) 61 | if indent < offset && indent < 4 { 62 | _, typ := matchesListItem(line, true) 63 | // new list item found 64 | if typ != notList { 65 | pc.Set(skipListParser, skipListParserValue) 66 | } 67 | return Close 68 | } 69 | pos, padding := util.IndentPosition(line, reader.LineOffset(), offset) 70 | reader.AdvanceAndSetPadding(pos, padding) 71 | 72 | return Continue | HasChildren 73 | } 74 | 75 | func (b *listItemParser) Close(node ast.Node, reader text.Reader, pc Context) { 76 | // nothing to do 77 | } 78 | 79 | func (b *listItemParser) CanInterruptParagraph() bool { 80 | return true 81 | } 82 | 83 | func (b *listItemParser) CanAcceptIndentedLine() bool { 84 | return false 85 | } 86 | -------------------------------------------------------------------------------- /parser/paragraph.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "github.com/enkogu/goldmark/ast" 5 | "github.com/enkogu/goldmark/text" 6 | ) 7 | 8 | type paragraphParser struct { 9 | } 10 | 11 | var defaultParagraphParser = ¶graphParser{} 12 | 13 | // NewParagraphParser returns a new BlockParser that 14 | // parses paragraphs. 15 | func NewParagraphParser() BlockParser { 16 | return defaultParagraphParser 17 | } 18 | 19 | func (b *paragraphParser) Trigger() []byte { 20 | return nil 21 | } 22 | 23 | func (b *paragraphParser) Open(parent ast.Node, reader text.Reader, pc Context) (ast.Node, State) { 24 | _, segment := reader.PeekLine() 25 | segment = segment.TrimLeftSpace(reader.Source()) 26 | if segment.IsEmpty() { 27 | return nil, NoChildren 28 | } 29 | node := ast.NewParagraph() 30 | node.Lines().Append(segment) 31 | reader.Advance(segment.Len() - 1) 32 | return node, NoChildren 33 | } 34 | 35 | func (b *paragraphParser) Continue(node ast.Node, reader text.Reader, pc Context) State { 36 | _, segment := reader.PeekLine() 37 | segment = segment.TrimLeftSpace(reader.Source()) 38 | if segment.IsEmpty() { 39 | return Close 40 | } 41 | node.Lines().Append(segment) 42 | reader.Advance(segment.Len() - 1) 43 | return Continue | NoChildren 44 | } 45 | 46 | func (b *paragraphParser) Close(node ast.Node, reader text.Reader, pc Context) { 47 | parent := node.Parent() 48 | if parent == nil { 49 | // paragraph has been transformed 50 | return 51 | } 52 | lines := node.Lines() 53 | if lines.Len() != 0 { 54 | // trim trailing spaces 55 | length := lines.Len() 56 | lastLine := node.Lines().At(length - 1) 57 | node.Lines().Set(length-1, lastLine.TrimRightSpace(reader.Source())) 58 | } 59 | if lines.Len() == 0 { 60 | node.Parent().RemoveChild(node.Parent(), node) 61 | return 62 | } 63 | } 64 | 65 | func (b *paragraphParser) CanInterruptParagraph() bool { 66 | return false 67 | } 68 | 69 | func (b *paragraphParser) CanAcceptIndentedLine() bool { 70 | return false 71 | } 72 | -------------------------------------------------------------------------------- /parser/raw_html.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "bytes" 5 | "regexp" 6 | 7 | "github.com/enkogu/goldmark/ast" 8 | "github.com/enkogu/goldmark/text" 9 | "github.com/enkogu/goldmark/util" 10 | ) 11 | 12 | type rawHTMLParser struct { 13 | } 14 | 15 | var defaultRawHTMLParser = &rawHTMLParser{} 16 | 17 | // NewRawHTMLParser return a new InlineParser that can parse 18 | // inline htmls 19 | func NewRawHTMLParser() InlineParser { 20 | return defaultRawHTMLParser 21 | } 22 | 23 | func (s *rawHTMLParser) Trigger() []byte { 24 | return []byte{'<'} 25 | } 26 | 27 | func (s *rawHTMLParser) Parse(parent ast.Node, block text.Reader, pc Context) ast.Node { 28 | line, _ := block.PeekLine() 29 | if len(line) > 1 && util.IsAlphaNumeric(line[1]) { 30 | return s.parseMultiLineRegexp(openTagRegexp, block, pc) 31 | } 32 | if len(line) > 2 && line[1] == '/' && util.IsAlphaNumeric(line[2]) { 33 | return s.parseMultiLineRegexp(closeTagRegexp, block, pc) 34 | } 35 | if bytes.HasPrefix(line, []byte("|`) 55 | var processingInstructionRegexp = regexp.MustCompile(`^(?:<\?).*?(?:\?>)`) 56 | var declRegexp = regexp.MustCompile(`^]*>`) 57 | var cdataRegexp = regexp.MustCompile(``) 58 | 59 | func (s *rawHTMLParser) parseSingleLineRegexp(reg *regexp.Regexp, block text.Reader, pc Context) ast.Node { 60 | line, segment := block.PeekLine() 61 | match := reg.FindSubmatchIndex(line) 62 | if match == nil { 63 | return nil 64 | } 65 | node := ast.NewRawHTML() 66 | node.Segments.Append(segment.WithStop(segment.Start + match[1])) 67 | block.Advance(match[1]) 68 | return node 69 | } 70 | 71 | var dummyMatch = [][]byte{} 72 | 73 | func (s *rawHTMLParser) parseMultiLineRegexp(reg *regexp.Regexp, block text.Reader, pc Context) ast.Node { 74 | sline, ssegment := block.Position() 75 | if block.Match(reg) { 76 | node := ast.NewRawHTML() 77 | eline, esegment := block.Position() 78 | block.SetPosition(sline, ssegment) 79 | for { 80 | line, segment := block.PeekLine() 81 | if line == nil { 82 | break 83 | } 84 | l, _ := block.Position() 85 | start := segment.Start 86 | if l == sline { 87 | start = ssegment.Start 88 | } 89 | end := segment.Stop 90 | if l == eline { 91 | end = esegment.Start 92 | } 93 | 94 | node.Segments.Append(text.NewSegment(start, end)) 95 | if l == eline { 96 | block.Advance(end - start) 97 | break 98 | } else { 99 | block.AdvanceLine() 100 | } 101 | } 102 | return node 103 | } 104 | return nil 105 | } 106 | 107 | func (s *rawHTMLParser) CloseBlock(parent ast.Node, pc Context) { 108 | // nothing to do 109 | } 110 | -------------------------------------------------------------------------------- /parser/setext_headings.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "github.com/enkogu/goldmark/ast" 5 | "github.com/enkogu/goldmark/text" 6 | "github.com/enkogu/goldmark/util" 7 | ) 8 | 9 | var temporaryParagraphKey = NewContextKey() 10 | 11 | type setextHeadingParser struct { 12 | HeadingConfig 13 | } 14 | 15 | func matchesSetextHeadingBar(line []byte) (byte, bool) { 16 | start := 0 17 | end := len(line) 18 | space := util.TrimLeftLength(line, []byte{' '}) 19 | if space > 3 { 20 | return 0, false 21 | } 22 | start += space 23 | level1 := util.TrimLeftLength(line[start:end], []byte{'='}) 24 | c := byte('=') 25 | var level2 int 26 | if level1 == 0 { 27 | level2 = util.TrimLeftLength(line[start:end], []byte{'-'}) 28 | c = '-' 29 | } 30 | if util.IsSpace(line[end-1]) { 31 | end -= util.TrimRightSpaceLength(line[start:end]) 32 | } 33 | if !((level1 > 0 && start+level1 == end) || (level2 > 0 && start+level2 == end)) { 34 | return 0, false 35 | } 36 | return c, true 37 | } 38 | 39 | // NewSetextHeadingParser return a new BlockParser that can parse Setext headings. 40 | func NewSetextHeadingParser(opts ...HeadingOption) BlockParser { 41 | p := &setextHeadingParser{} 42 | for _, o := range opts { 43 | o.SetHeadingOption(&p.HeadingConfig) 44 | } 45 | return p 46 | } 47 | 48 | func (b *setextHeadingParser) Trigger() []byte { 49 | return []byte{'-', '='} 50 | } 51 | 52 | func (b *setextHeadingParser) Open(parent ast.Node, reader text.Reader, pc Context) (ast.Node, State) { 53 | last := pc.LastOpenedBlock().Node 54 | if last == nil { 55 | return nil, NoChildren 56 | } 57 | paragraph, ok := last.(*ast.Paragraph) 58 | if !ok || paragraph.Parent() != parent { 59 | return nil, NoChildren 60 | } 61 | line, segment := reader.PeekLine() 62 | c, ok := matchesSetextHeadingBar(line) 63 | if !ok { 64 | return nil, NoChildren 65 | } 66 | level := 1 67 | if c == '-' { 68 | level = 2 69 | } 70 | node := ast.NewHeading(level) 71 | node.Lines().Append(segment) 72 | pc.Set(temporaryParagraphKey, last) 73 | return node, NoChildren | RequireParagraph 74 | } 75 | 76 | func (b *setextHeadingParser) Continue(node ast.Node, reader text.Reader, pc Context) State { 77 | return Close 78 | } 79 | 80 | func (b *setextHeadingParser) Close(node ast.Node, reader text.Reader, pc Context) { 81 | heading := node.(*ast.Heading) 82 | segment := node.Lines().At(0) 83 | heading.Lines().Clear() 84 | tmp := pc.Get(temporaryParagraphKey).(*ast.Paragraph) 85 | pc.Set(temporaryParagraphKey, nil) 86 | if tmp.Lines().Len() == 0 { 87 | next := heading.NextSibling() 88 | segment = segment.TrimLeftSpace(reader.Source()) 89 | if next == nil || !ast.IsParagraph(next) { 90 | para := ast.NewParagraph() 91 | para.Lines().Append(segment) 92 | heading.Parent().InsertAfter(heading.Parent(), heading, para) 93 | } else { 94 | next.(ast.Node).Lines().Unshift(segment) 95 | } 96 | heading.Parent().RemoveChild(heading.Parent(), heading) 97 | } else { 98 | heading.SetLines(tmp.Lines()) 99 | heading.SetBlankPreviousLines(tmp.HasBlankPreviousLines()) 100 | tp := tmp.Parent() 101 | if tp != nil { 102 | tp.RemoveChild(tp, tmp) 103 | } 104 | } 105 | 106 | if b.Attribute { 107 | parseLastLineAttributes(node, reader, pc) 108 | } 109 | 110 | if b.AutoHeadingID { 111 | _, ok := node.AttributeString("id") 112 | if !ok { 113 | generateAutoHeadingID(heading, reader, pc) 114 | } 115 | } 116 | } 117 | 118 | func (b *setextHeadingParser) CanInterruptParagraph() bool { 119 | return true 120 | } 121 | 122 | func (b *setextHeadingParser) CanAcceptIndentedLine() bool { 123 | return false 124 | } 125 | -------------------------------------------------------------------------------- /parser/thematic_break.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "github.com/enkogu/goldmark/ast" 5 | "github.com/enkogu/goldmark/text" 6 | "github.com/enkogu/goldmark/util" 7 | ) 8 | 9 | type thematicBreakPraser struct { 10 | } 11 | 12 | var defaultThematicBreakPraser = &thematicBreakPraser{} 13 | 14 | // NewThematicBreakParser returns a new BlockParser that 15 | // parses thematic breaks. 16 | func NewThematicBreakParser() BlockParser { 17 | return defaultThematicBreakPraser 18 | } 19 | 20 | func isThematicBreak(line []byte, offset int) bool { 21 | w, pos := util.IndentWidth(line, offset) 22 | if w > 3 { 23 | return false 24 | } 25 | mark := byte(0) 26 | count := 0 27 | for i := pos; i < len(line); i++ { 28 | c := line[i] 29 | if util.IsSpace(c) { 30 | continue 31 | } 32 | if mark == 0 { 33 | mark = c 34 | count = 1 35 | if mark == '*' || mark == '-' || mark == '_' { 36 | continue 37 | } 38 | return false 39 | } 40 | if c != mark { 41 | return false 42 | } 43 | count++ 44 | } 45 | return count > 2 46 | } 47 | 48 | func (b *thematicBreakPraser) Trigger() []byte { 49 | return []byte{'-', '*', '_'} 50 | } 51 | 52 | func (b *thematicBreakPraser) Open(parent ast.Node, reader text.Reader, pc Context) (ast.Node, State) { 53 | line, segment := reader.PeekLine() 54 | if isThematicBreak(line, reader.LineOffset()) { 55 | reader.Advance(segment.Len() - 1) 56 | return ast.NewThematicBreak(), NoChildren 57 | } 58 | return nil, NoChildren 59 | } 60 | 61 | func (b *thematicBreakPraser) Continue(node ast.Node, reader text.Reader, pc Context) State { 62 | return Close 63 | } 64 | 65 | func (b *thematicBreakPraser) Close(node ast.Node, reader text.Reader, pc Context) { 66 | // nothing to do 67 | } 68 | 69 | func (b *thematicBreakPraser) CanInterruptParagraph() bool { 70 | return true 71 | } 72 | 73 | func (b *thematicBreakPraser) CanAcceptIndentedLine() bool { 74 | return false 75 | } 76 | -------------------------------------------------------------------------------- /renderer/renderer.go: -------------------------------------------------------------------------------- 1 | // Package renderer renders the given AST to certain formats. 2 | package renderer 3 | 4 | import ( 5 | "bufio" 6 | "github.com/enkogu/goldmark/ast" 7 | "github.com/enkogu/goldmark/renderer/blocks" 8 | "github.com/enkogu/goldmark/util" 9 | "io" 10 | "sync" 11 | ) 12 | 13 | // A Config struct is a data structure that holds configuration of the Renderer. 14 | type Config struct { 15 | Options map[OptionName]interface{} 16 | NodeRenderers util.PrioritizedSlice 17 | } 18 | 19 | // NewConfig returns a new Config 20 | func NewConfig() *Config { 21 | return &Config{ 22 | Options: map[OptionName]interface{}{}, 23 | NodeRenderers: util.PrioritizedSlice{}, 24 | } 25 | } 26 | 27 | // An OptionName is a name of the option. 28 | type OptionName string 29 | 30 | // An Option interface is a functional option type for the Renderer. 31 | type Option interface { 32 | SetConfig(*Config) 33 | } 34 | 35 | type withNodeRenderers struct { 36 | value []util.PrioritizedValue 37 | } 38 | 39 | func (o *withNodeRenderers) SetConfig(c *Config) { 40 | c.NodeRenderers = append(c.NodeRenderers, o.value...) 41 | } 42 | 43 | // WithNodeRenderers is a functional option that allow you to add 44 | // NodeRenderers to the renderer. 45 | func WithNodeRenderers(ps ...util.PrioritizedValue) Option { 46 | return &withNodeRenderers{ps} 47 | } 48 | 49 | type withOption struct { 50 | name OptionName 51 | value interface{} 52 | } 53 | 54 | func (o *withOption) SetConfig(c *Config) { 55 | c.Options[o.name] = o.value 56 | } 57 | 58 | // WithOption is a functional option that allow you to set 59 | // an arbitrary option to the parser. 60 | func WithOption(name OptionName, value interface{}) Option { 61 | return &withOption{name, value} 62 | } 63 | 64 | // A SetOptioner interface sets given option to the object. 65 | type SetOptioner interface { 66 | // SetOption sets given option to the object. 67 | // Unacceptable options may be passed. 68 | // Thus implementations must ignore unacceptable options. 69 | SetOption(name OptionName, value interface{}) 70 | } 71 | 72 | 73 | 74 | // NodeRendererFunc is a function that renders a given node. 75 | type NodeRendererFunc func(rs b.RenderState, source []byte, n ast.Node, entering bool) (ast.WalkStatus, error) 76 | 77 | // A NodeRenderer interface offers NodeRendererFuncs. 78 | type NodeRenderer interface { 79 | // RendererFuncs registers NodeRendererFuncs to given NodeRendererFuncRegisterer. 80 | RegisterFuncs(NodeRendererFuncRegisterer) 81 | } 82 | 83 | // A NodeRendererFuncRegisterer registers 84 | type NodeRendererFuncRegisterer interface { 85 | // Register registers given NodeRendererFunc to this object. 86 | Register(ast.NodeKind, NodeRendererFunc) 87 | } 88 | 89 | // A Renderer interface renders given AST node to given 90 | // writer with given Renderer. 91 | type Renderer interface { 92 | Render(rs blocks.renderState, source []byte, n ast.Node) error 93 | 94 | // AddOptions adds given option to this renderer. 95 | AddOptions(...Option) 96 | } 97 | 98 | type renderer struct { 99 | config *Config 100 | options map[OptionName]interface{} 101 | nodeRendererFuncsTmp map[ast.NodeKind]NodeRendererFunc 102 | maxKind int 103 | nodeRendererFuncs []NodeRendererFunc 104 | initSync sync.Once 105 | 106 | } 107 | 108 | // NewRenderer returns a new Renderer with given options. 109 | func NewRenderer(options ...Option) Renderer { 110 | config := NewConfig() 111 | for _, opt := range options { 112 | opt.SetConfig(config) 113 | } 114 | 115 | r := &renderer{ 116 | options: map[OptionName]interface{}{}, 117 | config: config, 118 | nodeRendererFuncsTmp: map[ast.NodeKind]NodeRendererFunc{}, 119 | } 120 | 121 | return r 122 | } 123 | 124 | func (r *renderer) AddOptions(opts ...Option) { 125 | for _, opt := range opts { 126 | opt.SetConfig(r.config) 127 | } 128 | } 129 | 130 | func (r *renderer) Register(kind ast.NodeKind, v NodeRendererFunc) { 131 | r.nodeRendererFuncsTmp[kind] = v 132 | if int(kind) > r.maxKind { 133 | r.maxKind = int(kind) 134 | } 135 | } 136 | 137 | // Render renders the given AST node to the given writer with the given Renderer. 138 | func (r *renderer) Render(w io.Writer, source []byte, n ast.Node) error { 139 | r.initSync.Do(func() { 140 | r.options = r.config.Options 141 | r.config.NodeRenderers.Sort() 142 | l := len(r.config.NodeRenderers) 143 | for i := l - 1; i >= 0; i-- { 144 | v := r.config.NodeRenderers[i] 145 | nr, _ := v.Value.(NodeRenderer) 146 | if se, ok := v.Value.(SetOptioner); ok { 147 | for oname, ovalue := range r.options { 148 | se.SetOption(oname, ovalue) 149 | } 150 | } 151 | nr.RegisterFuncs(r) 152 | } 153 | r.nodeRendererFuncs = make([]NodeRendererFunc, r.maxKind+1) 154 | for kind, nr := range r.nodeRendererFuncsTmp { 155 | r.nodeRendererFuncs[kind] = nr 156 | } 157 | r.config = nil 158 | r.nodeRendererFuncsTmp = nil 159 | }) 160 | writer, ok := w.(util.BufWriter) 161 | if !ok { 162 | writer = bufio.NewWriter(w) 163 | } 164 | err := ast.Walk(n, func(n ast.Node, entering bool) (ast.WalkStatus, error) { 165 | s := ast.WalkStatus(ast.WalkContinue) 166 | var err error 167 | f := r.nodeRendererFuncs[n.Kind()] 168 | if f != nil { 169 | s, err = f(writer, source, n, entering) 170 | } 171 | return s, err 172 | }) 173 | if err != nil { 174 | return err 175 | } 176 | return writer.Flush() 177 | } 178 | -------------------------------------------------------------------------------- /testutil/testutil.go: -------------------------------------------------------------------------------- 1 | package testutil 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "fmt" 7 | "os" 8 | "runtime/debug" 9 | "strconv" 10 | "strings" 11 | 12 | "github.com/enkogu/goldmark" 13 | "github.com/enkogu/goldmark/util" 14 | ) 15 | 16 | // TestingT is a subset of the functionality provided by testing.T. 17 | type TestingT interface { 18 | Logf(string, ...interface{}) 19 | Skipf(string, ...interface{}) 20 | Errorf(string, ...interface{}) 21 | FailNow() 22 | } 23 | 24 | // MarkdownTestCase represents a test case. 25 | type MarkdownTestCase struct { 26 | No int 27 | Markdown string 28 | Expected string 29 | } 30 | 31 | const attributeSeparator = "//- - - - - - - - -//" 32 | const caseSeparator = "//= = = = = = = = = = = = = = = = = = = = = = = =//" 33 | 34 | // DoTestCaseFile runs test cases in a given file. 35 | func DoTestCaseFile(m goldmark.Markdown, filename string, t TestingT) { 36 | fp, err := os.Open(filename) 37 | if err != nil { 38 | panic(err) 39 | } 40 | defer fp.Close() 41 | 42 | scanner := bufio.NewScanner(fp) 43 | c := MarkdownTestCase{ 44 | No: -1, 45 | Markdown: "", 46 | Expected: "", 47 | } 48 | cases := []MarkdownTestCase{} 49 | line := 0 50 | for scanner.Scan() { 51 | line++ 52 | if util.IsBlank([]byte(scanner.Text())) { 53 | continue 54 | } 55 | c.No, err = strconv.Atoi(scanner.Text()) 56 | if err != nil { 57 | panic(fmt.Sprintf("%s: invalid case No at line %d", filename, line)) 58 | } 59 | if !scanner.Scan() { 60 | panic(fmt.Sprintf("%s: invalid case at line %d", filename, line)) 61 | } 62 | line++ 63 | if scanner.Text() != attributeSeparator { 64 | panic(fmt.Sprintf("%s: invalid separator '%s' at line %d", filename, scanner.Text(), line)) 65 | } 66 | buf := []string{} 67 | for scanner.Scan() { 68 | line++ 69 | text := scanner.Text() 70 | if text == attributeSeparator { 71 | break 72 | } 73 | buf = append(buf, text) 74 | } 75 | c.Markdown = strings.Join(buf, "\n") 76 | buf = []string{} 77 | for scanner.Scan() { 78 | line++ 79 | text := scanner.Text() 80 | if text == caseSeparator { 81 | break 82 | } 83 | buf = append(buf, text) 84 | } 85 | c.Expected = strings.Join(buf, "\n") 86 | cases = append(cases, c) 87 | } 88 | DoTestCases(m, cases, t) 89 | } 90 | 91 | // DoTestCases runs a set of test cases. 92 | func DoTestCases(m goldmark.Markdown, cases []MarkdownTestCase, t TestingT) { 93 | for _, testCase := range cases { 94 | DoTestCase(m, testCase, t) 95 | } 96 | } 97 | 98 | // DoTestCase runs a test case. 99 | func DoTestCase(m goldmark.Markdown, testCase MarkdownTestCase, t TestingT) { 100 | var ok bool 101 | var out bytes.Buffer 102 | defer func() { 103 | if err := recover(); err != nil { 104 | format := `============= case %d ================ 105 | Markdown: 106 | ----------- 107 | %s 108 | 109 | Expected: 110 | ---------- 111 | %s 112 | 113 | Actual 114 | --------- 115 | %v 116 | %s 117 | ` 118 | t.Errorf(format, testCase.No, testCase.Markdown, testCase.Expected, err, debug.Stack()) 119 | } else if !ok { 120 | format := `============= case %d ================ 121 | Markdown: 122 | ----------- 123 | %s 124 | 125 | Expected: 126 | ---------- 127 | %s 128 | 129 | Actual 130 | --------- 131 | %s 132 | ` 133 | t.Errorf(format, testCase.No, testCase.Markdown, testCase.Expected, out.Bytes()) 134 | } 135 | }() 136 | 137 | if err := m.Convert([]byte(testCase.Markdown), &out); err != nil { 138 | panic(err) 139 | } 140 | ok = bytes.Equal(bytes.TrimSpace(out.Bytes()), bytes.TrimSpace([]byte(testCase.Expected))) 141 | } 142 | -------------------------------------------------------------------------------- /testutil/testutil_test.go: -------------------------------------------------------------------------------- 1 | package testutil 2 | 3 | import "testing" 4 | 5 | // This will fail to compile if the TestingT interface is changed in a way 6 | // that doesn't conform to testing.T. 7 | var _ TestingT = (*testing.T)(nil) 8 | -------------------------------------------------------------------------------- /text/segment.go: -------------------------------------------------------------------------------- 1 | package text 2 | 3 | import ( 4 | "bytes" 5 | 6 | "github.com/enkogu/goldmark/util" 7 | ) 8 | 9 | var space = []byte(" ") 10 | 11 | // A Segment struct holds information about source potisions. 12 | type Segment struct { 13 | // Start is a start position of the segment. 14 | Start int 15 | 16 | // Stop is a stop position of the segment. 17 | // This value should be excluded. 18 | Stop int 19 | 20 | // Padding is a padding length of the segment. 21 | Padding int 22 | } 23 | 24 | // NewSegment return a new Segment. 25 | func NewSegment(start, stop int) Segment { 26 | return Segment{ 27 | Start: start, 28 | Stop: stop, 29 | Padding: 0, 30 | } 31 | } 32 | 33 | // NewSegmentPadding returns a new Segment with the given padding. 34 | func NewSegmentPadding(start, stop, n int) Segment { 35 | return Segment{ 36 | Start: start, 37 | Stop: stop, 38 | Padding: n, 39 | } 40 | } 41 | 42 | // Value returns a value of the segment. 43 | func (t *Segment) Value(buffer []byte) []byte { 44 | if t.Padding == 0 { 45 | return buffer[t.Start:t.Stop] 46 | } 47 | result := make([]byte, 0, t.Padding+t.Stop-t.Start+1) 48 | result = append(result, bytes.Repeat(space, t.Padding)...) 49 | return append(result, buffer[t.Start:t.Stop]...) 50 | } 51 | 52 | // Len returns a length of the segment. 53 | func (t *Segment) Len() int { 54 | return t.Stop - t.Start + t.Padding 55 | } 56 | 57 | // Between returns a segment between this segment and the given segment. 58 | func (t *Segment) Between(other Segment) Segment { 59 | if t.Stop != other.Stop { 60 | panic("invalid state") 61 | } 62 | return NewSegmentPadding( 63 | t.Start, 64 | other.Start, 65 | t.Padding-other.Padding, 66 | ) 67 | } 68 | 69 | // IsEmpty returns true if this segment is empty, otherwise false. 70 | func (t *Segment) IsEmpty() bool { 71 | return t.Start >= t.Stop && t.Padding == 0 72 | } 73 | 74 | // TrimRightSpace returns a new segment by slicing off all trailing 75 | // space characters. 76 | func (t *Segment) TrimRightSpace(buffer []byte) Segment { 77 | v := buffer[t.Start:t.Stop] 78 | l := util.TrimRightSpaceLength(v) 79 | if l == len(v) { 80 | return NewSegment(t.Start, t.Start) 81 | } 82 | return NewSegmentPadding(t.Start, t.Stop-l, t.Padding) 83 | } 84 | 85 | // TrimLeftSpace returns a new segment by slicing off all leading 86 | // space characters including padding. 87 | func (t *Segment) TrimLeftSpace(buffer []byte) Segment { 88 | v := buffer[t.Start:t.Stop] 89 | l := util.TrimLeftSpaceLength(v) 90 | return NewSegment(t.Start+l, t.Stop) 91 | } 92 | 93 | // TrimLeftSpaceWidth returns a new segment by slicing off leading space 94 | // characters until the given width. 95 | func (t *Segment) TrimLeftSpaceWidth(width int, buffer []byte) Segment { 96 | padding := t.Padding 97 | for ; width > 0; width-- { 98 | if padding == 0 { 99 | break 100 | } 101 | padding-- 102 | } 103 | if width == 0 { 104 | return NewSegmentPadding(t.Start, t.Stop, padding) 105 | } 106 | text := buffer[t.Start:t.Stop] 107 | start := t.Start 108 | for _, c := range text { 109 | if start >= t.Stop-1 || width <= 0 { 110 | break 111 | } 112 | if c == ' ' { 113 | width-- 114 | } else if c == '\t' { 115 | width -= 4 116 | } else { 117 | break 118 | } 119 | start++ 120 | } 121 | if width < 0 { 122 | padding = width * -1 123 | } 124 | return NewSegmentPadding(start, t.Stop, padding) 125 | } 126 | 127 | // WithStart returns a new Segment with same value except Start. 128 | func (t *Segment) WithStart(v int) Segment { 129 | return NewSegmentPadding(v, t.Stop, t.Padding) 130 | } 131 | 132 | // WithStop returns a new Segment with same value except Stop. 133 | func (t *Segment) WithStop(v int) Segment { 134 | return NewSegmentPadding(t.Start, v, t.Padding) 135 | } 136 | 137 | // ConcatPadding concats the padding to the given slice. 138 | func (t *Segment) ConcatPadding(v []byte) []byte { 139 | if t.Padding > 0 { 140 | return append(v, bytes.Repeat(space, t.Padding)...) 141 | } 142 | return v 143 | } 144 | 145 | // Segments is a collection of the Segment. 146 | type Segments struct { 147 | values []Segment 148 | } 149 | 150 | // NewSegments return a new Segments. 151 | func NewSegments() *Segments { 152 | return &Segments{ 153 | values: nil, 154 | } 155 | } 156 | 157 | // Append appends the given segment after the tail of the collection. 158 | func (s *Segments) Append(t Segment) { 159 | if s.values == nil { 160 | s.values = make([]Segment, 0, 20) 161 | } 162 | s.values = append(s.values, t) 163 | } 164 | 165 | // AppendAll appends all elements of given segments after the tail of the collection. 166 | func (s *Segments) AppendAll(t []Segment) { 167 | if s.values == nil { 168 | s.values = make([]Segment, 0, 20) 169 | } 170 | s.values = append(s.values, t...) 171 | } 172 | 173 | // Len returns the length of the collection. 174 | func (s *Segments) Len() int { 175 | if s.values == nil { 176 | return 0 177 | } 178 | return len(s.values) 179 | } 180 | 181 | // At returns a segment at the given index. 182 | func (s *Segments) At(i int) Segment { 183 | return s.values[i] 184 | } 185 | 186 | // Set sets the given Segment. 187 | func (s *Segments) Set(i int, v Segment) { 188 | s.values[i] = v 189 | } 190 | 191 | // SetSliced replace the collection with a subsliced value. 192 | func (s *Segments) SetSliced(lo, hi int) { 193 | s.values = s.values[lo:hi] 194 | } 195 | 196 | // Sliced returns a subslice of the collection. 197 | func (s *Segments) Sliced(lo, hi int) []Segment { 198 | return s.values[lo:hi] 199 | } 200 | 201 | // Clear delete all element of the collction. 202 | func (s *Segments) Clear() { 203 | s.values = nil 204 | } 205 | 206 | // Unshift insert the given Segment to head of the collection. 207 | func (s *Segments) Unshift(v Segment) { 208 | s.values = append(s.values[0:1], s.values[0:]...) 209 | s.values[0] = v 210 | } 211 | -------------------------------------------------------------------------------- /util/util_safe.go: -------------------------------------------------------------------------------- 1 | // +build appengine,js 2 | 3 | package util 4 | 5 | // BytesToReadOnlyString returns a string converted from given bytes. 6 | func BytesToReadOnlyString(b []byte) string { 7 | return string(b) 8 | } 9 | 10 | // StringToReadOnlyBytes returns bytes converted from given string. 11 | func StringToReadOnlyBytes(s string) []byte { 12 | return []byte(s) 13 | } 14 | -------------------------------------------------------------------------------- /util/util_unsafe.go: -------------------------------------------------------------------------------- 1 | // +build !appengine,!js 2 | 3 | package util 4 | 5 | import ( 6 | "reflect" 7 | "unsafe" 8 | ) 9 | 10 | // BytesToReadOnlyString returns a string converted from given bytes. 11 | func BytesToReadOnlyString(b []byte) string { 12 | return *(*string)(unsafe.Pointer(&b)) 13 | } 14 | 15 | // StringToReadOnlyBytes returns bytes converted from given string. 16 | func StringToReadOnlyBytes(s string) []byte { 17 | sh := (*reflect.StringHeader)(unsafe.Pointer(&s)) 18 | bh := reflect.SliceHeader{Data: sh.Data, Len: sh.Len, Cap: sh.Len} 19 | return *(*[]byte)(unsafe.Pointer(&bh)) 20 | } 21 | --------------------------------------------------------------------------------