├── .gitignore
├── .travis.yml
├── LICENSE
├── README.md
├── cmd
└── godown
│ └── main.go
├── go.mod
├── go.sum
├── godown.go
├── godown_test.go
└── testdata
├── test001.html
├── test001.md
├── test002.html
├── test002.md
├── test003.html
├── test003.md
├── test004.html
├── test004.md
├── test005.html
├── test005.md
├── test006.html
├── test006.md
├── test007.html
├── test007.md
├── test008.html
├── test008.md
├── test009.html
├── test009.md
├── test010.html
├── test010.md
├── test011.html
├── test011.md
├── test012.html
├── test012.md
├── test013.html
├── test013.md
├── test014.html
├── test014.md
├── test015.html
├── test015.md
├── test016.html
├── test016.md
├── test017.html
├── test017.md
├── test018.html
├── test018.md
├── test019.html
├── test019.md
├── test020.html
├── test020.md
├── test021.html
├── test021.md
├── test022.html
├── test022.md
├── test023.html
├── test023.md
├── test024.html
├── test024.md
├── test025.html
└── test025.md
/.gitignore:
--------------------------------------------------------------------------------
1 | *.swp
2 | godown
3 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: go
2 |
3 | go:
4 | - 1.8
5 | - 1.9.3
6 |
7 | env:
8 | secure: "sssm68fvzltcAnFd0Vc+OJV0eOicaTUO4I/CRX9LsnqzSsiNmaFT5o1O0Mx622ApYxCfiG3G53B8wuPzSMQxdPaSiMqzoXNLjD8gURaZBWTXc+kj9WFUoS4KW5L8KF3zrmS1u6Ja9U/elIpNqbpuwqT7sZUUJJM1JR50uEVmtP9oc/iqTKh3JK1HZCkb/PDVKs7xY5AEOhx1x0QOn9SegMUK2b83WeuSbta3Z6Rp4EW3p3WwI1WHZmm8+IYjvbwu18foQSetfro+pXCyDBpw1zLbBTDR8W02VwkH2vECMm4N7GYPmHWNx2lZFqoFp9zY5zCRUQG9KmxqbappalBCsT1ZyesUt7Wp/qYw5W+1Np7/vQhe8eeyyKMzsS7FBq8Imn4JiBPbj/1KAhVoZZKyv0qU4hgxHPZMC/JtVoTkIH3IXvTE88P92z9pFL30afQ692BXPe+XmCBph4zBdH88vksdiky9DWuXJ+O0rDCcLes45ij/wk6psdTPx3IXuMohfkO81F0pveksBYFkff8dXXxCABUzZbPaawEDnLAQKJ1m+oF3UYhPzVwrelNjFDOUq3mxsTU36uyhB9fb8sJ+BmorTD9AvqNvobcwKlQ6TaVJEhHjDJRxho82OG2gof9UbsJF3+6IM8uuUVy3TP7b1o+t8PQ3iNKTB4RUzGjJCOs="
9 |
10 | script:
11 | go test -v -race -coverprofile=coverage.txt -covermode=atomic
12 |
13 | after_success:
14 | - bash <(curl -s https://codecov.io/bash)
15 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2018 Yasuhiro Matsumoto
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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # godown
2 |
3 | [](https://travis-ci.org/mattn/godown)
4 | [](https://codecov.io/gh/mattn/godown)
5 | [](http://godoc.org/github.com/mattn/godown)
6 | [](https://goreportcard.com/report/github.com/mattn/godown)
7 |
8 | Convert HTML into Markdown
9 |
10 | This is work in progress.
11 |
12 | ## Usage
13 |
14 | ```
15 | err := godown.Convert(w, r)
16 | checkError(err)
17 | ```
18 |
19 |
20 | ## Command Line
21 |
22 | ```
23 | $ godown < index.html > index.md
24 | ```
25 |
26 | ## Installation
27 |
28 | ```
29 | $ go get github.com/mattn/godown/cmd/godown
30 | ```
31 |
32 | ## TODO
33 |
34 | * escape strings in HTML
35 |
36 | ## License
37 |
38 | MIT
39 |
40 | ## Author
41 |
42 | Yasuhiro Matsumoto (a.k.a. mattn)
43 |
--------------------------------------------------------------------------------
/cmd/godown/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "flag"
5 | "log"
6 | "os"
7 | "os/exec"
8 | "runtime"
9 | "strings"
10 |
11 | "github.com/mattn/godown"
12 | )
13 |
14 | var (
15 | guesslang = flag.String("g", "", "guesslang")
16 | option *godown.Option
17 | )
18 |
19 | func guesslanger(code string) (string, error) {
20 | var cmd *exec.Cmd
21 | if runtime.GOOS == "windows" {
22 | cmd = exec.Command("cmd", "/c", *guesslang)
23 | } else {
24 | cmd = exec.Command("sh", "-c", *guesslang)
25 | }
26 | cmd.Stdin = strings.NewReader(code)
27 | b, err := cmd.CombinedOutput()
28 | return strings.ToLower(strings.TrimSpace(string(b))), err
29 | }
30 |
31 | func main() {
32 | flag.Parse()
33 | if *guesslang != "" {
34 | option = &godown.Option{GuessLang: guesslanger}
35 | }
36 | option := &godown.Option{GuessLang: guesslanger}
37 | if err := godown.Convert(os.Stdout, os.Stdin, option); err != nil {
38 | log.Fatal(err)
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/mattn/godown
2 |
3 | go 1.14
4 |
5 | require (
6 | github.com/mattn/go-runewidth v0.0.8
7 | golang.org/x/net v0.0.0-20200202094626-16171245cfb2
8 | )
9 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/mattn/go-runewidth v0.0.8 h1:3tS41NlGYSmhhe/8fhGRzc+z3AYCw1Fe1WAyLuujKs0=
2 | github.com/mattn/go-runewidth v0.0.8/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
3 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
4 | golang.org/x/net v0.0.0-20200202094626-16171245cfb2 h1:CCH4IOTTfewWjGOlSp+zGcjutRKlBEZQ6wTn8ozI/nI=
5 | golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
6 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
7 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
8 |
--------------------------------------------------------------------------------
/godown.go:
--------------------------------------------------------------------------------
1 | package godown
2 |
3 | import (
4 | "bytes"
5 | "fmt"
6 | "io"
7 | "regexp"
8 | "strings"
9 | "unicode"
10 |
11 | "github.com/mattn/go-runewidth"
12 |
13 | "golang.org/x/net/html"
14 | )
15 |
16 | // A regex to escape certain characters
17 | // \ : since this is the excape character, become weird if printed literally
18 | // * : Used to start bullet lists, and as a delimiter
19 | // _ : Used as a delimiter
20 | // ( and ) : Used in links and images
21 | // [ and ] : Used in links and images
22 | // < : can be used to mean "raw HTML" which is allowed
23 | // > : Used in raw HTML, also used to define blockquotes
24 | // # : Used for headings
25 | // + : Can be used for unordered lists
26 | // - : Can be used for unordered lists
27 | // ! : Used for images
28 | // ` : Used for code blocks
29 | var escapeRegex = regexp.MustCompile(`(` + `\\|\*|_|\[|\]|\(|\)|<|>|#|\+|-|!|` + "`" + `)`)
30 |
31 | func isChildOf(node *html.Node, name string) bool {
32 | node = node.Parent
33 | return node != nil && node.Type == html.ElementNode && strings.ToLower(node.Data) == name
34 | }
35 |
36 | func hasClass(node *html.Node, clazz string) bool {
37 | for _, attr := range node.Attr {
38 | if attr.Key == "class" {
39 | for _, c := range strings.Fields(attr.Val) {
40 | if c == clazz {
41 | return true
42 | }
43 | }
44 | }
45 | }
46 | return false
47 | }
48 |
49 | func attr(node *html.Node, key string) string {
50 | for _, attr := range node.Attr {
51 | if attr.Key == key {
52 | return attr.Val
53 | }
54 | }
55 | return ""
56 | }
57 |
58 | // Gets the language of a code block based on the class
59 | // See: https://spec.commonmark.org/0.29/#example-112
60 | func langFromClass(node *html.Node) string {
61 | if node.FirstChild == nil || strings.ToLower(node.FirstChild.Data) != "code" {
62 | return ""
63 | }
64 |
65 | fChild := node.FirstChild
66 | classes := strings.Fields(attr(fChild, "class"))
67 | if len(classes) == 0 {
68 | return ""
69 | }
70 |
71 | prefix := "language-"
72 | for _, class := range classes {
73 | if !strings.HasPrefix(class, prefix) {
74 | continue
75 | }
76 | return strings.TrimPrefix(class, prefix)
77 | }
78 |
79 | return ""
80 | }
81 |
82 | func br(node *html.Node, w io.Writer, option *Option) {
83 | node = node.PrevSibling
84 | if node == nil {
85 | return
86 | }
87 |
88 | // If trimspace is set to true, new lines will be ignored in nodes
89 | // so we force a new line when using br()
90 | if option.TrimSpace {
91 | fmt.Fprint(w, "\n")
92 | return
93 | }
94 |
95 | switch node.Type {
96 | case html.TextNode:
97 | text := strings.Trim(node.Data, " \t")
98 | if text != "" && !strings.HasSuffix(text, "\n") {
99 | fmt.Fprint(w, "\n")
100 | }
101 | case html.ElementNode:
102 | switch strings.ToLower(node.Data) {
103 | case "br", "p", "ul", "ol", "div", "blockquote", "h1", "h2", "h3", "h4", "h5", "h6":
104 | fmt.Fprint(w, "\n")
105 | }
106 | }
107 | }
108 |
109 | func table(node *html.Node, w io.Writer, option *Option) {
110 | var list []*html.Node // create a list not to mess up the loop
111 |
112 | for tsection := node.FirstChild; tsection != nil; tsection = tsection.NextSibling {
113 | // if the thead/tbody/tfoot is not explicitly set, it is implicitly set as tbody
114 | if tsection.Type == html.ElementNode {
115 | switch strings.ToLower(tsection.Data) {
116 | case "thead", "tbody", "tfoot":
117 | for tr := tsection.FirstChild; tr != nil; tr = tr.NextSibling {
118 | if strings.TrimSpace(tr.Data) == "" {
119 | continue
120 | }
121 | list = append(list, tr)
122 | }
123 | }
124 | }
125 | }
126 |
127 | // Now we create a new node, add all the
to the node and convert it
128 | newTableNode := new(html.Node)
129 | for _, n := range list {
130 | n.Parent.RemoveChild(n)
131 | newTableNode.AppendChild(n)
132 | }
133 |
134 | tableRows(newTableNode, w, option)
135 | fmt.Fprint(w, "\n")
136 | }
137 |
138 | func tableRows(node *html.Node, w io.Writer, option *Option) {
139 | var rows [][]string
140 | for tr := node.FirstChild; tr != nil; tr = tr.NextSibling {
141 | if tr.Type != html.ElementNode || strings.ToLower(tr.Data) != "tr" {
142 | continue
143 | }
144 | var cols []string
145 | for td := tr.FirstChild; td != nil; td = td.NextSibling {
146 | nodeType := strings.ToLower(td.Data)
147 | if td.Type != html.ElementNode || (nodeType != "td" && nodeType != "th") {
148 | continue
149 | }
150 | var buf bytes.Buffer
151 | walk(td, &buf, 0, option)
152 | cols = append(cols, buf.String())
153 | }
154 | rows = append(rows, cols)
155 | }
156 |
157 | maxcol := 0
158 | for _, cols := range rows {
159 | if len(cols) > maxcol {
160 | maxcol = len(cols)
161 | }
162 | }
163 | widths := make([]int, maxcol)
164 | for _, cols := range rows {
165 | for i := 0; i < maxcol; i++ {
166 | if i < len(cols) {
167 | width := runewidth.StringWidth(cols[i])
168 | if widths[i] < width {
169 | widths[i] = width
170 | }
171 | }
172 | }
173 | }
174 | for i, cols := range rows {
175 | for j := 0; j < maxcol; j++ {
176 | fmt.Fprint(w, "|")
177 | if j < len(cols) {
178 | width := runewidth.StringWidth(cols[j])
179 | fmt.Fprint(w, cols[j])
180 | fmt.Fprint(w, strings.Repeat(" ", widths[j]-width))
181 | } else {
182 | fmt.Fprint(w, strings.Repeat(" ", widths[j]))
183 | }
184 | }
185 | fmt.Fprint(w, "|\n")
186 | if i == 0 {
187 | for j := 0; j < maxcol; j++ {
188 | fmt.Fprint(w, "|")
189 | fmt.Fprint(w, strings.Repeat("-", widths[j]))
190 | }
191 | fmt.Fprint(w, "|\n")
192 | }
193 | }
194 | }
195 |
196 | var emptyElements = []string{
197 | "area",
198 | "base",
199 | "br",
200 | "col",
201 | "embed",
202 | "hr",
203 | "img",
204 | "input",
205 | "keygen",
206 | "link",
207 | "meta",
208 | "param",
209 | "source",
210 | "track",
211 | "wbr",
212 | }
213 |
214 | func raw(node *html.Node, w io.Writer, option *Option) {
215 | html.Render(w, node)
216 | }
217 |
218 | func bq(node *html.Node, w io.Writer, option *Option) {
219 | if node.Type == html.TextNode {
220 | fmt.Fprint(w, strings.Replace(node.Data, "\u00a0", " ", -1))
221 | } else {
222 | for c := node.FirstChild; c != nil; c = c.NextSibling {
223 | bq(c, w, option)
224 | }
225 | }
226 | }
227 |
228 | func pre(node *html.Node, w io.Writer, option *Option) {
229 | if node.Type == html.TextNode {
230 | fmt.Fprint(w, node.Data)
231 | } else {
232 | for c := node.FirstChild; c != nil; c = c.NextSibling {
233 | pre(c, w, option)
234 | }
235 | }
236 | }
237 |
238 | // In the spec, https://spec.commonmark.org/0.29/#delimiter-run
239 | // A left-flanking delimiter run should not followed by Unicode whitespace
240 | // A right-flanking delimiter run should not preceded by Unicode whitespace
241 | // This will wrap the delimiter (such as **) around the non-whitespace contents, but preserve the whitespace
242 | func aroundNonWhitespace(node *html.Node, w io.Writer, nest int, option *Option, before, after string) {
243 | buf := &bytes.Buffer{}
244 |
245 | walk(node, buf, nest, option)
246 | s := buf.String()
247 |
248 | // If the contents are simply whitespace, return without adding any delimiters
249 | if strings.TrimSpace(s) == "" {
250 | fmt.Fprint(w, s)
251 | return
252 | }
253 |
254 | start := 0
255 | for ; start < len(s); start++ {
256 | c := s[start]
257 | if !unicode.IsSpace(rune(c)) {
258 | break
259 | }
260 | }
261 |
262 | stop := len(s)
263 | for ; stop > start; stop-- {
264 | c := s[stop-1]
265 | if !unicode.IsSpace(rune(c)) {
266 | break
267 | }
268 | }
269 |
270 | s = s[:start] + before + s[start:stop] + after + s[stop:]
271 |
272 | fmt.Fprint(w, s)
273 | }
274 |
275 | func walk(node *html.Node, w io.Writer, nest int, option *Option) {
276 | if node.Type == html.TextNode {
277 | if option.TrimSpace && strings.TrimSpace(node.Data) == "" {
278 | return
279 | }
280 |
281 | text := regexp.MustCompile(`[[:space:]][[:space:]]*`).ReplaceAllString(strings.Trim(node.Data, "\t\r\n"), " ")
282 |
283 | if !option.doNotEscape {
284 | text = escapeRegex.ReplaceAllStringFunc(text, func(str string) string {
285 | return `\` + str
286 | })
287 | }
288 | fmt.Fprint(w, text)
289 | }
290 |
291 | italicChar := "_"
292 | if option.ItalicsAsterix {
293 | italicChar = "*"
294 | }
295 |
296 | n := 0
297 | for c := node.FirstChild; c != nil; c = c.NextSibling {
298 | switch c.Type {
299 | case html.CommentNode:
300 | if option.IgnoreComments {
301 | break
302 | }
303 | fmt.Fprint(w, "\n")
306 | case html.ElementNode:
307 | customWalk, ok := option.customRulesMap[strings.ToLower(c.Data)]
308 | if ok {
309 | customWalk(c, w, nest, option)
310 | break
311 | }
312 |
313 | switch strings.ToLower(c.Data) {
314 | case "a":
315 | // Links are invalid in markdown if the link text extends beyond a single line
316 | // So we render the contents and strip any spaces
317 | href := attr(c, "href")
318 | end := fmt.Sprintf("](%s)", href)
319 | title := attr(c, "title")
320 | if title != "" {
321 | end = fmt.Sprintf("](%s %q)", href, title)
322 | }
323 | aroundNonWhitespace(c, w, nest, option, "[", end)
324 | case "b", "strong":
325 | aroundNonWhitespace(c, w, nest, option, "**", "**")
326 | case "i", "em":
327 | aroundNonWhitespace(c, w, nest, option, italicChar, italicChar)
328 | case "del", "s":
329 | aroundNonWhitespace(c, w, nest, option, "~~", "~~")
330 | case "br":
331 | br(c, w, option)
332 | fmt.Fprint(w, "\n\n")
333 | case "p":
334 | br(c, w, option)
335 | walk(c, w, nest, option)
336 | br(c, w, option)
337 | fmt.Fprint(w, "\n\n")
338 | case "code":
339 | if !isChildOf(c, "pre") {
340 | fmt.Fprint(w, "`")
341 | pre(c, w, option)
342 | fmt.Fprint(w, "`")
343 | }
344 | case "pre":
345 | br(c, w, option)
346 |
347 | clone := option.Clone()
348 | clone.doNotEscape = true
349 |
350 | var buf bytes.Buffer
351 | pre(c, &buf, clone)
352 | inner := buf.String()
353 |
354 | var lang string = langFromClass(c)
355 | if option != nil && option.GuessLang != nil {
356 | if guess, err := option.GuessLang(buf.String()); err == nil {
357 | lang = guess
358 | }
359 | }
360 |
361 | fmt.Fprint(w, "```"+lang+"\n")
362 | fmt.Fprint(w, inner)
363 | if !strings.HasSuffix(inner, "\n") {
364 | fmt.Fprint(w, "\n")
365 | }
366 | fmt.Fprint(w, "```\n\n")
367 | case "div":
368 | br(c, w, option)
369 | walk(c, w, nest, option)
370 | fmt.Fprint(w, "\n")
371 | case "blockquote":
372 | br(c, w, option)
373 | var buf bytes.Buffer
374 | if hasClass(c, "code") {
375 | bq(c, &buf, option)
376 | var lang string
377 | if option != nil && option.GuessLang != nil {
378 | if guess, err := option.GuessLang(buf.String()); err == nil {
379 | lang = guess
380 | }
381 | }
382 | fmt.Fprint(w, "```"+lang+"\n")
383 | fmt.Fprint(w, strings.TrimLeft(buf.String(), "\n"))
384 | if !strings.HasSuffix(buf.String(), "\n") {
385 | fmt.Fprint(w, "\n")
386 | }
387 | fmt.Fprint(w, "```\n\n")
388 | } else {
389 | walk(c, &buf, nest+1, option)
390 |
391 | if lines := strings.Split(strings.TrimSpace(buf.String()), "\n"); len(lines) > 0 {
392 | for _, l := range lines {
393 | fmt.Fprint(w, "> "+strings.TrimSpace(l)+"\n")
394 | }
395 | fmt.Fprint(w, "\n")
396 | }
397 | }
398 | case "ul", "ol":
399 | br(c, w, option)
400 |
401 | newOption := option.Clone()
402 | newOption.TrimSpace = true
403 |
404 | var buf bytes.Buffer
405 | walk(c, &buf, nest+1, newOption)
406 |
407 | // Remove any empty lines in the list
408 | if lines := strings.Split(buf.String(), "\n"); len(lines) > 0 {
409 | for i, l := range lines {
410 | if strings.TrimSpace(l) == "" {
411 | continue
412 | }
413 |
414 | if i > 0 {
415 | fmt.Fprint(w, "\n")
416 | }
417 |
418 | fmt.Fprint(w, l)
419 | }
420 | fmt.Fprint(w, "\n")
421 | if nest == 0 {
422 | fmt.Fprint(w, "\n")
423 | }
424 | }
425 | case "li":
426 | br(c, w, option)
427 |
428 | var buf bytes.Buffer
429 | walk(c, &buf, 0, option)
430 |
431 | markPrinted := false
432 |
433 | for _, l := range strings.Split(buf.String(), "\n") {
434 | if strings.TrimSpace(l) == "" {
435 | continue
436 | }
437 | // if markPrinted {
438 |
439 | // }
440 | if markPrinted {
441 | fmt.Fprint(w, "\n ")
442 | }
443 |
444 | fmt.Fprint(w, strings.Repeat(" ", nest-1))
445 |
446 | if !markPrinted {
447 | if isChildOf(c, "ul") {
448 | fmt.Fprint(w, "* ")
449 | } else if isChildOf(c, "ol") {
450 | n++
451 | fmt.Fprint(w, fmt.Sprintf("%d. ", n))
452 | }
453 |
454 | markPrinted = true
455 | }
456 |
457 | fmt.Fprint(w, l)
458 | }
459 |
460 | fmt.Fprint(w, "\n")
461 |
462 | case "h1", "h2", "h3", "h4", "h5", "h6":
463 | br(c, w, option)
464 | fmt.Fprint(w, strings.Repeat("#", int(rune(c.Data[1])-rune('0')))+" ")
465 | walk(c, w, nest, option)
466 | fmt.Fprint(w, "\n\n")
467 | case "img":
468 | src := attr(c, "src")
469 | alt := attr(c, "alt")
470 | title := attr(c, "title")
471 |
472 | if src == "" {
473 | break
474 | }
475 |
476 | full := fmt.Sprintf("", alt, src)
477 | if title != "" {
478 | full = fmt.Sprintf("", alt, src, title)
479 | }
480 |
481 | fmt.Fprint(w, full)
482 | case "hr":
483 | br(c, w, option)
484 | fmt.Fprint(w, "\n---\n\n")
485 | case "table":
486 | br(c, w, option)
487 | table(c, w, option)
488 | case "style":
489 | if option != nil && option.Style {
490 | br(c, w, option)
491 | raw(c, w, option)
492 | fmt.Fprint(w, "\n\n")
493 | }
494 | case "script":
495 | if option != nil && option.Script {
496 | br(c, w, option)
497 | raw(c, w, option)
498 | fmt.Fprint(w, "\n\n")
499 | }
500 | default:
501 | walk(c, w, nest, option)
502 | }
503 | default:
504 | walk(c, w, nest, option)
505 | }
506 | }
507 | }
508 |
509 | // WalkFunc type is an signature for functions traversing HTML nodes
510 | type WalkFunc func(node *html.Node, w io.Writer, nest int, option *Option)
511 |
512 | // CustomRule is an interface to define custom conversion rules
513 | //
514 | // Rule method accepts `next WalkFunc` as an argument, which `customRule` should call
515 | // to let walk function continue parsing the content inside the HTML tag.
516 | // It returns a tagName to indicate what HTML element this `customRule` handles and the `customRule`
517 | // function itself, where conversion logic should reside.
518 | //
519 | // See example TestRule implementation in godown_test.go
520 | type CustomRule interface {
521 | Rule(next WalkFunc) (tagName string, customRule WalkFunc)
522 | }
523 |
524 | // Option is optional information for Convert.
525 | type Option struct {
526 | GuessLang func(string) (string, error)
527 | Script bool
528 | Style bool
529 | TrimSpace bool
530 | CustomRules []CustomRule
531 | IgnoreComments bool
532 | ItalicsAsterix bool // Used to know if to use _ or * for italics
533 | doNotEscape bool // Used to know if to escape certain characters
534 | customRulesMap map[string]WalkFunc
535 | }
536 |
537 | // To make a copy of an option without changing the original
538 | func (o *Option) Clone() *Option {
539 | if o == nil {
540 | return nil
541 | }
542 |
543 | clone := *o
544 | return &clone
545 | }
546 |
547 | // Convert convert HTML to Markdown. Read HTML from r and write to w.
548 | func Convert(w io.Writer, r io.Reader, option *Option) error {
549 | doc, err := html.Parse(r)
550 | if err != nil {
551 | return err
552 | }
553 | if option == nil {
554 | option = &Option{}
555 | }
556 |
557 | option.customRulesMap = make(map[string]WalkFunc)
558 | for _, cr := range option.CustomRules {
559 | tag, customWalk := cr.Rule(walk)
560 | option.customRulesMap[tag] = customWalk
561 | }
562 |
563 | walk(doc, w, 0, option)
564 | fmt.Fprint(w, "\n")
565 | return nil
566 | }
567 |
--------------------------------------------------------------------------------
/godown_test.go:
--------------------------------------------------------------------------------
1 | package godown
2 |
3 | import (
4 | "bytes"
5 | "fmt"
6 | "io"
7 | "io/ioutil"
8 | "os"
9 | "path/filepath"
10 | "sort"
11 | "strings"
12 | "testing"
13 |
14 | "golang.org/x/net/html"
15 | )
16 |
17 | func TestGodown(t *testing.T) {
18 | m, err := filepath.Glob("testdata/*.html")
19 | if err != nil {
20 | t.Fatal(err)
21 | }
22 | sort.Strings(m)
23 | for _, file := range m {
24 | f, err := os.Open(file)
25 | if err != nil {
26 | t.Fatal(err)
27 | }
28 | var buf bytes.Buffer
29 | if err = Convert(&buf, f, nil); err != nil {
30 | t.Fatal(err)
31 | }
32 |
33 | b, err := ioutil.ReadFile(file[:len(file)-4] + "md")
34 | if err != nil {
35 | t.Fatal(err)
36 | }
37 | if string(b) != buf.String() {
38 | t.Errorf("(%s):\nwant:\n%s}}}\ngot:\n%s}}}\n", file, string(b), buf.String())
39 | }
40 | f.Close()
41 | }
42 | }
43 |
44 | type errReader int
45 |
46 | func (e errReader) Read(p []byte) (n int, err error) {
47 | return 0, io.ErrUnexpectedEOF
48 | }
49 |
50 | func TestError(t *testing.T) {
51 | var buf bytes.Buffer
52 | var e errReader
53 | err := Convert(&buf, e, nil)
54 | if err == nil {
55 | t.Fatal("should be an error")
56 | }
57 | }
58 |
59 | func TestGuessLang(t *testing.T) {
60 | var buf bytes.Buffer
61 | err := Convert(&buf, strings.NewReader(`
62 |
63 | def do_something():
64 | pass
65 |
66 | `), &Option{
67 | GuessLang: func(s string) (string, error) { return "python", nil },
68 | })
69 | if err != nil {
70 | t.Fatal(err)
71 | }
72 | want := "```python\ndef do_something():\n pass\n```\n\n\n"
73 | if buf.String() != want {
74 | t.Errorf("\nwant:\n%s}}}\ngot:\n%s}}}\n", want, buf.String())
75 | }
76 | }
77 |
78 | func TestGuessLangFromClass(t *testing.T) {
79 | var buf bytes.Buffer
80 | err := Convert(&buf, strings.NewReader(`
81 | def do_something():
82 | pass
83 |
84 | `), nil)
85 | if err != nil {
86 | t.Fatal(err)
87 | }
88 | want := "```python\ndef do_something():\n pass\n```\n\n\n"
89 | if buf.String() != want {
90 | t.Errorf("\nwant:\n%s}}}\ngot:\n%s}}}\n", want, buf.String())
91 | }
92 | }
93 |
94 | func TestGuessLangBq(t *testing.T) {
95 | var buf bytes.Buffer
96 | err := Convert(&buf, strings.NewReader(`
97 |
98 | def do_something():
99 | pass
100 |
101 | `), &Option{
102 | GuessLang: func(s string) (string, error) { return "python", nil },
103 | })
104 | if err != nil {
105 | t.Fatal(err)
106 | }
107 | want := "```python\ndef do_something():\n pass\n```\n\n\n"
108 | if buf.String() != want {
109 | t.Errorf("\nwant:\n%s}}}\ngot:\n%s}}}\n", want, buf.String())
110 | }
111 | }
112 |
113 | func TestWhiteSpaceDelimiter(t *testing.T) {
114 | // Test adding delimiters only on the inner contents
115 | var buf bytes.Buffer
116 | err := Convert(&buf, strings.NewReader(
117 | ` foo bar `,
118 | ), nil)
119 | if err != nil {
120 | t.Fatal(err)
121 | }
122 | want := " **foo bar** \n"
123 | if buf.String() != want {
124 | t.Errorf("\nwant:\n%q}}}\ngot:\n%q}}}\n", want, buf.String())
125 | }
126 |
127 | // Test that no delimiters are added if the contents is all whitespace
128 | var buf2 bytes.Buffer
129 | err = Convert(&buf2, strings.NewReader(
130 | `Hello hi`,
131 | ), nil)
132 | if err != nil {
133 | t.Fatal(err)
134 | }
135 | want = "Hello hi\n"
136 | if buf2.String() != want {
137 | t.Errorf("\nwant:\n%q}}}\ngot:\n%q}}}\n", want, buf2.String())
138 | }
139 |
140 | // Test that line breaks are preserved even if delimiters are not added
141 | var buf3 bytes.Buffer
142 | err = Convert(&buf3, strings.NewReader(
143 | ` `,
144 | ), nil)
145 | if err != nil {
146 | t.Fatal(err)
147 | }
148 | want = "\n\n\n"
149 | if buf3.String() != want {
150 | t.Errorf("\nwant:\n%q}}}\ngot:\n%q}}}\n", want, buf3.String())
151 | }
152 | }
153 |
154 | func TestEmptyImageSrc(t *testing.T) {
155 | var buf bytes.Buffer
156 | err := Convert(&buf, strings.NewReader(
157 | ` `,
158 | ), nil)
159 | if err != nil {
160 | t.Fatal(err)
161 | }
162 | want := "\n"
163 | if buf.String() != want {
164 | t.Errorf("\nwant:\n%q}}}\ngot:\n%q}}}\n", want, buf.String())
165 | }
166 | }
167 |
168 | func TestBlockLink(t *testing.T) {
169 | var buf bytes.Buffer
170 | err := Convert(&buf, strings.NewReader(
171 | `
`,
172 | ), nil)
173 | if err != nil {
174 | t.Fatal(err)
175 | }
176 | want := "[](https://example.org)\n\n"
177 | if buf.String() != want {
178 | t.Errorf("\nwant:\n%q}}}\ngot:\n%q}}}\n", want, buf.String())
179 | }
180 | }
181 |
182 | func TestScript(t *testing.T) {
183 | var buf bytes.Buffer
184 | err := Convert(&buf, strings.NewReader(`
185 | here is script
186 |
187 |
188 |
189 |
192 | `), &Option{
193 | Script: true,
194 | })
195 | if err != nil {
196 | t.Fatal(err)
197 | }
198 | want := `here is script
199 |
200 |
201 |
202 |
205 |
206 |
207 | `
208 | if buf.String() != want {
209 | t.Errorf("\nwant:\n%s}}}\ngot:\n%s}}}\n", want, buf.String())
210 | }
211 | }
212 |
213 | func TestStyle(t *testing.T) {
214 | var buf bytes.Buffer
215 | err := Convert(&buf, strings.NewReader(`
216 | here is style
217 |
218 |
223 | `), &Option{
224 | Style: true,
225 | })
226 | if err != nil {
227 | t.Fatal(err)
228 | }
229 | want := `here is style
230 |
231 |
236 |
237 |
238 | `
239 | if buf.String() != want {
240 | t.Errorf("\nwant:\n%s}}}\ngot:\n%s}}}\n", want, buf.String())
241 | }
242 | }
243 |
244 | type TestRule struct{}
245 |
246 | func (r *TestRule) Rule(next WalkFunc) (string, WalkFunc) {
247 | return "test", func(node *html.Node, w io.Writer, nest int, option *Option) {
248 | fmt.Fprint(w, "_")
249 | next(node, w, nest, option)
250 | fmt.Fprint(w, "_")
251 | }
252 | }
253 |
254 | func TestCustomRules(t *testing.T) {
255 | var buf bytes.Buffer
256 | err := Convert(&buf, strings.NewReader(`
257 | here is the text in custom tag
258 | `), &Option{
259 | CustomRules: []CustomRule{&TestRule{}},
260 | })
261 | if err != nil {
262 | t.Fatal(err)
263 | }
264 | want := `_here is the text in custom tag_
265 | `
266 | if buf.String() != want {
267 | t.Errorf("\nwant:\n%s}}}\ngot:\n%s}}}\n", want, buf.String())
268 | }
269 | }
270 |
271 | type TestOverwriteRule struct{}
272 |
273 | func (r *TestOverwriteRule) Rule(next WalkFunc) (string, WalkFunc) {
274 | return "div", func(node *html.Node, w io.Writer, nest int, option *Option) {
275 | fmt.Fprint(w, "___")
276 | next(node, w, nest, option)
277 | fmt.Fprint(w, "___")
278 | }
279 | }
280 |
281 | func TestCustomOverwriteRules(t *testing.T) {
282 | var buf bytes.Buffer
283 | err := Convert(&buf, strings.NewReader(`
284 | here is the text in custom tag
285 | `), &Option{
286 | CustomRules: []CustomRule{&TestOverwriteRule{}},
287 | })
288 | if err != nil {
289 | t.Fatal(err)
290 | }
291 | want := `___here is the text in custom tag___
292 | `
293 | if buf.String() != want {
294 | t.Errorf("\nwant:\n%s}}}\ngot:\n%s}}}\n", want, buf.String())
295 | }
296 | }
297 |
298 | func TestItalicStyle(t *testing.T) {
299 | var buf bytes.Buffer
300 | from := `Hello world `
301 |
302 | // The default mode
303 | err := Convert(&buf, strings.NewReader(from), nil)
304 | if err != nil {
305 | t.Fatal(err)
306 | }
307 |
308 | want := `_Hello world_
309 | `
310 | if buf.String() != want {
311 | t.Errorf("\nwant:\n%s}}}\ngot:\n%s}}}\n", want, buf.String())
312 | }
313 |
314 | buf.Reset()
315 |
316 | // Change the italic style to asterix
317 | err = Convert(&buf, strings.NewReader(from), &Option{ItalicsAsterix: true})
318 | if err != nil {
319 | t.Fatal(err)
320 | }
321 |
322 | want = `*Hello world*
323 | `
324 | if buf.String() != want {
325 | t.Errorf("\nwant:\n%s}}}\ngot:\n%s}}}\n", want, buf.String())
326 | }
327 | }
328 |
--------------------------------------------------------------------------------
/testdata/test001.html:
--------------------------------------------------------------------------------
1 | Hello Golang !
2 |
--------------------------------------------------------------------------------
/testdata/test001.md:
--------------------------------------------------------------------------------
1 | Hello **Golang**\!
2 |
--------------------------------------------------------------------------------
/testdata/test002.html:
--------------------------------------------------------------------------------
1 | Hello Golang !
2 |
--------------------------------------------------------------------------------
/testdata/test002.md:
--------------------------------------------------------------------------------
1 | Hello _Golang_\!
2 |
--------------------------------------------------------------------------------
/testdata/test003.html:
--------------------------------------------------------------------------------
1 |
2 | foo
3 | bar
4 | baz
5 |
6 |
--------------------------------------------------------------------------------
/testdata/test003.md:
--------------------------------------------------------------------------------
1 |
2 | * foo
3 | * bar
4 | * baz
5 |
6 |
7 |
--------------------------------------------------------------------------------
/testdata/test004.html:
--------------------------------------------------------------------------------
1 |
2 | foo
3 | bar
4 | baz
5 |
6 |
--------------------------------------------------------------------------------
/testdata/test004.md:
--------------------------------------------------------------------------------
1 |
2 | 1. foo
3 | 2. bar
4 | 3. baz
5 |
6 |
7 |
--------------------------------------------------------------------------------
/testdata/test005.html:
--------------------------------------------------------------------------------
1 |
2 | blah, blah, blah
3 |
4 |
5 |
6 |
7 | blah, blah, blah
8 |
9 |
10 |
--------------------------------------------------------------------------------
/testdata/test005.md:
--------------------------------------------------------------------------------
1 | > blah, blah, blah
2 |
3 | > > blah, blah, blah
4 |
5 |
6 |
--------------------------------------------------------------------------------
/testdata/test006.html:
--------------------------------------------------------------------------------
1 |
2 | void main(void) {
3 | puts("hello world");
4 | }
5 |
6 |
7 |
8 | void main(void) {
9 | puts("hello world ");
10 | }
11 |
12 |
13 | void main(void) {
14 | puts("hello world ");
15 | }
16 |
17 |
18 | foo
19 |
--------------------------------------------------------------------------------
/testdata/test006.md:
--------------------------------------------------------------------------------
1 | ```
2 | void main(void) {
3 | puts("hello world");
4 | }
5 | ```
6 |
7 | ```
8 | void main(void) {
9 | puts("hello world");
10 | }
11 | ```
12 |
13 | ```
14 | void main(void) {
15 | puts("hello world");
16 | }
17 | ```
18 |
19 | ```
20 | foo
21 | ```
22 |
23 |
24 |
--------------------------------------------------------------------------------
/testdata/test007.html:
--------------------------------------------------------------------------------
1 | GitHub
2 |
3 | GitHub
4 |
5 | GitHub
6 |
--------------------------------------------------------------------------------
/testdata/test007.md:
--------------------------------------------------------------------------------
1 | [GitHub](https://github.com/)
2 |
3 | [GitHub](https://github.com/ "GitHub Homepage")
4 |
5 | [GitHub]()
6 |
7 |
8 |
--------------------------------------------------------------------------------
/testdata/test008.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/testdata/test008.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | 
4 |
5 |
6 |
--------------------------------------------------------------------------------
/testdata/test009.html:
--------------------------------------------------------------------------------
1 | Yes, We Can No, We Can't
2 |
--------------------------------------------------------------------------------
/testdata/test009.md:
--------------------------------------------------------------------------------
1 | ~~Yes, We Can~~ ~~No, We Can't~~
2 |
--------------------------------------------------------------------------------
/testdata/test010.html:
--------------------------------------------------------------------------------
1 | Elit earum porro doloribus exercitationem in quis Natus vero ad impedit est facere adipisci. Et delectus alias nesciunt quo quod similique, voluptas dolore alias temporibus dolor Corporis obcaecati maxime possimus.
2 |
3 | Amet est harum nihil fugiat dicta Odio laboriosam provident necessitatibus minus aperiam quisquam. Accusamus repellat sapiente sunt in provident. Iste voluptatum voluptas facilis in libero Fugit vel dicta illum labore
4 |
--------------------------------------------------------------------------------
/testdata/test010.md:
--------------------------------------------------------------------------------
1 | Elit earum porro doloribus exercitationem in quis Natus vero ad impedit est facere adipisci. Et delectus alias nesciunt quo quod similique, voluptas dolore alias temporibus dolor Corporis obcaecati maxime possimus.
2 |
3 | Amet est harum nihil fugiat dicta Odio laboriosam provident necessitatibus minus aperiam quisquam. Accusamus repellat sapiente sunt in provident. Iste voluptatum voluptas facilis in libero Fugit vel dicta illum labore
4 |
5 |
6 |
--------------------------------------------------------------------------------
/testdata/test011.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | foo bar
4 | bar baz
5 |
6 |
7 |
8 |
11 |
--------------------------------------------------------------------------------
/testdata/test011.md:
--------------------------------------------------------------------------------
1 | foo bar bar baz
2 |
3 | foo bar
4 |
5 | bar baz
6 |
7 |
8 |
--------------------------------------------------------------------------------
/testdata/test012.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | foo1
4 | bar1
5 |
6 |
7 | foo2
8 | baあああ
9 |
10 |
11 |
12 |
13 |
14 | いいい
15 | いい
16 | bar1
17 |
18 |
19 | foo2
20 | baあああ
21 |
22 |
23 |
24 |
25 |
26 | いいい
27 | いい
28 | bar1
29 |
30 |
31 | foo2
32 |
33 |
34 |
35 |
36 |
37 |
38 | Request
39 | Handled in
40 | Wait time
41 | Actual response time
42 |
43 |
44 |
45 |
46 | /
47 | 4s
48 | 0
49 | 4s
50 |
51 |
52 | /ping
53 | 4s
54 | 4s
55 | 8s
56 |
57 |
58 | /dfd
59 | 5s
60 | 8s
61 | 13s
62 |
63 |
64 |
65 |
66 | /foot
67 | 3s
68 | 5
69 | 2s
70 |
71 |
72 |
73 |
--------------------------------------------------------------------------------
/testdata/test012.md:
--------------------------------------------------------------------------------
1 | |foo1|bar1 |
2 | |----|--------|
3 | |foo2|baあああ|
4 |
5 | |いいい いい|bar1 |
6 | |-----------|--------|
7 | |foo2 |baあああ|
8 |
9 | |いいい いい|bar1|
10 | |-----------|----|
11 | |foo2 | |
12 |
13 | |Request|Handled in|Wait time|Actual response time|
14 | |-------|----------|---------|--------------------|
15 | |/ |4s |0 |4s |
16 | |/ping |4s |4s |8s |
17 | |/dfd |5s |8s |13s |
18 | |/foot |3s |5 |2s |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/testdata/test013.html:
--------------------------------------------------------------------------------
1 | H1
2 |
3 | H2
4 |
5 | H3
6 |
7 | H4
8 |
9 | H5
10 |
11 | H6
12 |
--------------------------------------------------------------------------------
/testdata/test013.md:
--------------------------------------------------------------------------------
1 | # H1
2 |
3 | ## H2
4 |
5 | ### H3
6 |
7 | #### H4
8 |
9 | ##### H5
10 |
11 | ###### H6
12 |
13 |
14 |
--------------------------------------------------------------------------------
/testdata/test014.html:
--------------------------------------------------------------------------------
1 | inline code
.
2 |
--------------------------------------------------------------------------------
/testdata/test014.md:
--------------------------------------------------------------------------------
1 | inline `code`.
2 |
--------------------------------------------------------------------------------
/testdata/test015.html:
--------------------------------------------------------------------------------
1 |
2 | package main
3 |
4 | import (
5 | "fmt"
6 | )
7 |
8 | func main () {
9 | fmt.Println ("hello world" )
10 | }
11 |
12 |
13 | foo
14 |
--------------------------------------------------------------------------------
/testdata/test015.md:
--------------------------------------------------------------------------------
1 | ```
2 | package main
3 |
4 | import (
5 | "fmt"
6 | )
7 |
8 | func main() {
9 | fmt.Println("hello world")
10 | }
11 | ```
12 |
13 | ```
14 | foo
15 | ```
16 |
17 |
18 |
--------------------------------------------------------------------------------
/testdata/test016.html:
--------------------------------------------------------------------------------
1 | hello world
2 |
--------------------------------------------------------------------------------
/testdata/test016.md:
--------------------------------------------------------------------------------
1 | **hello**
2 |
3 | [world](#)
4 |
--------------------------------------------------------------------------------
/testdata/test017.html:
--------------------------------------------------------------------------------
1 | foo bar
2 |
--------------------------------------------------------------------------------
/testdata/test017.md:
--------------------------------------------------------------------------------
1 | foo
2 |
3 | ---
4 |
5 | bar
6 |
--------------------------------------------------------------------------------
/testdata/test018.html:
--------------------------------------------------------------------------------
1 |
2 | nest1
3 |
4 | nest1-1
5 | nest1-2
6 | nest1-3
7 |
10 | nest1-4
11 |
12 | nest2
13 |
14 | nest2-1
15 | nest2-2
16 |
17 |
18 |
--------------------------------------------------------------------------------
/testdata/test018.md:
--------------------------------------------------------------------------------
1 |
2 | * nest1
3 | 1. nest1\-1
4 | 2. nest1\-2
5 | 3. nest1\-3
6 | * nest1\-3\-1
7 | 4. nest1\-4
8 | * nest2
9 | * nest2\-1
10 | * nest2\-2
11 |
12 |
13 |
--------------------------------------------------------------------------------
/testdata/test019.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
--------------------------------------------------------------------------------
/testdata/test019.md:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
--------------------------------------------------------------------------------
/testdata/test020.html:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/testdata/test020.md:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/testdata/test021.html:
--------------------------------------------------------------------------------
1 | >100 = > 29
2 |
--------------------------------------------------------------------------------
/testdata/test021.md:
--------------------------------------------------------------------------------
1 | \>100 = \> 29
2 |
3 |
4 |
--------------------------------------------------------------------------------
/testdata/test022.html:
--------------------------------------------------------------------------------
1 | >100 = > 29
2 |
--------------------------------------------------------------------------------
/testdata/test022.md:
--------------------------------------------------------------------------------
1 | ```
2 | >100 = > 29
3 | ```
4 |
5 |
6 |
--------------------------------------------------------------------------------
/testdata/test023.html:
--------------------------------------------------------------------------------
1 | many more examples
2 |
--------------------------------------------------------------------------------
/testdata/test023.md:
--------------------------------------------------------------------------------
1 | [many](many) [more](more) [examples](examples)
2 |
--------------------------------------------------------------------------------
/testdata/test024.html:
--------------------------------------------------------------------------------
1 |
2 | nest1
3 |
4 |
5 | nest1-1.1
6 | nest1-1.2
7 |
8 | nest1-2
9 | nest1-3
10 |
13 | nest1-4
14 |
15 | nest2
16 |
17 | nest2-1
18 | nest2-2
19 |
20 |
21 | Text after list
22 |
--------------------------------------------------------------------------------
/testdata/test024.md:
--------------------------------------------------------------------------------
1 |
2 | * nest1
3 | 1. nest1\-1.1
4 | nest1\-1.2
5 | 2. nest1\-2
6 | 3. nest1\-3
7 | * nest1\-3\-1
8 | 4. nest1\-4
9 | * nest2
10 | * nest2\-1
11 | * nest2\-2
12 |
13 | Text after list
14 |
15 |
16 |
--------------------------------------------------------------------------------
/testdata/test025.html:
--------------------------------------------------------------------------------
1 |
2 | Make sure it wraps appropriately. That way it will look really neat.
3 | Funny
4 |
5 |
6 |
7 | hello
8 | hi
9 |
10 |
11 |
12 |
13 | wow
14 | wawu
15 |
16 |
17 |
18 | Hello
19 |
20 | Amazing
21 | even funnier
22 |
23 | amazong
24 | whatver
25 |
26 |
27 | Hmmm
28 |
29 |
30 |
31 |
32 | Example
33 |
34 | Hello
35 | Hi
36 |
37 |
38 |
39 |
40 | Interesting.
41 | Funny
42 |
43 | Amazing
44 |
45 | even funnier
46 | amazong
47 | whatver
48 |
49 |
50 | Hmmm
51 |
52 |
53 |
54 |
--------------------------------------------------------------------------------
/testdata/test025.md:
--------------------------------------------------------------------------------
1 |
2 | * Make sure it wraps appropriately. That way it will look really neat.
3 | * Funny
4 | |hello|hi |
5 | |-----|----|
6 | |wow |wawu|
7 | ## Hello
8 | * Amazing
9 | * even funnier
10 | * amazong
11 | * whatver
12 | * Hmmm
13 |
14 |
15 | 1. [Example](https://example.com)
16 | 1. Hello
17 | 2. Hi
18 |
19 |
20 | * Interesting.
21 | * Funny
22 | * Amazing
23 | * even funnier
24 | * amazong
25 | * whatver
26 | * Hmmm
27 |
28 |
29 |
--------------------------------------------------------------------------------