diff --git a/internal/text/License b/internal/text/License new file mode 100644 index 0000000..480a328 --- /dev/null +++ b/internal/text/License @@ -0,0 +1,19 @@ +Copyright 2012 Keith Rarick + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/internal/text/Readme b/internal/text/Readme new file mode 100644 index 0000000..7e6e7c0 --- /dev/null +++ b/internal/text/Readme @@ -0,0 +1,3 @@ +This is a Go package for manipulating paragraphs of text. + +See http://go.pkgdoc.org/github.com/kr/text for full documentation. diff --git a/internal/text/doc.go b/internal/text/doc.go new file mode 100644 index 0000000..cf4c198 --- /dev/null +++ b/internal/text/doc.go @@ -0,0 +1,3 @@ +// Package text provides rudimentary functions for manipulating text in +// paragraphs. +package text diff --git a/internal/text/indent.go b/internal/text/indent.go new file mode 100644 index 0000000..4ebac45 --- /dev/null +++ b/internal/text/indent.go @@ -0,0 +1,74 @@ +package text + +import ( + "io" +) + +// Indent inserts prefix at the beginning of each non-empty line of s. The +// end-of-line marker is NL. +func Indent(s, prefix string) string { + return string(IndentBytes([]byte(s), []byte(prefix))) +} + +// IndentBytes inserts prefix at the beginning of each non-empty line of b. +// The end-of-line marker is NL. +func IndentBytes(b, prefix []byte) []byte { + var res []byte + bol := true + for _, c := range b { + if bol && c != '\n' { + res = append(res, prefix...) + } + res = append(res, c) + bol = c == '\n' + } + return res +} + +// Writer indents each line of its input. +type indentWriter struct { + w io.Writer + bol bool + pre [][]byte + sel int + off int +} + +// NewIndentWriter makes a new write filter that indents the input +// lines. Each line is prefixed in order with the corresponding +// element of pre. If there are more lines than elements, the last +// element of pre is repeated for each subsequent line. +func NewIndentWriter(w io.Writer, pre ...[]byte) io.Writer { + return &indentWriter{ + w: w, + pre: pre, + bol: true, + } +} + +// The only errors returned are from the underlying indentWriter. +func (w *indentWriter) Write(p []byte) (n int, err error) { + for _, c := range p { + if w.bol { + var i int + i, err = w.w.Write(w.pre[w.sel][w.off:]) + w.off += i + if err != nil { + return n, err + } + } + _, err = w.w.Write([]byte{c}) + if err != nil { + return n, err + } + n++ + w.bol = c == '\n' + if w.bol { + w.off = 0 + if w.sel < len(w.pre)-1 { + w.sel++ + } + } + } + return n, nil +} diff --git a/internal/text/indent_test.go b/internal/text/indent_test.go new file mode 100644 index 0000000..5c723ee --- /dev/null +++ b/internal/text/indent_test.go @@ -0,0 +1,119 @@ +package text + +import ( + "bytes" + "testing" +) + +type T struct { + inp, exp, pre string +} + +var tests = []T{ + { + "The quick brown fox\njumps over the lazy\ndog.\nBut not quickly.\n", + "xxxThe quick brown fox\nxxxjumps over the lazy\nxxxdog.\nxxxBut not quickly.\n", + "xxx", + }, + { + "The quick brown fox\njumps over the lazy\ndog.\n\nBut not quickly.", + "xxxThe quick brown fox\nxxxjumps over the lazy\nxxxdog.\n\nxxxBut not quickly.", + "xxx", + }, +} + +func TestIndent(t *testing.T) { + for _, test := range tests { + got := Indent(test.inp, test.pre) + if got != test.exp { + t.Errorf("mismatch %q != %q", got, test.exp) + } + } +} + +type IndentWriterTest struct { + inp, exp string + pre []string +} + +var ts = []IndentWriterTest{ + { + ` +The quick brown fox +jumps over the lazy +dog. +But not quickly. +`[1:], + ` +xxxThe quick brown fox +xxxjumps over the lazy +xxxdog. +xxxBut not quickly. +`[1:], + []string{"xxx"}, + }, + { + ` +The quick brown fox +jumps over the lazy +dog. +But not quickly. +`[1:], + ` +xxaThe quick brown fox +xxxjumps over the lazy +xxxdog. +xxxBut not quickly. +`[1:], + []string{"xxa", "xxx"}, + }, + { + ` +The quick brown fox +jumps over the lazy +dog. +But not quickly. +`[1:], + ` +xxaThe quick brown fox +xxbjumps over the lazy +xxcdog. +xxxBut not quickly. +`[1:], + []string{"xxa", "xxb", "xxc", "xxx"}, + }, + { + ` +The quick brown fox +jumps over the lazy +dog. + +But not quickly.`[1:], + ` +xxaThe quick brown fox +xxxjumps over the lazy +xxxdog. +xxx +xxxBut not quickly.`[1:], + []string{"xxa", "xxx"}, + }, +} + +func TestIndentWriter(t *testing.T) { + for _, test := range ts { + b := new(bytes.Buffer) + pre := make([][]byte, len(test.pre)) + for i := range test.pre { + pre[i] = []byte(test.pre[i]) + } + w := NewIndentWriter(b, pre...) + if _, err := w.Write([]byte(test.inp)); err != nil { + t.Error(err) + } + if got := b.String(); got != test.exp { + t.Errorf("mismatch %q != %q", got, test.exp) + t.Log(got) + t.Log(test.exp) + } + } +} diff --git a/internal/text/wrap.go b/internal/text/wrap.go new file mode 100644 index 0000000..71d1710 --- /dev/null +++ b/internal/text/wrap.go @@ -0,0 +1,87 @@ +package text + +import ( + "bytes" + "math" +) + +var ( + nl = []byte{'\n'} + sp = []byte{' '} +) + +const defaultPenalty = 1e5 + +// Wrap wraps s into a paragraph of lines of length lim, with minimal raggedness. +func Wrap(s, tab string, lim int) string { + return string(WrapBytes([]byte(s), []byte(tab), lim)) +} + +// WrapBytes wraps b into a paragraph of lines of length lim, with minimal raggedness. +func WrapBytes(b, tab []byte, lim int) []byte { + words := bytes.Split(bytes.Replace(bytes.TrimSpace(b), nl, sp, -1), sp) + var lines [][]byte + if len(words) > 0 { + words[0] = append(tab, words[0]...) + } + for _, line := range WrapWords(words, 1, lim, defaultPenalty) { + lines = append(lines, bytes.Join(line, sp)) + } + return bytes.Join(lines, nl) +} + +// WrapWords is the low-level line-breaking algorithm, useful if you need more +// control over the details of the text wrapping process. For most uses, either +// Wrap or WrapBytes will be sufficient and more convenient. +// +// WrapWords splits a list of words into lines with minimal "raggedness", +// treating each byte as one unit, accounting for spc units between adjacent +// words on each line, and attempting to limit lines to lim units. Raggedness +// is the total error over all lines, where error is the square of the +// difference of the length of the line and lim. Too-long lines (which only +// happen when a single word is longer than lim units) have pen penalty units +// added to the error. +func WrapWords(words [][]byte, spc, lim, pen int) [][][]byte { + n := len(words) + + length := make([][]int, n) + for i := 0; i < n; i++ { + length[i] = make([]int, n) + length[i][i] = len(words[i]) + for j := i + 1; j < n; j++ { + length[i][j] = length[i][j-1] + spc + len(words[j]) + } + } + + nbrk := make([]int, n) + cost := make([]int, n) + for i := range cost { + cost[i] = math.MaxInt32 + } + for i := n - 1; i >= 0; i-- { + if length[i][n-1] <= lim || i == n-1 { + cost[i] = 0 + nbrk[i] = n + } else { + for j := i + 1; j < n; j++ { + d := lim - length[i][j-1] + c := d*d + cost[j] + if length[i][j-1] > lim { + c += pen // too-long lines get a worse penalty + } + if c < cost[i] { + cost[i] = c + nbrk[i] = j + } + } + } + } + + var lines [][][]byte + i := 0 + for i < n { + lines = append(lines, words[i:nbrk[i]]) + i = nbrk[i] + } + return lines +} diff --git a/internal/text/wrap_test.go b/internal/text/wrap_test.go new file mode 100644 index 0000000..4c393aa --- /dev/null +++ b/internal/text/wrap_test.go @@ -0,0 +1,62 @@ +package text + +import ( + "bytes" + "testing" +) + +var text = "The quick brown fox jumps over the lazy dog." + +func TestWrap(t *testing.T) { + exp := [][]string{ + {"The", "quick", "brown", "fox"}, + {"jumps", "over", "the", "lazy", "dog."}, + } + words := bytes.Split([]byte(text), sp) + got := WrapWords(words, 1, 24, defaultPenalty) + if len(exp) != len(got) { + t.Fail() + } + for i := range exp { + if len(exp[i]) != len(got[i]) { + t.Fail() + } + for j := range exp[i] { + if exp[i][j] != string(got[i][j]) { + t.Fatal(i, exp[i][j], got[i][j]) + } + } + } +} + +func TestWrapNarrow(t *testing.T) { + exp := "The\nquick\nbrown\nfox\njumps\nover\nthe\nlazy\ndog." + if Wrap(text, "", 5) != exp { + t.Fail() + } +} + +func TestWrapOneLine(t *testing.T) { + exp := "The quick brown fox jumps over the lazy dog." + if Wrap(text, "", 500) != exp { + t.Fail() + } +} + +func TestWrapBug1(t *testing.T) { + cases := []struct { + limit int + text string + want string + }{ + {4, "aaaaa", "aaaaa"}, + {4, "a aaaaa", "a\naaaaa"}, + } + + for _, test := range cases { + got := Wrap(test.text, "", test.limit) + if got != test.want { + t.Errorf("Wrap(%q, %d) = %q want %q", test.text, test.limit, got, test.want) + } + } +} diff --git a/mmark.go b/mmark.go index e16c8b6..7b79ef8 100644 --- a/mmark.go +++ b/mmark.go @@ -16,6 +16,7 @@ import ( "github.com/mmarkdown/mmark/v2/mparser" "github.com/mmarkdown/mmark/v2/render/man" "github.com/mmarkdown/mmark/v2/render/mhtml" + "github.com/mmarkdown/mmark/v2/render/text" "github.com/mmarkdown/mmark/v2/render/xml" ) @@ -28,10 +29,12 @@ var ( flagHTML = flag.Bool("html", false, "create HTML output") flagIndex = flag.Bool("index", true, "generate an index at the end of the document") flagMan = flag.Bool("man", false, "generate manual pages (nroff)") + flagText = flag.Bool("text", false, "generate text for ANSI escapes") flagUnsafe = flag.Bool("unsafe", false, "allow unsafe includes") flagIntraEmph = flag.Bool("intra-emphasis", false, "interpret camel_case_value as emphasizing \"case\" (legacy behavior)") flagVersion = flag.Bool("version", false, "show mmark version") flagUnicode = flag.Bool("unicode", true, "from xml2rfc 3.16 onwards unicode is allowed in ") + flagWidth = flag.Int("width", 100, "text width when generating markdown") ) func main() { @@ -178,6 +181,9 @@ func main() { opts.Flags |= man.ManFragment } renderer = man.NewRenderer(opts) + case *flagText: + opts := text.RendererOptions{TextWidth: *flagWidth} + renderer = text.NewRenderer(opts) default: opts := xml.RendererOptions{ Flags: xml.CommonFlags, diff --git a/render/text/helpers.go b/render/text/helpers.go new file mode 100644 index 0000000..ac83889 --- /dev/null +++ b/render/text/helpers.go @@ -0,0 +1,131 @@ +package text + +import ( + "bytes" + "io" + "regexp" + + "github.com/gomarkdown/markdown/ast" + mtext "github.com/mmarkdown/mmark/v2/internal/text" +) + +func (r *Renderer) outOneOf(w io.Writer, outFirst bool, first, second string) {} +func (r *Renderer) outPrefix(w io.Writer) { r.out(w, r.prefix.flatten()); r.suppress = false } +func (r *Renderer) endline(w io.Writer) { r.outs(w, "\n"); r.suppress = false } +func (r *Renderer) outs(w io.Writer, s string) { w.Write([]byte(s)); r.suppress = false } +func (r *Renderer) out(w io.Writer, d []byte) { w.Write(d); r.suppress = false } + +func (r *Renderer) newline(w io.Writer) { + if r.suppress { + return + } + r.out(w, r.prefix.flatten()) + r.outs(w, "\n") + r.suppress = true +} + +var re = regexp.MustCompile(" +") + +// lastNode returns true if we are the last node under this parent. +func lastNode(node ast.Node) bool { return ast.GetNextNode(node) == nil } + +// wrapText wraps the text in data, taking len(prefix) into account. +func (r *Renderer) wrapText(data, prefix, tab []byte) []byte { + replaced := re.ReplaceAll(data, []byte(" ")) + wrapped := mtext.WrapBytes(replaced, tab, r.opts.TextWidth-len(prefix)) + return r.indentText(wrapped, prefix) +} + +func (r *Renderer) indentText(data, prefix []byte) []byte { + return mtext.IndentBytes(data, prefix) +} + +type prefixStack struct { + p [][]byte +} + +func (r *Renderer) pop() []byte { + last := r.prefix.pop() + if last != nil && r.prefix.len() == 0 { + r.suppress = false + } + return last +} + +func (r *Renderer) push(data []byte) { r.prefix.push(data) } +func (r *Renderer) peek() int { return r.prefix.peek() } + +func (p *prefixStack) push(data []byte) { p.p = append(p.p, data) } + +func (p *prefixStack) pop() []byte { + if len(p.p) == 0 { + return nil + } + last := p.p[len(p.p)-1] + p.p = p.p[:len(p.p)-1] + return last +} + +// peek returns the lenght of the last pushed element. +func (p *prefixStack) peek() int { + if len(p.p) == 0 { + return 0 + } + last := p.p[len(p.p)-1] + return len(last) +} + +// flatten flattens the stack in reverse order. +func (p *prefixStack) flatten() []byte { + ret := []byte{} + for _, b := range p.p { + ret = append(ret, b...) + } + return ret +} + +func (p *prefixStack) len() (l int) { + for _, b := range p.p { + l += len(b) + } + return l +} + +// listPrefixLength returns the length of the prefix we need for list in ast.Node +func listPrefixLength(list *ast.List, start int) int { + numChild := len(list.Children) + start + switch { + case numChild < 10: + return 3 + case numChild < 100: + return 4 + case numChild < 1000: + return 5 + } + return 6 // bit of a ridicules list +} + +func Space(length int) []byte { return bytes.Repeat([]byte(" "), length) } + +type ansiStack []string + +func (a *ansiStack) push(code string) { *a = append(*a, code) } + +func (a *ansiStack) pop() string { + if len(*a) == 0 { + return "" + } + last := (*a)[len(*a)-1] + *a = (*a)[:len(*a)-1] + return last +} + +func (a *ansiStack) print(w io.Writer) { + for _, code := range *a { + io.WriteString(w, code) + } +} + +func isSpace(c byte) bool { + return c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\f' || c == '\v' +} diff --git a/render/text/renderer.go b/render/text/renderer.go new file mode 100644 index 0000000..a6e1103 --- /dev/null +++ b/render/text/renderer.go @@ -0,0 +1,727 @@ +// The package text outputs text. +package text + +import ( + "bufio" + "bytes" + "fmt" + "io" + "strconv" + + "github.com/gomarkdown/markdown/ast" + "github.com/gomarkdown/markdown/html" + "github.com/mmarkdown/mmark/v2/mast" +) + +// Flags control optional behavior of Markdown renderer. +type Flags int + +// Markdown renderer configuration options. +const ( + FlagsNone Flags = 0 + + CommonFlags Flags = FlagsNone +) + +// RendererOptions is a collection of supplementary parameters tweaking +// the behavior of various parts of Markdown renderer. +type RendererOptions struct { + Flags Flags // Flags allow customizing this renderer's behavior + + TextWidth int + + // if set, called at the start of RenderNode(). Allows replacing rendering of some nodes + RenderNodeHook html.RenderNodeFunc +} + +// Renderer implements Renderer interface for Markdown output. +type Renderer struct { + opts RendererOptions + + // TODO(miek): paraStart should probably be a stack, aside in para in aside, etc. + paraStart int + headingStart int + + prefix *prefixStack // track current prefix, quote, aside, etc. + + // tables + cellStart int + col int + colWidth []int + colAlign []ast.CellAlignFlags + tableType ast.Node + + suppress bool // when true we suppress newlines + + deferredFootBuf *bytes.Buffer // deferred footnote buffer. Appended to the doc at the end. + deferredFootID map[string]struct{} + + deferredLinkBuf *bytes.Buffer // deferred footnote buffer. Appended to the doc at the end. + deferredLinkID map[string]struct{} +} + +// NewRenderer creates and configures an Renderer object, which satisfies the Renderer interface. +func NewRenderer(opts RendererOptions) *Renderer { + if opts.TextWidth == 0 { + opts.TextWidth = 80 + } + r := &Renderer{ + opts: opts, + prefix: &prefixStack{p: [][]byte{}}, + deferredFootBuf: &bytes.Buffer{}, + deferredFootID: make(map[string]struct{}), + deferredLinkBuf: &bytes.Buffer{}, + deferredLinkID: make(map[string]struct{}), + } + r.push(Space(0)) // default indent for all text, except heading. + return r +} + +func (r *Renderer) hardBreak(w io.Writer, node *ast.Hardbreak) { + r.endline(w) + r.newline(w) +} + +func (r *Renderer) heading(w io.Writer, node *ast.Heading, entering bool) { + if entering { + return + } + + r.newline(w) + r.outs(w, "\n") +} + +func (r *Renderer) horizontalRule(w io.Writer, node *ast.HorizontalRule) { + r.newline(w) + r.outs(w, "******") + r.newline(w) +} + +func (r *Renderer) citation(w io.Writer, node *ast.Citation, entering bool) { + r.outs(w, "[") + for i, dest := range node.Destination { + if i > 0 { + r.outs(w, ", ") + } + r.out(w, dest) + + } + r.outs(w, "]") +} + +func (r *Renderer) paragraph(w io.Writer, para *ast.Paragraph, entering bool) { + if entering { + if buf, ok := w.(*bytes.Buffer); ok { + r.paraStart = buf.Len() + } + return + } + + buf, ok := w.(*bytes.Buffer) + end := 0 + if ok { + end = buf.Len() + } + // Reformat the entire buffer and rewrite to the writer. + b := buf.Bytes()[r.paraStart:end] + + var indented []byte + p := bytes.Split(b, []byte("\\\n")) + for i := range p { + if len(indented) > 0 { + p1 := r.wrapText(p[i], r.prefix.flatten(), []byte("")) + indented = append(indented, []byte("\\\n")...) + indented = append(indented, p1...) + continue + } + indented = r.wrapText(p[i], r.prefix.flatten(), []byte("")) + } + if len(indented) == 0 { + indented = make([]byte, r.prefix.peek()+3) + } + + buf.Truncate(r.paraStart) + + // Now an indented list didn't get is marker yet, override the initial spaces that have been + // created with the list marker, taking the current prefix into account. + listItem, inList := para.Parent.(*ast.ListItem) + firstPara := ast.GetPrevNode(para) // only the first para in the listItem needs a list marker + if inList && firstPara == nil { + plen := r.prefix.len() - r.prefix.peek() + switch x := listItem.ListFlags; { + case x&ast.ListTypeOrdered != 0: + list := listItem.Parent.(*ast.List) // this must be always true + pos := []byte(strconv.Itoa(list.Start)) + for i := 0; i < len(pos); i++ { + indented[plen+i] = pos[i] + } + indented[plen+len(pos)] = '.' + indented[plen+len(pos)+1] = ' ' + + list.Start++ + case x&ast.ListTypeTerm != 0: + indented = indented[plen+r.prefix.peek()-3:] // remove prefix. + indented[plen+0] = '*' + case x&ast.ListTypeDefinition != 0: + indented[plen+0] = ' ' + indented[plen+1] = ' ' + indented[plen+2] = ' ' + default: + if plen == 0 { + indented[plen+0] = 'o' + } + if plen == 3 { + indented[plen+0] = '+' + } + if plen == 6 { + indented[plen+0] = 'o' + } + if plen > 6 { + indented[plen+0] = '-' + } + indented[plen+1] = ' ' + indented[plen+2] = ' ' + } + } + + r.out(w, indented) + r.endline(w) + + // A paragraph can be rendered if we are in a subfigure, if so suppress some newlines. + if _, inCaption := para.Parent.(*ast.CaptionFigure); inCaption { + return + } + if !lastNode(para) { + r.newline(w) + } +} + +func (r *Renderer) list(w io.Writer, list *ast.List, entering bool) { + if entering { + if list.Start == 0 { + list.Start = 1 + } + l := listPrefixLength(list, list.Start) + if list.ListFlags&ast.ListTypeOrdered != 0 { + r.push(Space(l)) + } else { + r.push(Space(3)) + } + return + } + r.pop() +} + +func (r *Renderer) codeBlock(w io.Writer, codeBlock *ast.CodeBlock, entering bool) { + // Indent codeblock with 3 spaces + indented := r.indentText(codeBlock.Literal, append(r.prefix.flatten(), Space(3)...)) + r.out(w, indented) + r.outPrefix(w) + r.newline(w) + + r.newline(w) + return +} + +func (r *Renderer) table(w io.Writer, tab *ast.Table, entering bool) { + if entering { + r.colWidth, r.colAlign = r.tableColWidth(tab) + r.col = 0 + } else { + r.colWidth = []int{} + r.colAlign = []ast.CellAlignFlags{} + } +} + +func (r *Renderer) tableRow(w io.Writer, tableRow *ast.TableRow, entering bool) { + if entering { + r.outPrefix(w) + r.col = 0 + for i, width := range r.colWidth { + if _, isFooter := r.tableType.(*ast.TableFooter); isFooter { + r.out(w, bytes.Repeat([]byte("="), width+1)) + + if i == len(r.colWidth)-1 { + r.endline(w) + r.outPrefix(w) + } else { + r.outs(w, "|") + } + } + } + + return + } + + for i, width := range r.colWidth { + if _, isHeader := r.tableType.(*ast.TableHeader); isHeader { + if i == 0 { + r.outPrefix(w) + } + heading := bytes.Repeat([]byte("-"), width+1) + + switch r.colAlign[i] { + case ast.TableAlignmentLeft: + heading[0] = '-' + case ast.TableAlignmentRight: + heading[width] = '-' + } + r.out(w, heading) + if i == len(r.colWidth)-1 { + r.endline(w) + } else { + r.outs(w, "|") + } + } + } +} + +func (r *Renderer) tableCell(w io.Writer, tableCell *ast.TableCell, entering bool) { + // we get called when we're calculating the column width, only when r.tableColWidth is set we need to output. + if len(r.colWidth) == 0 { + return + } + if entering { + if buf, ok := w.(*bytes.Buffer); ok { + r.cellStart = buf.Len() + 1 + } + if r.col > 0 { + r.out(w, Space1) + } + return + } + + cur := 0 + if buf, ok := w.(*bytes.Buffer); ok { + cur = buf.Len() + } + size := r.colWidth[r.col] + fill := bytes.Repeat(Space1, size-(cur-r.cellStart)) + r.out(w, fill) + if r.col == len(r.colWidth)-1 { + r.endline(w) + } else { + r.outs(w, "|") + } + r.col++ +} + +func (r *Renderer) htmlSpan(w io.Writer, span *ast.HTMLSpan) {} + +func (r *Renderer) crossReference(w io.Writer, cr *ast.CrossReference, entering bool) { + if entering { + r.outs(w, "(#") + r.out(w, cr.Destination) + return + } + r.outs(w, ")") +} + +func (r *Renderer) index(w io.Writer, index *ast.Index, entering bool) { + if !entering { + return + } + + r.outs(w, "(!") + if index.Primary { + r.outs(w, "!") + } + r.out(w, index.Item) + + if len(index.Subitem) > 0 { + r.outs(w, ", ") + r.out(w, index.Subitem) + } + r.outs(w, ")") +} + +func (r *Renderer) link(w io.Writer, link *ast.Link, entering bool) { + if !entering { + return + } + // clear link so we don't render any children. + defer func() { *link = ast.Link{} }() + + // footnote + if link.NoteID > 0 { + ast.RemoveFromTree(link.Footnote) + if len(link.DeferredID) > 0 { + + r.outs(w, "[^") + r.out(w, link.DeferredID) + r.outs(w, "]") + + if _, ok := r.deferredFootID[string(link.DeferredID)]; ok { + return + } + + r.deferredFootBuf.Write(Space(3)) + r.deferredFootBuf.Write([]byte("[^")) + r.deferredFootBuf.Write(link.DeferredID) + r.deferredFootBuf.Write([]byte("]: ")) + r.deferredFootBuf.Write(link.Title) + + r.deferredFootID[string(link.DeferredID)] = struct{}{} + + return + } + r.outs(w, "^[") + r.out(w, link.Title) + r.outs(w, "]") + return + } + + for _, child := range link.GetChildren() { + ast.WalkFunc(child, func(node ast.Node, entering bool) ast.WalkStatus { + if text, ok := node.(*ast.Text); ok { + if bytes.Compare(text.Literal, link.Destination) == 0 { + return ast.GoToNext + } + } + return r.RenderNode(w, node, entering) + }) + } + + if len(link.DeferredID) == 0 { + r.outs(w, " <") + r.out(w, link.Destination) + r.outs(w, ">") + link.Destination = []byte{} + + if len(link.Title) > 0 { + r.outs(w, ` "`) + r.out(w, link.Title) + r.outs(w, `"`) + } + return + } + + r.outs(w, "[") + r.out(w, link.DeferredID) + r.outs(w, "]") + + if _, ok := r.deferredLinkID[string(link.DeferredID)]; ok { + return + } + + r.out(r.deferredLinkBuf, Space(3)) + r.outs(r.deferredLinkBuf, "[") + r.out(r.deferredLinkBuf, link.DeferredID) + r.outs(r.deferredLinkBuf, "]: ") + r.out(r.deferredLinkBuf, link.Destination) + if len(link.Title) > 0 { + r.outs(r.deferredLinkBuf, ` "`) + r.out(r.deferredLinkBuf, link.Title) + r.outs(r.deferredLinkBuf, `"`) + } + + r.deferredLinkID[string(link.DeferredID)] = struct{}{} +} + +func (r *Renderer) image(w io.Writer, node *ast.Image, entering bool) { + if !entering { + return + } + // clear image so we don't render any children. + defer func() { *node = ast.Image{} }() + + r.outs(w, "![") + for _, child := range node.GetChildren() { + ast.WalkFunc(child, func(node ast.Node, entering bool) ast.WalkStatus { + return r.RenderNode(w, node, entering) + }) + } + r.outs(w, "]") + + r.outs(w, "(") + r.out(w, node.Destination) + if len(node.Title) > 0 { + r.outs(w, ` "`) + r.out(w, node.Title) + r.outs(w, `"`) + } + r.outs(w, ")") +} + +func (r *Renderer) mathBlock(w io.Writer, mathBlock *ast.MathBlock, entering bool) { + if !entering { + return + } + r.outPrefix(w) + r.outs(w, "$$") + + math := r.indentText(mathBlock.Literal, r.prefix.flatten()) + r.out(w, math) + + r.outPrefix(w) + r.outs(w, "$$\n") + + if !lastNode(mathBlock) { + r.newline(w) + } +} + +func (r *Renderer) captionFigure(w io.Writer, figure *ast.CaptionFigure, entering bool) { + // if one of our children is an image this is an subfigure. + isImage := false + ast.WalkFunc(figure, func(node ast.Node, entering bool) ast.WalkStatus { + _, isImage = node.(*ast.Image) + if isImage { + return ast.Terminate + } + return ast.GoToNext + + }) + if isImage && entering { + r.outs(w, "") + r.endline(w) + } + if !entering { + r.newline(w) + } +} + +func (r *Renderer) caption(w io.Writer, caption *ast.Caption, entering bool) { + if !entering { + r.endline(w) + r.newline(w) + return + } + + r.outPrefix(w) + switch ast.GetPrevNode(caption).(type) { + case *ast.BlockQuote: + r.outs(w, "Quote: ") + return + case *ast.Table: + r.outs(w, "Table: ") + return + case *ast.CodeBlock: + r.outs(w, "Figure: ") + return + } + // If here, we're dealing with a subfigure captionFigure. + r.outs(w, "") + r.endline(w) + r.outs(w, "") +} + +func (r *Renderer) blockQuote(w io.Writer, block *ast.BlockQuote, entering bool) { + if entering { + r.push(Quote) + return + } + r.pop() + r.newline(w) +} + +func (r *Renderer) aside(w io.Writer, block *ast.Aside, entering bool) { + if entering { + r.push(Aside) + return + } + r.pop() + if !lastNode(block) { + r.newline(w) + } +} + +// RenderNode renders a markdown node to markdown. +func (r *Renderer) RenderNode(w io.Writer, node ast.Node, entering bool) ast.WalkStatus { + if r.opts.RenderNodeHook != nil { + status, didHandle := r.opts.RenderNodeHook(w, node, entering) + if didHandle { + return status + } + } + + switch node := node.(type) { + case *ast.Document: + // do nothing + case *mast.Title: + r.out(w, node.Content) + r.outs(w, "\n") + r.newline(w) + case *mast.Bibliography: + // discard + case *mast.BibliographyItem: + // discard + case *mast.DocumentIndex, *mast.IndexLetter, *mast.IndexItem, *mast.IndexSubItem, *mast.IndexLink: + // discard + case *mast.ReferenceBlock: + // discard + case *ast.Footnotes: + // do nothing, we're not outputing a footnote list + case *ast.Text: + r.text(w, node, entering) + case *ast.Softbreak: + case *ast.Hardbreak: + r.hardBreak(w, node) + case *ast.Callout: + r.callout(w, node, entering) + case *ast.Emph: + r.out(w, node.Literal) + case *ast.Strong: + r.out(w, node.Literal) + case *ast.Del: + r.out(w, node.Literal) + case *ast.Citation: + r.citation(w, node, entering) + case *ast.DocumentMatter: + // don't output + case *ast.Heading: + r.heading(w, node, entering) + case *ast.HorizontalRule: + if entering { + r.newline(w) + r.outPrefix(w) + r.outs(w, "********\n") + r.newline(w) + } + case *ast.Paragraph: + r.paragraph(w, node, entering) + case *ast.HTMLSpan: + r.out(w, node.Literal) + case *ast.HTMLBlock: + r.out(w, node.Literal) + r.endline(w) + r.newline(w) + case *ast.List: + r.list(w, node, entering) + if !entering { + r.newline(w) + } + case *ast.ListItem: + case *ast.CodeBlock: + r.codeBlock(w, node, entering) + case *ast.Caption: + r.caption(w, node, entering) + case *ast.CaptionFigure: + r.captionFigure(w, node, entering) + case *ast.Table: + r.table(w, node, entering) + case *ast.TableCell: + r.tableCell(w, node, entering) + case *ast.TableHeader: + r.tableType = node + case *ast.TableBody: + r.tableType = node + case *ast.TableFooter: + r.tableType = node + case *ast.TableRow: + r.tableRow(w, node, entering) + case *ast.BlockQuote: + r.blockQuote(w, node, entering) + case *ast.Aside: + r.aside(w, node, entering) + case *ast.CrossReference: + r.crossReference(w, node, entering) + case *ast.Index: + r.index(w, node, entering) + case *ast.Link: + r.link(w, node, entering) + case *ast.Math: + r.outOneOf(w, true, "$", "$") + if entering { + r.out(w, node.Literal) + } + r.outOneOf(w, false, "$", "$") + case *ast.Image: + r.image(w, node, entering) + case *ast.Code: + r.outs(w, "`") + r.out(w, node.Literal) + r.outs(w, "`") + case *ast.MathBlock: + r.mathBlock(w, node, entering) + case *ast.Subscript: + r.outOneOf(w, true, "~", "~") + if entering { + r.out(w, node.Literal) + } + r.outOneOf(w, false, "~", "~") + case *ast.Superscript: + r.outOneOf(w, true, "^", "^") + if entering { + r.out(w, node.Literal) + } + r.outOneOf(w, false, "^", "^") + default: + panic(fmt.Sprintf("Unknown node %T", node)) + } + return ast.GoToNext +} + +func (r *Renderer) callout(w io.Writer, node *ast.Callout, entering bool) { + if !entering { + return + } + r.outs(w, "<<") + r.out(w, node.ID) + r.outs(w, ">>") +} + +func (r *Renderer) text(w io.Writer, node *ast.Text, entering bool) { + if !entering { + return + } + _, isTableCell := node.Parent.(*ast.TableCell) + if isTableCell { + allSpace := true + for i := range node.Literal { + if !isSpace(node.Literal[i]) { + allSpace = false + break + } + } + if allSpace { + return + } + } + + r.out(w, node.Literal) +} + +func (r *Renderer) RenderHeader(_ io.Writer, _ ast.Node) {} +func (r *Renderer) writeDocumentHeader(_ io.Writer) {} + +func (r *Renderer) RenderFooter(w io.Writer, _ ast.Node) { + if r.deferredFootBuf.Len() > 0 { + r.outs(w, "\n") + io.Copy(w, r.deferredFootBuf) + } + if r.deferredLinkBuf.Len() > 0 { + r.outs(w, "\n") + io.Copy(w, r.deferredLinkBuf) + } + + buf, ok := w.(*bytes.Buffer) + if !ok { + return + } + + trimmed := &bytes.Buffer{} + + scanner := bufio.NewScanner(buf) + for scanner.Scan() { + trimmed.Write(bytes.TrimRight(scanner.Bytes(), " ")) + trimmed.WriteString("\n") + } + if err := scanner.Err(); err != nil { + return + } + + buf.Truncate(0) + data := trimmed.Bytes() + ld := len(data) + if ld > 2 && data[ld-1] == '\n' && data[ld-2] == '\n' { + ld-- + } + buf.Write(data[:ld]) +} + +var ( + Space1 = Space(1) + Aside = []byte("| ") + Quote = []byte("| ") +) diff --git a/render/text/table.go b/render/text/table.go new file mode 100644 index 0000000..5b174e8 --- /dev/null +++ b/render/text/table.go @@ -0,0 +1,44 @@ +package text + +import ( + "bytes" + + "github.com/gomarkdown/markdown/ast" +) + +func (r *Renderer) tableColWidth(tab *ast.Table) ([]int, []ast.CellAlignFlags) { + cells := 0 + ast.WalkFunc(tab, func(node ast.Node, entering bool) ast.WalkStatus { + switch node := node.(type) { + case *ast.TableRow: + cells = len(node.GetChildren()) + break + } + return ast.GoToNext + }) + + width := make([]int, cells) + align := make([]ast.CellAlignFlags, cells) + + ast.WalkFunc(tab, func(node ast.Node, entering bool) ast.WalkStatus { + switch node := node.(type) { + case *ast.TableRow: + for col, cell := range node.GetChildren() { + + align[col] = cell.(*ast.TableCell).Align + + buf := &bytes.Buffer{} + ast.WalkFunc(cell, func(node1 ast.Node, entering bool) ast.WalkStatus { + r.RenderNode(buf, node1, entering) + return ast.GoToNext + }) + if l := buf.Len(); l > width[col] { + width[col] = l + 1 // space in beginning or end + + } + } + } + return ast.GoToNext + }) + return width, align +}