├── .dockerignore ├── Dockerfile ├── LICENSE ├── README.md ├── go.mod ├── go.sum ├── main.go └── markdown ├── main.go ├── main_test.go ├── stringwidth_test.go └── testdata ├── escapeurl.golden.md ├── escapeurl.in.md ├── linebreak.golden.md ├── linebreak.in.md ├── listend.golden.md ├── listend.in.md ├── reference.golden.md ├── reference.in.md ├── successive.golden.md ├── successive.in.md ├── url.golden.md ├── url.in.md ├── widechar.golden.md └── widechar.in.md /.dockerignore: -------------------------------------------------------------------------------- 1 | .dockerignore 2 | .git 3 | Dockerfile 4 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.21-alpine3.19 AS build 2 | 3 | WORKDIR /markdownfmt 4 | 5 | COPY go.mod go.sum ./ 6 | RUN go mod download 7 | 8 | COPY . . 9 | 10 | RUN set -eux; go build -v -trimpath -o markdownfmt ./; ./markdownfmt -h 11 | 12 | FROM alpine:3.19 13 | 14 | COPY --from=build /markdownfmt/markdownfmt /usr/local/bin/ 15 | 16 | CMD ["markdownfmt"] 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2014 Dmitri Shuralyov 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 | markdownfmt 2 | =========== 3 | 4 | [![Go Reference](https://pkg.go.dev/badge/github.com/shurcooL/markdownfmt.svg)](https://pkg.go.dev/github.com/shurcooL/markdownfmt) 5 | 6 | Like `gofmt`, but for Markdown. 7 | 8 | ![Markdown Format Demo](https://github.com/shurcooL/atom-markdown-format/blob/master/Demo.gif?raw=true) 9 | 10 | Note that `markdownfmt` works with pure Markdown files. If you want to use it with Markdown files that have front matter, consider one of [alternatives](#alternatives) that supports that. 11 | 12 | Installation 13 | ------------ 14 | 15 | ```sh 16 | go install github.com/shurcooL/markdownfmt@latest 17 | ``` 18 | 19 | Usage 20 | ----- 21 | 22 | ```sh 23 | usage: markdownfmt [flags] [path ...] 24 | -d display diffs instead of rewriting files 25 | -l list files whose formatting differs from markdownfmt's 26 | -w write result to (source) file instead of stdout 27 | ``` 28 | 29 | Editor Plugins 30 | -------------- 31 | 32 | - [vim-markdownfmt](https://github.com/moorereason/vim-markdownfmt) for Vim. 33 | - [emacs-markdownfmt](https://github.com/nlamirault/emacs-markdownfmt) for Emacs. 34 | - [vscode-markdownfmt](https://marketplace.visualstudio.com/itemdetails?itemName=AnmolSinghJaggi.vscode-markdownfmt) for Visual Studio Code. 35 | - Built-in in Conception. 36 | - [markdown-format](https://atom.io/packages/markdown-format) for Atom (deprecated). 37 | - Add a plugin for your favorite editor here? 38 | 39 | Alternatives 40 | ------------ 41 | 42 | - [`mdfmt`](https://github.com/moorereason/mdfmt) - Fork of `markdownfmt` that adds front matter support. 43 | - [`tidy-markdown`](https://github.com/slang800/tidy-markdown) - Project with similar goals, but written in JS and based on a slightly different [styleguide](https://github.com/slang800/markdown-styleguide). 44 | - [Flowmark](https://github.com/jlevy/atom-flowmark) - A JS-based Atom plugin with line wrapping, YAML frontmatter support, and other normalization features. 45 | 46 | License 47 | ------- 48 | 49 | - [MIT License](LICENSE) 50 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/shurcooL/markdownfmt 2 | 3 | go 1.21 4 | 5 | require ( 6 | github.com/mattn/go-runewidth v0.0.13 7 | github.com/shurcooL/go v0.0.0-20200502201357-93f07166e636 8 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 9 | ) 10 | 11 | // 1.5.2 and 1.6.0 have a bug with code block + list parsing where code blocks after a list get consumed into the final list item (which is the opposite of the bug they were trying to fix when they broke it: https://github.com/russross/blackfriday/issues/239, https://github.com/russross/blackfriday/issues/495, https://github.com/russross/blackfriday/issues/485, https://github.com/russross/blackfriday/pull/521 12 | require github.com/russross/blackfriday v1.5.1 13 | 14 | require ( 15 | github.com/rivo/uniseg v0.2.0 // indirect 16 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 // indirect 17 | ) 18 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= 2 | github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 3 | github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= 4 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 5 | github.com/russross/blackfriday v1.5.1 h1:B8ZN6pD4PVofmlDCDUdELeYrbsVIDM/bpjW3v3zgcRc= 6 | github.com/russross/blackfriday v1.5.1/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= 7 | github.com/shurcooL/go v0.0.0-20200502201357-93f07166e636 h1:aSISeOcal5irEhJd1M+IrApc0PdcN7e7Aj4yuEnOrfQ= 8 | github.com/shurcooL/go v0.0.0-20200502201357-93f07166e636/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= 9 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 h1:SrN+KX8Art/Sf4HNj6Zcz06G7VEz+7w9tdXTPOZ7+l4= 10 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 11 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= 12 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 13 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // markdownfmt formats Markdown. 2 | package main // import "github.com/shurcooL/markdownfmt" 3 | 4 | import ( 5 | "bytes" 6 | "flag" 7 | "fmt" 8 | "go/scanner" 9 | "io" 10 | "io/ioutil" 11 | "os" 12 | "os/exec" 13 | "path/filepath" 14 | "strings" 15 | 16 | "github.com/shurcooL/markdownfmt/markdown" 17 | "golang.org/x/term" 18 | ) 19 | 20 | var ( 21 | // Main operation modes. 22 | list = flag.Bool("l", false, "list files whose formatting differs from markdownfmt's") 23 | write = flag.Bool("w", false, "write result to (source) file instead of stdout") 24 | doDiff = flag.Bool("d", false, "display diffs instead of rewriting files") 25 | 26 | exitCode = 0 27 | ) 28 | 29 | func report(err error) { 30 | scanner.PrintError(os.Stderr, err) 31 | exitCode = 2 32 | } 33 | 34 | func usage() { 35 | fmt.Fprintf(os.Stderr, "usage: markdownfmt [flags] [path ...]\n") 36 | flag.PrintDefaults() 37 | } 38 | 39 | func isMarkdownFile(f os.FileInfo) bool { 40 | // Ignore non-Markdown files. 41 | name := f.Name() 42 | return !f.IsDir() && !strings.HasPrefix(name, ".") && (strings.HasSuffix(name, ".md") || strings.HasSuffix(name, ".markdown")) 43 | } 44 | 45 | func processFile(filename string, in io.Reader, out io.Writer, stdin bool) error { 46 | if in == nil { 47 | f, err := os.Open(filename) 48 | if err != nil { 49 | return err 50 | } 51 | defer f.Close() 52 | in = f 53 | } 54 | 55 | src, err := ioutil.ReadAll(in) 56 | if err != nil { 57 | return err 58 | } 59 | 60 | isTerminal := func() bool { 61 | return term.IsTerminal(int(os.Stdout.Fd())) && os.Getenv("TERM") != "dumb" 62 | } 63 | res, err := markdown.Process(filename, src, &markdown.Options{ 64 | Terminal: !*list && !*write && !*doDiff && isTerminal(), 65 | }) 66 | if err != nil { 67 | return err 68 | } 69 | if len(res) > 0 { 70 | res = []byte(strings.TrimSpace(string(res)) + "\n") 71 | } 72 | 73 | if !bytes.Equal(src, res) { 74 | // formatting has changed 75 | if *list { 76 | fmt.Fprintln(out, filename) 77 | } 78 | if *write { 79 | err = ioutil.WriteFile(filename, res, 0) 80 | if err != nil { 81 | return err 82 | } 83 | } 84 | if *doDiff { 85 | data, err := diff(src, res) 86 | if err != nil { 87 | return fmt.Errorf("computing diff: %s", err) 88 | } 89 | fmt.Printf("diff %s markdownfmt/%s\n", filename, filename) 90 | out.Write(data) 91 | } 92 | } 93 | 94 | if !*list && !*write && !*doDiff { 95 | _, err = out.Write(res) 96 | } 97 | 98 | return err 99 | } 100 | 101 | func visitFile(path string, f os.FileInfo, err error) error { 102 | if err == nil && isMarkdownFile(f) { 103 | err = processFile(path, nil, os.Stdout, false) 104 | } 105 | if err != nil { 106 | report(err) 107 | } 108 | return nil 109 | } 110 | 111 | func walkDir(path string) { 112 | filepath.Walk(path, visitFile) 113 | } 114 | 115 | func main() { 116 | // call markdownfmtMain in a separate function 117 | // so that it can use defer and have them 118 | // run before the exit. 119 | markdownfmtMain() 120 | os.Exit(exitCode) 121 | } 122 | 123 | func markdownfmtMain() { 124 | flag.Usage = usage 125 | flag.Parse() 126 | 127 | if flag.NArg() == 0 { 128 | if err := processFile("", os.Stdin, os.Stdout, true); err != nil { 129 | report(err) 130 | } 131 | return 132 | } 133 | 134 | for i := 0; i < flag.NArg(); i++ { 135 | path := flag.Arg(i) 136 | switch dir, err := os.Stat(path); { 137 | case err != nil: 138 | report(err) 139 | case dir.IsDir(): 140 | walkDir(path) 141 | default: 142 | if err := processFile(path, nil, os.Stdout, false); err != nil { 143 | report(err) 144 | } 145 | } 146 | } 147 | } 148 | 149 | func diff(b1, b2 []byte) (data []byte, err error) { 150 | f1, err := ioutil.TempFile("", "markdownfmt") 151 | if err != nil { 152 | return 153 | } 154 | defer os.Remove(f1.Name()) 155 | defer f1.Close() 156 | 157 | f2, err := ioutil.TempFile("", "markdownfmt") 158 | if err != nil { 159 | return 160 | } 161 | defer os.Remove(f2.Name()) 162 | defer f2.Close() 163 | 164 | f1.Write(b1) 165 | f2.Write(b2) 166 | 167 | data, err = exec.Command("diff", "-u", f1.Name(), f2.Name()).CombinedOutput() 168 | if len(data) > 0 { 169 | // diff exits with a non-zero status when the files don't match. 170 | // Ignore that failure as long as we get output. 171 | err = nil 172 | } 173 | return 174 | } 175 | -------------------------------------------------------------------------------- /markdown/main.go: -------------------------------------------------------------------------------- 1 | // Package markdown provides a Markdown renderer. 2 | package markdown 3 | 4 | import ( 5 | "bytes" 6 | "fmt" 7 | "go/format" 8 | "io/ioutil" 9 | "strings" 10 | 11 | "github.com/mattn/go-runewidth" 12 | "github.com/russross/blackfriday" 13 | "github.com/shurcooL/go/indentwriter" 14 | ) 15 | 16 | type markdownRenderer struct { 17 | normalTextMarker map[*bytes.Buffer]int 18 | orderedListCounter map[int]int 19 | paragraph map[int]bool // Used to keep track of whether a given list item uses a paragraph for large spacing. 20 | listDepth int 21 | lastNormalText string 22 | 23 | // TODO: Clean these up. 24 | headers []string 25 | columnAligns []int 26 | columnWidths []int 27 | cells []string 28 | 29 | opt Options 30 | 31 | // stringWidth is used internally to calculate visual width of a string. 32 | stringWidth func(s string) (width int) 33 | } 34 | 35 | func formatCode(lang string, text []byte) (formattedCode []byte, ok bool) { 36 | switch lang { 37 | case "Go", "go": 38 | gofmt, err := format.Source(text) 39 | if err != nil { 40 | return nil, false 41 | } 42 | return gofmt, true 43 | default: 44 | return nil, false 45 | } 46 | } 47 | 48 | // Block-level callbacks. 49 | func (*markdownRenderer) BlockCode(out *bytes.Buffer, text []byte, lang string) { 50 | doubleSpace(out) 51 | 52 | // Parse out the language name. 53 | count := 0 54 | for _, elt := range strings.Fields(lang) { 55 | if elt[0] == '.' { 56 | elt = elt[1:] 57 | } 58 | if len(elt) == 0 { 59 | continue 60 | } 61 | out.WriteString("```" + elt + "\n") 62 | count++ 63 | break 64 | } 65 | 66 | if formattedCode, ok := formatCode(lang, text); ok { 67 | text = formattedCode 68 | } 69 | 70 | text = []byte(strings.Trim(string(text), "\n")) 71 | if count == 0 { 72 | text = []byte("\t" + strings.Replace(string(text), "\n", "\n\t", -1)) 73 | } 74 | out.Write(text) 75 | out.WriteString("\n") 76 | 77 | if count != 0 { 78 | out.WriteString("```\n") 79 | } 80 | } 81 | func (*markdownRenderer) BlockQuote(out *bytes.Buffer, text []byte) { 82 | doubleSpace(out) 83 | lines := bytes.Split(text, []byte("\n")) 84 | for i, line := range lines { 85 | if i == len(lines)-1 { 86 | continue 87 | } 88 | out.WriteString(">") 89 | if len(line) != 0 { 90 | out.WriteString(" ") 91 | out.Write(line) 92 | } 93 | out.WriteString("\n") 94 | } 95 | } 96 | func (*markdownRenderer) BlockHtml(out *bytes.Buffer, text []byte) { 97 | doubleSpace(out) 98 | out.Write(text) 99 | out.WriteByte('\n') 100 | } 101 | func (*markdownRenderer) TitleBlock(out *bytes.Buffer, text []byte) { 102 | } 103 | func (mr *markdownRenderer) Header(out *bytes.Buffer, text func() bool, level int, id string) { 104 | marker := out.Len() 105 | doubleSpace(out) 106 | 107 | fmt.Fprint(out, strings.Repeat("#", level), " ") 108 | 109 | if !text() { 110 | out.Truncate(marker) 111 | return 112 | } 113 | 114 | out.WriteString("\n") 115 | } 116 | func (*markdownRenderer) HRule(out *bytes.Buffer) { 117 | doubleSpace(out) 118 | out.WriteString("---\n") 119 | } 120 | func (mr *markdownRenderer) List(out *bytes.Buffer, text func() bool, flags int) { 121 | marker := out.Len() 122 | doubleSpace(out) 123 | 124 | mr.listDepth++ 125 | defer func() { mr.listDepth-- }() 126 | if flags&blackfriday.LIST_TYPE_ORDERED != 0 { 127 | mr.orderedListCounter[mr.listDepth] = 1 128 | } 129 | if !text() { 130 | out.Truncate(marker) 131 | return 132 | } 133 | } 134 | func (mr *markdownRenderer) ListItem(out *bytes.Buffer, text []byte, flags int) { 135 | if flags&blackfriday.LIST_TYPE_ORDERED != 0 { 136 | fmt.Fprintf(out, "%d.", mr.orderedListCounter[mr.listDepth]) 137 | indentwriter.New(out, 1).Write(text) 138 | mr.orderedListCounter[mr.listDepth]++ 139 | } else { 140 | out.WriteString("-") 141 | indentwriter.New(out, 1).Write(text) 142 | } 143 | out.WriteString("\n") 144 | if mr.paragraph[mr.listDepth] { 145 | if flags&blackfriday.LIST_ITEM_END_OF_LIST == 0 { 146 | out.WriteString("\n") 147 | } 148 | mr.paragraph[mr.listDepth] = false 149 | } 150 | } 151 | func (mr *markdownRenderer) Paragraph(out *bytes.Buffer, text func() bool) { 152 | marker := out.Len() 153 | doubleSpace(out) 154 | 155 | mr.paragraph[mr.listDepth] = true 156 | 157 | if !text() { 158 | out.Truncate(marker) 159 | return 160 | } 161 | out.WriteString("\n") 162 | } 163 | 164 | func (mr *markdownRenderer) Table(out *bytes.Buffer, header, body []byte, columnData []int) { 165 | doubleSpace(out) 166 | for column, cell := range mr.headers { 167 | out.WriteByte('|') 168 | out.WriteByte(' ') 169 | out.WriteString(cell) 170 | for i := mr.stringWidth(cell); i < mr.columnWidths[column]; i++ { 171 | out.WriteByte(' ') 172 | } 173 | out.WriteByte(' ') 174 | } 175 | out.WriteString("|\n") 176 | for column, width := range mr.columnWidths { 177 | out.WriteByte('|') 178 | if mr.columnAligns[column]&blackfriday.TABLE_ALIGNMENT_LEFT != 0 { 179 | out.WriteByte(':') 180 | } else { 181 | out.WriteByte('-') 182 | } 183 | for ; width > 0; width-- { 184 | out.WriteByte('-') 185 | } 186 | if mr.columnAligns[column]&blackfriday.TABLE_ALIGNMENT_RIGHT != 0 { 187 | out.WriteByte(':') 188 | } else { 189 | out.WriteByte('-') 190 | } 191 | } 192 | out.WriteString("|\n") 193 | for i := 0; i < len(mr.cells); { 194 | for column := range mr.headers { 195 | cell := []byte(mr.cells[i]) 196 | i++ 197 | out.WriteByte('|') 198 | out.WriteByte(' ') 199 | switch mr.columnAligns[column] { 200 | default: 201 | fallthrough 202 | case blackfriday.TABLE_ALIGNMENT_LEFT: 203 | out.Write(cell) 204 | for i := mr.stringWidth(string(cell)); i < mr.columnWidths[column]; i++ { 205 | out.WriteByte(' ') 206 | } 207 | case blackfriday.TABLE_ALIGNMENT_CENTER: 208 | spaces := mr.columnWidths[column] - mr.stringWidth(string(cell)) 209 | for i := 0; i < spaces/2; i++ { 210 | out.WriteByte(' ') 211 | } 212 | out.Write(cell) 213 | for i := 0; i < spaces-(spaces/2); i++ { 214 | out.WriteByte(' ') 215 | } 216 | case blackfriday.TABLE_ALIGNMENT_RIGHT: 217 | for i := mr.stringWidth(string(cell)); i < mr.columnWidths[column]; i++ { 218 | out.WriteByte(' ') 219 | } 220 | out.Write(cell) 221 | } 222 | out.WriteByte(' ') 223 | } 224 | out.WriteString("|\n") 225 | } 226 | 227 | mr.headers = nil 228 | mr.columnAligns = nil 229 | mr.columnWidths = nil 230 | mr.cells = nil 231 | } 232 | func (*markdownRenderer) TableRow(out *bytes.Buffer, text []byte) { 233 | } 234 | func (mr *markdownRenderer) TableHeaderCell(out *bytes.Buffer, text []byte, align int) { 235 | mr.columnAligns = append(mr.columnAligns, align) 236 | columnWidth := mr.stringWidth(string(text)) 237 | mr.columnWidths = append(mr.columnWidths, columnWidth) 238 | mr.headers = append(mr.headers, string(text)) 239 | } 240 | func (mr *markdownRenderer) TableCell(out *bytes.Buffer, text []byte, align int) { 241 | columnWidth := mr.stringWidth(string(text)) 242 | column := len(mr.cells) % len(mr.headers) 243 | if columnWidth > mr.columnWidths[column] { 244 | mr.columnWidths[column] = columnWidth 245 | } 246 | mr.cells = append(mr.cells, string(text)) 247 | } 248 | 249 | func (*markdownRenderer) Footnotes(out *bytes.Buffer, text func() bool) { 250 | out.WriteString("") // TODO 251 | } 252 | func (*markdownRenderer) FootnoteItem(out *bytes.Buffer, name, text []byte, flags int) { 253 | out.WriteString("") // TODO 254 | } 255 | 256 | // Span-level callbacks. 257 | func (*markdownRenderer) AutoLink(out *bytes.Buffer, link []byte, kind int) { 258 | out.Write(escape(link)) 259 | } 260 | func (*markdownRenderer) CodeSpan(out *bytes.Buffer, text []byte) { 261 | out.WriteByte('`') 262 | out.Write(text) 263 | out.WriteByte('`') 264 | } 265 | func (mr *markdownRenderer) DoubleEmphasis(out *bytes.Buffer, text []byte) { 266 | if mr.opt.Terminal { 267 | out.WriteString("\x1b[1m") // Bold. 268 | } 269 | out.WriteString("**") 270 | out.Write(text) 271 | out.WriteString("**") 272 | if mr.opt.Terminal { 273 | out.WriteString("\x1b[0m") // Reset. 274 | } 275 | } 276 | func (*markdownRenderer) Emphasis(out *bytes.Buffer, text []byte) { 277 | if len(text) == 0 { 278 | return 279 | } 280 | out.WriteByte('*') 281 | out.Write(text) 282 | out.WriteByte('*') 283 | } 284 | func (*markdownRenderer) Image(out *bytes.Buffer, link, title, alt []byte) { 285 | out.WriteString("![") 286 | out.Write(alt) 287 | out.WriteString("](") 288 | out.Write(escape(link)) 289 | if len(title) != 0 { 290 | out.WriteString(` "`) 291 | out.Write(title) 292 | out.WriteString(`"`) 293 | } 294 | out.WriteString(")") 295 | } 296 | func (*markdownRenderer) LineBreak(out *bytes.Buffer) { 297 | out.WriteString(" \n") 298 | } 299 | func (*markdownRenderer) Link(out *bytes.Buffer, link, title, content []byte) { 300 | out.WriteString("[") 301 | out.Write(content) 302 | out.WriteString("](") 303 | out.Write(escape(link)) 304 | if len(title) != 0 { 305 | out.WriteString(` "`) 306 | out.Write(title) 307 | out.WriteString(`"`) 308 | } 309 | out.WriteString(")") 310 | } 311 | func (*markdownRenderer) RawHtmlTag(out *bytes.Buffer, tag []byte) { 312 | out.Write(tag) 313 | } 314 | func (*markdownRenderer) TripleEmphasis(out *bytes.Buffer, text []byte) { 315 | out.WriteString("***") 316 | out.Write(text) 317 | out.WriteString("***") 318 | } 319 | func (*markdownRenderer) StrikeThrough(out *bytes.Buffer, text []byte) { 320 | out.WriteString("~~") 321 | out.Write(text) 322 | out.WriteString("~~") 323 | } 324 | func (*markdownRenderer) FootnoteRef(out *bytes.Buffer, ref []byte, id int) { 325 | out.WriteString("") // TODO 326 | } 327 | 328 | // escape replaces instances of backslash with escaped backslash in text. 329 | func escape(text []byte) []byte { 330 | return bytes.Replace(text, []byte(`\`), []byte(`\\`), -1) 331 | } 332 | 333 | func isNumber(data []byte) bool { 334 | for _, b := range data { 335 | if b < '0' || b > '9' { 336 | return false 337 | } 338 | } 339 | return len(data) > 0 340 | } 341 | 342 | func needsEscaping(text []byte, lastNormalText string) bool { 343 | switch string(text) { 344 | case `\`, 345 | "`", 346 | "*", 347 | "_", 348 | "{", "}", 349 | "[", "]", 350 | //"(", ")", 351 | "#", 352 | "+", 353 | "-": 354 | return true 355 | case "!": 356 | return false 357 | case ".": 358 | // Return true if number, because a period after a number must be escaped to not get parsed as an ordered list. 359 | return isNumber([]byte(lastNormalText)) 360 | case "<", ">": 361 | return true 362 | default: 363 | return false 364 | } 365 | } 366 | 367 | // Low-level callbacks. 368 | func (*markdownRenderer) Entity(out *bytes.Buffer, entity []byte) { 369 | out.Write(entity) 370 | } 371 | func (mr *markdownRenderer) NormalText(out *bytes.Buffer, text []byte) { 372 | normalText := string(text) 373 | if needsEscaping(text, mr.lastNormalText) { 374 | text = append([]byte("\\"), text...) 375 | } 376 | mr.lastNormalText = normalText 377 | if mr.listDepth > 0 && string(text) == "\n" { // TODO: See if this can be cleaned up... It's needed for lists. 378 | return 379 | } 380 | cleanString := cleanWithoutTrim(string(text)) 381 | if cleanString == "" { 382 | return 383 | } 384 | if mr.skipSpaceIfNeededNormalText(out, cleanString) { // Skip first space if last character is already a space (i.e., no need for a 2nd space in a row). 385 | cleanString = cleanString[1:] 386 | } 387 | out.WriteString(cleanString) 388 | if len(cleanString) >= 1 && cleanString[len(cleanString)-1] == ' ' { // If it ends with a space, make note of that. 389 | mr.normalTextMarker[out] = out.Len() 390 | } 391 | } 392 | 393 | // Header and footer. 394 | func (*markdownRenderer) DocumentHeader(out *bytes.Buffer) {} 395 | func (*markdownRenderer) DocumentFooter(out *bytes.Buffer) {} 396 | 397 | func (*markdownRenderer) GetFlags() int { return 0 } 398 | 399 | func (mr *markdownRenderer) skipSpaceIfNeededNormalText(out *bytes.Buffer, cleanString string) bool { 400 | if cleanString[0] != ' ' { 401 | return false 402 | } 403 | if _, ok := mr.normalTextMarker[out]; !ok { 404 | mr.normalTextMarker[out] = -1 405 | } 406 | return mr.normalTextMarker[out] == out.Len() 407 | } 408 | 409 | // cleanWithoutTrim is like clean, but doesn't trim blanks. 410 | func cleanWithoutTrim(s string) string { 411 | var b []byte 412 | var p byte 413 | for i := 0; i < len(s); i++ { 414 | q := s[i] 415 | if q == '\n' || q == '\r' || q == '\t' { 416 | q = ' ' 417 | } 418 | if q != ' ' || p != ' ' { 419 | b = append(b, q) 420 | p = q 421 | } 422 | } 423 | return string(b) 424 | } 425 | 426 | func doubleSpace(out *bytes.Buffer) { 427 | if out.Len() > 0 { 428 | out.WriteByte('\n') 429 | } 430 | } 431 | 432 | // terminalStringWidth returns width of s, taking into account possible ANSI escape codes 433 | // (which don't count towards string width). 434 | func terminalStringWidth(s string) (width int) { 435 | width = runewidth.StringWidth(s) 436 | width -= strings.Count(s, "\x1b[1m") * len("[1m") // HACK, TODO: Find a better way of doing this. 437 | width -= strings.Count(s, "\x1b[0m") * len("[0m") // HACK, TODO: Find a better way of doing this. 438 | return width 439 | } 440 | 441 | // NewRenderer returns a Markdown renderer. 442 | // If opt is nil the defaults are used. 443 | func NewRenderer(opt *Options) blackfriday.Renderer { 444 | mr := &markdownRenderer{ 445 | normalTextMarker: make(map[*bytes.Buffer]int), 446 | orderedListCounter: make(map[int]int), 447 | paragraph: make(map[int]bool), 448 | 449 | stringWidth: runewidth.StringWidth, 450 | } 451 | if opt != nil { 452 | mr.opt = *opt 453 | } 454 | if mr.opt.Terminal { 455 | mr.stringWidth = terminalStringWidth 456 | } 457 | return mr 458 | } 459 | 460 | // Options specifies options for formatting. 461 | type Options struct { 462 | // Terminal specifies if ANSI escape codes are emitted for styling. 463 | Terminal bool 464 | } 465 | 466 | // Process formats Markdown. 467 | // If opt is nil the defaults are used. 468 | // Error can only occur when reading input from filename rather than src. 469 | func Process(filename string, src []byte, opt *Options) ([]byte, error) { 470 | // Get source. 471 | text, err := readSource(filename, src) 472 | if err != nil { 473 | return nil, err 474 | } 475 | 476 | // extensions for GitHub Flavored Markdown-like parsing. 477 | const extensions = blackfriday.EXTENSION_NO_INTRA_EMPHASIS | 478 | blackfriday.EXTENSION_TABLES | 479 | blackfriday.EXTENSION_FENCED_CODE | 480 | blackfriday.EXTENSION_AUTOLINK | 481 | blackfriday.EXTENSION_STRIKETHROUGH | 482 | blackfriday.EXTENSION_SPACE_HEADERS | 483 | blackfriday.EXTENSION_NO_EMPTY_LINE_BEFORE_BLOCK 484 | 485 | output := blackfriday.Markdown(text, NewRenderer(opt), extensions) 486 | return output, nil 487 | } 488 | 489 | // If src != nil, readSource returns src. 490 | // If src == nil, readSource returns the result of reading the file specified by filename. 491 | func readSource(filename string, src []byte) ([]byte, error) { 492 | if src != nil { 493 | return src, nil 494 | } 495 | return ioutil.ReadFile(filename) 496 | } 497 | -------------------------------------------------------------------------------- /markdown/main_test.go: -------------------------------------------------------------------------------- 1 | package markdown_test 2 | 3 | import ( 4 | "bytes" 5 | "flag" 6 | "io/ioutil" 7 | "log" 8 | "os" 9 | "os/exec" 10 | "path/filepath" 11 | "strings" 12 | "testing" 13 | 14 | "github.com/shurcooL/markdownfmt/markdown" 15 | ) 16 | 17 | func Example() { 18 | input := []byte(`Title 19 | = 20 | 21 | This is a new paragraph. I wonder if I have too many spaces. 22 | What about new paragraph. 23 | But the next one... 24 | 25 | Is really new. 26 | 27 | 1. Item one. 28 | 1. Item TWO. 29 | 30 | 31 | Final paragraph. 32 | `) 33 | 34 | output, err := markdown.Process("", input, nil) 35 | if err != nil { 36 | log.Fatalln(err) 37 | } 38 | 39 | os.Stdout.Write(output) 40 | 41 | // Output: 42 | // Title 43 | // ===== 44 | // 45 | // This is a new paragraph. I wonder if I have too many spaces. What about new paragraph. But the next one... 46 | // 47 | // Is really new. 48 | // 49 | // 1. Item one. 50 | // 2. Item TWO. 51 | // 52 | // Final paragraph. 53 | // 54 | } 55 | 56 | func Example_two() { 57 | input := []byte(`Title 58 | == 59 | 60 | Subtitle 61 | --- 62 | 63 | How about ` + "`this`" + ` and other stuff like *italic*, **bold** and ***super extra***. 64 | `) 65 | 66 | output, err := markdown.Process("", input, nil) 67 | if err != nil { 68 | log.Fatalln(err) 69 | } 70 | 71 | os.Stdout.Write(output) 72 | 73 | // Output: 74 | // Title 75 | // ===== 76 | // 77 | // Subtitle 78 | // -------- 79 | // 80 | // How about `this` and other stuff like *italic*, **bold** and ***super extra***. 81 | // 82 | } 83 | 84 | var updateFlag = flag.Bool("update", false, "Update golden files.") 85 | 86 | func Test(t *testing.T) { 87 | fis, err := ioutil.ReadDir("testdata") 88 | if err != nil { 89 | t.Fatal(err) 90 | } 91 | for _, fi := range fis { 92 | if !strings.HasSuffix(fi.Name(), ".in.md") { 93 | continue 94 | } 95 | name := strings.TrimSuffix(fi.Name(), ".in.md") 96 | t.Run(name, func(t *testing.T) { 97 | got, err := markdown.Process(filepath.Join("testdata", name+".in.md"), nil, nil) 98 | if err != nil { 99 | t.Fatal("markdown.Process:", err) 100 | } 101 | if *updateFlag { 102 | err := ioutil.WriteFile(filepath.Join("testdata", name+".golden.md"), got, 0644) 103 | if err != nil { 104 | t.Fatal(err) 105 | } 106 | return 107 | } 108 | 109 | want, err := ioutil.ReadFile(filepath.Join("testdata", name+".golden.md")) 110 | if err != nil { 111 | t.Fatal(err) 112 | } 113 | 114 | diff, err := diff(got, want) 115 | if err != nil { 116 | t.Fatal(err) 117 | } 118 | if len(diff) != 0 { 119 | t.Errorf("difference of %d lines:\n%s", bytes.Count(diff, []byte("\n")), string(diff)) 120 | } 121 | }) 122 | } 123 | } 124 | 125 | // TODO: Factor out. 126 | func diff(b1, b2 []byte) (data []byte, err error) { 127 | f1, err := ioutil.TempFile("", "markdownfmt") 128 | if err != nil { 129 | return 130 | } 131 | defer os.Remove(f1.Name()) 132 | defer f1.Close() 133 | 134 | f2, err := ioutil.TempFile("", "markdownfmt") 135 | if err != nil { 136 | return 137 | } 138 | defer os.Remove(f2.Name()) 139 | defer f2.Close() 140 | 141 | f1.Write(b1) 142 | f2.Write(b2) 143 | 144 | data, err = exec.Command("diff", "-u", f1.Name(), f2.Name()).CombinedOutput() 145 | if len(data) > 0 { 146 | // diff exits with a non-zero status when the files don't match. 147 | // Ignore that failure as long as we get output. 148 | err = nil 149 | } 150 | return 151 | } 152 | -------------------------------------------------------------------------------- /markdown/stringwidth_test.go: -------------------------------------------------------------------------------- 1 | package markdown 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "github.com/mattn/go-runewidth" 8 | ) 9 | 10 | // Test that string "**bold**" has width 8 when terminal is off, and no ANSI escape codes are added. 11 | func TestNormalStringWidth(t *testing.T) { 12 | r := NewRenderer(nil).(*markdownRenderer) 13 | var buf bytes.Buffer 14 | r.DoubleEmphasis(&buf, []byte("bold")) 15 | if got, want := buf.String(), "**bold**"; got != want { 16 | t.Errorf("got %q, want %q", got, want) 17 | } 18 | if got, want := r.stringWidth("**bold**"), len("**bold**"); got != want { 19 | t.Errorf("got %v, want %v", got, want) 20 | } 21 | if got, want := r.stringWidth("\x1b[1m**bold**\x1b[0m"), runewidth.StringWidth("\x1b[1m**bold**\x1b[0m"); got != want { 22 | t.Errorf("got %q, want %q", got, want) 23 | } 24 | } 25 | 26 | // Test that string "\x1b[1m**bold**\x1b[0m" has width 8 when terminal is on. ANSI escape codes should not count for string width. 27 | func TestTerminalStringWidth(t *testing.T) { 28 | r := NewRenderer(&Options{Terminal: true}).(*markdownRenderer) 29 | var buf bytes.Buffer 30 | r.DoubleEmphasis(&buf, []byte("bold")) 31 | if got, want := buf.String(), "\x1b[1m**bold**\x1b[0m"; got != want { 32 | t.Errorf("got %q, want %q", got, want) 33 | } 34 | if got, want := r.stringWidth("\x1b[1m**bold**\x1b[0m"), len("**bold**"); got != want { 35 | t.Errorf("got %v, want %v", got, want) 36 | } 37 | if got, dontWant := r.stringWidth("\x1b[1m**bold**\x1b[0m"), runewidth.StringWidth("\x1b[1m**bold**\x1b[0m"); got == dontWant { 38 | t.Errorf("got %q, dontWant %q", got, dontWant) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /markdown/testdata/escapeurl.golden.md: -------------------------------------------------------------------------------- 1 | Test for issue https://github.com/shurcooL/markdownfmt/issues/35. 2 | 3 | [Link](path\\to\\page) 4 | 5 | ![Image](path\\to\\image) 6 | 7 | https://path\\to\\page 8 | -------------------------------------------------------------------------------- /markdown/testdata/escapeurl.in.md: -------------------------------------------------------------------------------- 1 | Test for issue https://github.com/shurcooL/markdownfmt/issues/35. 2 | 3 | [Link](path\\to\\page) 4 | 5 | ![Image](path\\to\\image) 6 | 7 | https://path\\to\\page 8 | -------------------------------------------------------------------------------- /markdown/testdata/linebreak.golden.md: -------------------------------------------------------------------------------- 1 | Some text with two trailing spaces for linebreak. 2 | More spaced **text** *immediately* after that. 3 | More than two spaces become two. 4 | -------------------------------------------------------------------------------- /markdown/testdata/linebreak.in.md: -------------------------------------------------------------------------------- 1 | Some text with two trailing spaces for linebreak. 2 | More spaced **text** *immediately* after that. 3 | More than two spaces become two. 4 | -------------------------------------------------------------------------------- /markdown/testdata/listend.golden.md: -------------------------------------------------------------------------------- 1 | Test that when the document ends with a double spaced list, an extra blank line isn't appended. See issue #30. 2 | 3 | - An item. 4 | 5 | - Another time with a blank line in between. 6 | -------------------------------------------------------------------------------- /markdown/testdata/listend.in.md: -------------------------------------------------------------------------------- 1 | Test that when the document ends with a double spaced list, an extra blank line isn't appended. See issue #30. 2 | 3 | - An item. 4 | 5 | - Another time with a blank line in between. 6 | -------------------------------------------------------------------------------- /markdown/testdata/reference.golden.md: -------------------------------------------------------------------------------- 1 | An h1 header 2 | ============ 3 | 4 | Paragraphs are separated by a blank line. 5 | 6 | 2nd paragraph. *Italic*, **bold**, `monospace`. Itemized lists look like: 7 | 8 | - this one 9 | - that one 10 | - the other one 11 | 12 | Nothing to note here. 13 | 14 | > Block quotes are written like so. 15 | > 16 | > > They can be nested. 17 | > 18 | > They can span multiple paragraphs, if you like. 19 | 20 | - Item 1 21 | - Item 2 22 | - Item 2a 23 | - Item 2a 24 | - Item 2b 25 | - Item 3 26 | 27 | Hmm. 28 | 29 | 1. Item 1 30 | 2. Item 2 31 | 1. Blah. 32 | 2. Blah. 33 | 3. Item 3 34 | - Item 3a 35 | - Item 3b 36 | 37 | Large spacing... 38 | 39 | 1. An entire paragraph is written here, and bigger spacing between list items is desired. This is supported too. 40 | 41 | 2. Item 2 42 | 43 | 1. Blah. 44 | 45 | 2. Blah. 46 | 47 | 3. Item 3 48 | 49 | - Item 3a 50 | 51 | - Item 3b 52 | 53 | Last paragraph here. 54 | 55 | An h2 header 56 | ------------ 57 | 58 | - Paragraph right away. 59 | - **Big item**: Right away after header. 60 | 61 | [Visit GitHub!](www.github.com) 62 | 63 | ![Hmm](http://example.org/image.png) 64 | 65 | ![Alt text](/path/to/img.jpg "Optional title") ![Alt text](/path/to/img.jpg "Hello \" 世界") 66 | 67 | ~~Mistaken text.~~ 68 | 69 | This (**should** be *fine*). 70 | 71 | A \> B. 72 | 73 | It's possible to backslash escape \ tags and \`backticks\`. They are treated as text. 74 | 75 | 1986\. What a great season. 76 | 77 | The year was 1986. What a great season. 78 | 79 | \*literal asterisks\*. 80 | 81 | --- 82 | 83 | http://example.com 84 | 85 | Now a [link](www.github.com) in a paragraph. End with [link_underscore.go](www.github.com). 86 | 87 | - [Link](www.example.com) 88 | 89 | ### An h3 header 90 | 91 | Here's a numbered list: 92 | 93 | 1. first item 94 | 2. second item 95 | 3. third item 96 | 97 | Note again how the actual text starts at 4 columns in (4 characters from the left side). Here's a code sample: 98 | 99 | ``` 100 | # Let me re-iterate ... 101 | for i in 1 .. 10 { do-something(i) } 102 | ``` 103 | 104 | As you probably guessed, indented 4 spaces. By the way, instead of indenting the block, you can use delimited blocks, if you like: 105 | 106 | ``` 107 | define foobar() { 108 | print "Welcome to flavor country!"; 109 | } 110 | ``` 111 | 112 | (which makes copying & pasting easier). You can optionally mark the delimited block for Pandoc to syntax highlight it: 113 | 114 | ```Go 115 | func main() { 116 | println("Hi.") 117 | } 118 | ``` 119 | 120 | Here's a table. 121 | 122 | | Name | Age | 123 | |-------|-----| 124 | | Bob | 27 | 125 | | Alice | 23 | 126 | 127 | Colons can be used to align columns. 128 | 129 | | Tables | Are | Cool | 130 | |---------------|:-------------:|----------:| 131 | | col 3 is | right-aligned | $1600 | 132 | | col 2 is | centered! | $12 | 133 | | zebra stripes | are neat | $1 | 134 | | support for | サブタイトル | priceless | 135 | 136 | The outer pipes (|) are optional, and you don't need to make the raw Markdown line up prettily. You can also use inline Markdown. 137 | 138 | | Markdown | More | Pretty | 139 | |----------|-----------|------------| 140 | | *Still* | `renders` | **nicely** | 141 | | 1 | 2 | 3 | 142 | 143 | Nested Lists 144 | ============ 145 | 146 | ### Codeblock within list 147 | 148 | - list1 149 | 150 | ```C 151 | if (i == 5) 152 | break; 153 | ``` 154 | 155 | ### Blockquote within list 156 | 157 | - list1 158 | 159 | > This a quote within a list. 160 | 161 | ### Table within list 162 | 163 | - list1 164 | 165 | | Header One | Header Two | 166 | |------------|------------| 167 | | Item One | Item Two | 168 | 169 | ### Multi-level nested 170 | 171 | - Item 1 172 | 173 | Another paragraph inside this list item is indented just like the previous paragraph. 174 | 175 | - Item 2 176 | 177 | - Item 2a 178 | 179 | Things go here. 180 | 181 | > This a quote within a list. 182 | 183 | And they stay here. 184 | 185 | - Item 2b 186 | 187 | - Item 3 188 | 189 | Line Breaks 190 | =========== 191 | 192 | Some text with two trailing spaces for linebreak. 193 | More text immediately after. 194 | Useful for writing poems. 195 | 196 | Done. 197 | -------------------------------------------------------------------------------- /markdown/testdata/reference.in.md: -------------------------------------------------------------------------------- 1 | An h1 header 2 | ============ 3 | 4 | Paragraphs are separated by a blank line. 5 | 6 | 2nd paragraph. *Italic*, **bold**, `monospace`. Itemized lists look like: 7 | 8 | - this one 9 | - that one 10 | - the other one 11 | 12 | Nothing to note here. 13 | 14 | > Block quotes are written like so. 15 | > 16 | > > They can be nested. 17 | > 18 | > They can span multiple paragraphs, if you like. 19 | 20 | - Item 1 21 | - Item 2 22 | - Item 2a 23 | - Item 2a 24 | - Item 2b 25 | - Item 3 26 | 27 | Hmm. 28 | 29 | 1. Item 1 30 | 2. Item 2 31 | 1. Blah. 32 | 2. Blah. 33 | 3. Item 3 34 | - Item 3a 35 | - Item 3b 36 | 37 | Large spacing... 38 | 39 | 1. An entire paragraph is written here, and bigger spacing between list items is desired. This is supported too. 40 | 41 | 2. Item 2 42 | 43 | 1. Blah. 44 | 45 | 2. Blah. 46 | 47 | 3. Item 3 48 | 49 | - Item 3a 50 | 51 | - Item 3b 52 | 53 | Last paragraph here. 54 | 55 | An h2 header 56 | ------------ 57 | 58 | - Paragraph right away. 59 | - **Big item**: Right away after header. 60 | 61 | [Visit GitHub!](www.github.com) 62 | 63 | ![Hmm](http://example.org/image.png) 64 | 65 | ![Alt text](/path/to/img.jpg "Optional title") ![Alt text](/path/to/img.jpg "Hello \" 世界") 66 | 67 | ~~Mistaken text.~~ 68 | 69 | This (**should** be *fine*). 70 | 71 | A \> B. 72 | 73 | It's possible to backslash escape \ tags and \`backticks\`. They are treated as text. 74 | 75 | 1986\. What a great season. 76 | 77 | The year was 1986. What a great season. 78 | 79 | \*literal asterisks\*. 80 | 81 | --- 82 | 83 | http://example.com 84 | 85 | Now a [link](www.github.com) in a paragraph. End with [link_underscore.go](www.github.com). 86 | 87 | - [Link](www.example.com) 88 | 89 | ### An h3 header 90 | 91 | Here's a numbered list: 92 | 93 | 1. first item 94 | 2. second item 95 | 3. third item 96 | 97 | Note again how the actual text starts at 4 columns in (4 characters from the left side). Here's a code sample: 98 | 99 | ``` 100 | # Let me re-iterate ... 101 | for i in 1 .. 10 { do-something(i) } 102 | ``` 103 | 104 | As you probably guessed, indented 4 spaces. By the way, instead of indenting the block, you can use delimited blocks, if you like: 105 | 106 | ``` 107 | define foobar() { 108 | print "Welcome to flavor country!"; 109 | } 110 | ``` 111 | 112 | (which makes copying & pasting easier). You can optionally mark the delimited block for Pandoc to syntax highlight it: 113 | 114 | ```Go 115 | func main() { 116 | println("Hi.") 117 | } 118 | ``` 119 | 120 | Here's a table. 121 | 122 | | Name | Age | 123 | |-------|-----| 124 | | Bob | 27 | 125 | | Alice | 23 | 126 | 127 | Colons can be used to align columns. 128 | 129 | | Tables | Are | Cool | 130 | |---------------|:-------------:|----------:| 131 | | col 3 is | right-aligned | $1600 | 132 | | col 2 is | centered! | $12 | 133 | | zebra stripes | are neat | $1 | 134 | | support for | サブタイトル | priceless | 135 | 136 | The outer pipes (|) are optional, and you don't need to make the raw Markdown line up prettily. You can also use inline Markdown. 137 | 138 | | Markdown | More | Pretty | 139 | |----------|-----------|------------| 140 | | *Still* | `renders` | **nicely** | 141 | | 1 | 2 | 3 | 142 | 143 | Nested Lists 144 | ============ 145 | 146 | ### Codeblock within list 147 | 148 | - list1 149 | 150 | ```C 151 | if (i == 5) 152 | break; 153 | ``` 154 | 155 | ### Blockquote within list 156 | 157 | - list1 158 | 159 | > This a quote within a list. 160 | 161 | ### Table within list 162 | 163 | - list1 164 | 165 | | Header One | Header Two | 166 | |------------|------------| 167 | | Item One | Item Two | 168 | 169 | ### Multi-level nested 170 | 171 | - Item 1 172 | 173 | Another paragraph inside this list item is indented just like the previous paragraph. 174 | 175 | - Item 2 176 | 177 | - Item 2a 178 | 179 | Things go here. 180 | 181 | > This a quote within a list. 182 | 183 | And they stay here. 184 | 185 | - Item 2b 186 | 187 | - Item 3 188 | 189 | Line Breaks 190 | =========== 191 | 192 | Some text with two trailing spaces for linebreak. 193 | More text immediately after. 194 | Useful for writing poems. 195 | 196 | Done. 197 | -------------------------------------------------------------------------------- /markdown/testdata/successive.golden.md: -------------------------------------------------------------------------------- 1 | Test for issue https://github.com/shurcooL/markdownfmt/issues/20. 2 | 3 | text text 4 | 5 | [link](https://github.com) text 6 | 7 | *italic* text 8 | 9 | **bold** text 10 | 11 | ***massive*** text 12 | 13 | `noformat` text 14 | 15 | text [link](https://github.com) 16 | 17 | text *italic* 18 | 19 | text **bold** 20 | 21 | text ***massive*** 22 | 23 | text `noformat` 24 | -------------------------------------------------------------------------------- /markdown/testdata/successive.in.md: -------------------------------------------------------------------------------- 1 | Test for issue https://github.com/shurcooL/markdownfmt/issues/20. 2 | 3 | text 4 | text 5 | 6 | [link](https://github.com) 7 | text 8 | 9 | *italic* 10 | text 11 | 12 | **bold** 13 | text 14 | 15 | ***massive*** 16 | text 17 | 18 | `noformat` 19 | text 20 | 21 | text 22 | [link](https://github.com) 23 | 24 | text 25 | *italic* 26 | 27 | text 28 | **bold** 29 | 30 | text 31 | ***massive*** 32 | 33 | text 34 | `noformat` 35 | -------------------------------------------------------------------------------- /markdown/testdata/url.golden.md: -------------------------------------------------------------------------------- 1 | Lorem [ipsum](https://www.example.com). 2 | 3 | Lorem [ipsum](https://www.example.com). 4 | -------------------------------------------------------------------------------- /markdown/testdata/url.in.md: -------------------------------------------------------------------------------- 1 | Lorem [ipsum](https://www.example.com). 2 | 3 | Lorem [ipsum](https://www.example.com)\. 4 | -------------------------------------------------------------------------------- /markdown/testdata/widechar.golden.md: -------------------------------------------------------------------------------- 1 | タイトル 2 | ======== 3 | 4 | サブタイトル 5 | ------------ 6 | 7 | aaa/あああ 8 | ---------- 9 | -------------------------------------------------------------------------------- /markdown/testdata/widechar.in.md: -------------------------------------------------------------------------------- 1 | タイトル 2 | == 3 | 4 | サブタイトル 5 | --- 6 | 7 | aaa/あああ 8 | ---------- 9 | --------------------------------------------------------------------------------