diff options
Diffstat (limited to 'cnmfmt')
-rw-r--r-- | cnmfmt/cnmfmt.go | 525 | ||||
-rw-r--r-- | cnmfmt/cnmfmt_test.go | 457 |
2 files changed, 982 insertions, 0 deletions
diff --git a/cnmfmt/cnmfmt.go b/cnmfmt/cnmfmt.go new file mode 100644 index 0000000..cb8dc64 --- /dev/null +++ b/cnmfmt/cnmfmt.go @@ -0,0 +1,525 @@ +// Package cnmfmt provides parsing and composition for CNMfmt formatting. +package cnmfmt // import "contnet.org/lib/cnm-go/cnmfmt" + +import ( + "bytes" + "io" + "strings" + + "contnet.org/lib/cnm-go" +) + +func init() { + cnm.RegisterTextContentParser("fmt", parseTextFmt) +} + +// Text represents a paragraph of CNMfmt text. +type Text struct { + // Spans are spans of formatted text. + Spans []Span +} + +// ParseParagraph parses a single CNMfmt text paragraph s. +func ParseParagraph(s string) Text { + s = cnm.CollapseWhitespace(s) + + t := Text{} + var buf bytes.Buffer + format := Format{} + last := rune(-1) + url := false + + for _, r := range s { + if url && format.Link == "" { // need URL for link + if handleURL(r, &last, &format, &buf) { + continue + } + } + + switch r { + case '*', '/', '_', '`', '@': + handleTag(r, &last, &t, &format, &buf, &url) + + case '\\': + if last == '\\' { + buf.WriteString("\\\\") + last = -1 + } else { + if last >= 0 { + buf.WriteRune(last) + } + last = '\\' + } + + default: + if last >= 0 { + buf.WriteRune(last) + } + buf.WriteRune(r) + last = -1 + } + } + + if url && format.Link == "" { + if last >= 0 { + buf.WriteRune(last) + } + format.Link = Unescape(buf.String()) + buf.Reset() + } else if last >= 0 { + buf.WriteRune(last) + } + last = -1 + handleTag(-1, &last, &t, &format, &buf, &url) + + t.trimUnescape() + + return t +} + +func (t *Text) trimUnescape() { + var spans []Span + + for _, span := range t.Spans { + if span.Text != "" || span.Format.Link != "" { + spans = append(spans, span) + } + } + t.Spans, spans = spans, nil + + for i := len(t.Spans) - 1; i >= 0; i-- { + span := t.Spans[i] + if span.Text != "" || span.Format.Link != "" { + spans = append(spans, span) + } + } + for i := 0; i < len(spans)/2; i++ { + spans[i], spans[len(spans)-1-i] = spans[len(spans)-1-i], spans[i] + } + t.Spans = spans + + for i := range t.Spans { + t.Spans[i].Text = Unescape(t.Spans[i].Text) + } +} + +func (t *Text) appendSpan(format Format, txt string) { + if txt != "" || format.Link != "" { + t.Spans = append(t.Spans, Span{format, txt}) + } +} + +func handleURL(r rune, last *rune, format *Format, buf *bytes.Buffer) bool { + if r == '@' && *last == '@' { // end without text + format.Link = Unescape(buf.String()) + buf.Reset() + return false + } else if *last == '\\' { + buf.WriteByte('\\') + buf.WriteRune(r) + *last = -1 + } else if r == '\\' || r == '@' { + *last = r + } else if r != ' ' { // url + buf.WriteRune(r) + } else if buf.Len() > 0 { // space, then text + format.Link = Unescape(buf.String()) + buf.Reset() + } // else: prefix space + return true +} + +func handleTag(r rune, last *rune, txt *Text, format *Format, buf *bytes.Buffer, url *bool) { + if *last == '\\' { + buf.WriteRune(r) + *last = -1 + } else if *last == r { + txt.appendSpan(*format, buf.String()) + buf.Reset() + switch r { + case '*': + format.Bold = !format.Bold + case '/': + format.Italic = !format.Italic + case '_': + format.Underline = !format.Underline + case '`': + format.Monospace = !format.Monospace + case '@': + format.Link = "" + *url = !*url + } + *last = -1 + } else { + switch *last { + case '*', '/', '_', '`', '@': + buf.WriteRune(*last) + } + *last = r + } +} + +// WriteIndent writes the formatted text indented by n tabs. +func (t Text) WriteIndent(w io.Writer, n int) error { + var state [5]byte // bold, italic, underline, monospace, link + si := 0 + format := Format{} + spans := EscapeSpans(t.Spans) + var line []string + for _, span := range spans { + order := tagOrder(state[:si], format, span.Format) + for _, f := range order { + switch f { + case '*': + format.Bold = !format.Bold + line = append(line, "**") + case '/': + format.Italic = !format.Italic + line = append(line, "//") + case '_': + format.Underline = !format.Underline + line = append(line, "__") + case '`': + format.Monospace = !format.Monospace + line = append(line, "``") + case '@': + if format.Link != "" { + line = append(line, "@@") + } + if span.Format.Link != "" { + pad := "" + if span.Text != "" { + pad = " " + } + line = append(line, "@@", cnm.Escape(span.Format.Link), pad) + } + } + } + line = append(line, span.Text) + si = cleanupTags(state[:], order, span.Format) + format = span.Format + } + return writeIndent(w, strings.Join(line, ""), n) +} + +func tagOrder(state []byte, old, new Format) []byte { + ldiff := "" + if old.Link != new.Link { + ldiff = "1" + } + diff := Format{ + Bold: old.Bold != new.Bold, + Italic: old.Italic != new.Italic, + Underline: old.Underline != new.Underline, + Monospace: old.Monospace != new.Monospace, + Link: ldiff, + } + + var order [5]byte + oi := 0 + for i := len(state) - 1; i >= 0; i-- { + switch state[i] { + case '*': + if diff.Bold { + order[oi] = '*' + oi++ + diff.Bold = false + } + case '/': + if diff.Italic { + order[oi] = '/' + oi++ + diff.Italic = false + } + case '_': + if diff.Underline { + order[oi] = '_' + oi++ + diff.Underline = false + } + case '`': + if diff.Monospace { + order[oi] = '`' + oi++ + diff.Monospace = false + } + case '@': + if diff.Link != "" { + order[oi] = '@' + oi++ + diff.Link = "" + } + } + } + + if diff.Bold { + order[oi] = '*' + oi++ + } + if diff.Italic { + order[oi] = '/' + oi++ + } + if diff.Underline { + order[oi] = '_' + oi++ + } + if diff.Monospace { + order[oi] = '`' + oi++ + } + if diff.Link != "" { + order[oi] = '@' + oi++ + } + + return order[:oi] +} + +func cleanupTags(state []byte, order []byte, format Format) int { + var newState [10]byte + copy(newState[:5], state) + copy(newState[5:], order) + for i := range newState { + switch newState[i] { + case '*': + if !format.Bold { + newState[i] = 0 + } + case '/': + if !format.Italic { + newState[i] = 0 + } + case '_': + if !format.Underline { + newState[i] = 0 + } + case '`': + if !format.Monospace { + newState[i] = 0 + } + case '@': + if format.Link == "" { + newState[i] = 0 + } + } + } + si := 0 + for _, f := range newState { + if f > 0 { + state[si] = f + si++ + } + } + return si +} + +// Span represents a span of text with a format. +type Span struct { + // Format is the format of the text. + Format Format + + // Text is the text content of the span. + Text string +} + +// Format represents a state of CNMfmt formatting. +type Format struct { + // Bold text. + Bold bool + + // Italic text. + Italic bool + + // Underlined text. + Underline bool + + // Monospaced text. + Monospace bool + + // Hyperlink URL (if non-empty). + Link string +} + +// Escape escapes CNMfmt and CNM text special characters. +func Escape(s string) string { + return EscapeFmt(cnm.Escape(s)) +} + +// EscapeSpans escapes CNMfmt and CNM text within spans. +// +// This function will not needlessly escape spaces at the start or end of a +// span if the sibling span contains nonspaces. +func EscapeSpans(spans []Span) []Span { + // XXX: this is an ugly solution + esc := make([]Span, len(spans)) + for i := range spans { + start := false + end := false + span := spans[i] + if i+1 < len(spans) { + s := spans[i+1].Text + if len(s) > 0 && s[0] != ' ' { + span.Text = span.Text + "x" + end = true + } + } + if i > 0 { + s := spans[i-1].Text + if len(s) > 0 && s[len(s)-1] != ' ' { + span.Text = "x" + span.Text + start = true + } + } + span.Text = Escape(span.Text) + if start { + span.Text = span.Text[1:] + } + if end { + span.Text = span.Text[:len(span.Text)-1] + } + esc[i] = span + } + return esc +} + +var escapeReplacer = strings.NewReplacer( + `*`, `\*`, + `/`, `\/`, + `_`, `\_`, + "`", "\\`", + `@`, `\@`, +) + +// EscapeFmt escapes only CNMfmt format toggle characters. +func EscapeFmt(s string) string { + return escapeReplacer.Replace(s) +} + +// Unescape resolves CNM text and CNMfmt escape sequences in s. +func Unescape(s string) string { + return cnm.Unescape(UnescapeFmt(s)) +} + +var unescapeReplacer = strings.NewReplacer( + `\\`, `\\`, + `\*`, `*`, + `\/`, `/`, + `\_`, `_`, + "\\`", "`", + `\@`, `@`, +) + +// UnescapeFmt resolves only CNMfmt escape sequences in s. +func UnescapeFmt(s string) string { + return unescapeReplacer.Replace(s) +} + +// TextFmtContents represents CNM `text fmt` contents. +type TextFmtContents struct { + Paragraphs []Text +} + +// NewTextFmtBlock creates a new `text fmt` block containing provided CNMfmt +// paragraphs. +func NewTextFmtBlock(paragraphs []Text) *cnm.TextBlock { + return cnm.NewTextBlock("fmt", TextFmtContents{paragraphs}) +} + +// WriteIndent writes the formatted text contents indented by n tabs. +func (tf TextFmtContents) WriteIndent(w io.Writer, n int) error { + for i, p := range tf.Paragraphs { + if i != 0 { + if err := writeIndent(w, "", 0); err != nil { + return err + } + } + if err := p.WriteIndent(w, n); err != nil { + return err + } + } + return nil +} + +// Parse parses paragraphs of CNMfmt text. +func Parse(paragraphs string) []Text { + var txt []Text + var paragraph []string + + for _, line := range strings.Split(paragraphs, "\n") { + end := false + if line != "" { + if strings.Trim(line, "\n\r\t\f ") == "" { + end = true + } else { + paragraph = append(paragraph, line) + } + } else if len(paragraph) > 0 { + end = true + } + if end { + txt = append(txt, ParseParagraph(strings.Join(paragraph, " "))) + paragraph = nil + } + } + if len(paragraph) > 0 { + txt = append(txt, ParseParagraph(strings.Join(paragraph, " "))) + } + + return txt +} + +func writeIndent(w io.Writer, s string, depth int) error { + const tabs = "\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t" + + if s == "" { + _, err := w.Write([]byte{'\n'}) + return err + } + if depth == 0 { + _, err := w.Write([]byte(s + "\n")) + return err + } + + var ind string + if depth <= len(tabs) { + ind = tabs[:depth] + } else { + ind = strings.Repeat("\t", depth) + } + _, err := w.Write([]byte(ind + s + "\n")) + return err + +} + +func parseTextFmt(p *cnm.Parser, block *cnm.TokenBlock) (cnm.TextContents, error) { + txt := TextFmtContents{} + var paragraph []string + var err error + for err == nil { + if !p.Empty() && p.Indent() <= block.Indent() { + break + } + + token := p.RawText() + end := false + if text, ok := token.(*cnm.TokenRawText); ok { + if strings.Trim(text.Text, "\n\r\t\f ") == "" { + end = true + } else { + paragraph = append(paragraph, text.Text) + } + } else if _, ok := token.(*cnm.TokenEmptyLine); ok && len(paragraph) > 0 { + end = true + } + if end { + txt.Paragraphs = append(txt.Paragraphs, ParseParagraph(strings.Join(paragraph, " "))) + paragraph = nil + } + err = p.Next() + } + if len(paragraph) > 0 { + txt.Paragraphs = append(txt.Paragraphs, ParseParagraph(strings.Join(paragraph, " "))) + } + return txt, err +} diff --git a/cnmfmt/cnmfmt_test.go b/cnmfmt/cnmfmt_test.go new file mode 100644 index 0000000..89a40a9 --- /dev/null +++ b/cnmfmt/cnmfmt_test.go @@ -0,0 +1,457 @@ +package cnmfmt + +import ( + "bytes" + "io" + "strings" + "testing" + + "contnet.org/lib/cnm-go" +) + +var parseTests = map[string]Text{ + "\\nfoo\nbar\\": Text{[]Span{ + Span{Format{}, "\nfoo bar\\"}, + }}, + "**foo": Text{[]Span{ + Span{Format{Bold: true}, "foo"}, + }}, + "//foo": Text{[]Span{ + Span{Format{Italic: true}, "foo"}, + }}, + "__foo": Text{[]Span{ + Span{Format{Underline: true}, "foo"}, + }}, + "``foo": Text{[]Span{ + Span{Format{Monospace: true}, "foo"}, + }}, + "foo*bar": Text{[]Span{ + Span{Format{}, "foo*bar"}, + }}, + "foo*": Text{[]Span{ + Span{Format{}, "foo*"}, + }}, + "foo**": Text{[]Span{ + Span{Format{}, "foo"}, + }}, + "foo***": Text{[]Span{ + Span{Format{}, "foo"}, + Span{Format{Bold: true}, "*"}, + }}, + "foo****": Text{[]Span{ + Span{Format{}, "foo"}, + }}, + "*foo": Text{[]Span{ + Span{Format{}, "*foo"}, + }}, + "****foo": Text{[]Span{ + Span{Format{}, "foo"}, + }}, + "******foo": Text{[]Span{ + Span{Format{Bold: true}, "foo"}, + }}, + "foo ** bar": Text{[]Span{ + Span{Format{}, "foo "}, + Span{Format{Bold: true}, " bar"}, + }}, + "foo** bar": Text{[]Span{ + Span{Format{}, "foo"}, + Span{Format{Bold: true}, " bar"}, + }}, + "foo **bar": Text{[]Span{ + Span{Format{}, "foo "}, + Span{Format{Bold: true}, "bar"}, + }}, + "foo ** bar ** baz": Text{[]Span{ + Span{Format{}, "foo "}, + Span{Format{Bold: true}, " bar "}, + Span{Format{}, " baz"}, + }}, + "foo ** bar** baz": Text{[]Span{ + Span{Format{}, "foo "}, + Span{Format{Bold: true}, " bar"}, + Span{Format{}, " baz"}, + }}, + "**__**foo": Text{[]Span{ + Span{Format{Underline: true}, "foo"}, + }}, + "***": Text{[]Span{ + Span{Format{Bold: true}, "*"}, + }}, + "*\\**": Text{[]Span{ + Span{Format{}, "***"}, + }}, + "\\*": Text{[]Span{ + Span{Format{}, "*"}, + }}, + "\\*\\*": Text{[]Span{ + Span{Format{}, "**"}, + }}, + "\\**": Text{[]Span{ + Span{Format{}, "**"}, + }}, + "*\\*": Text{[]Span{ + Span{Format{}, "**"}, + }}, + "\\": Text{[]Span{ + Span{Format{}, "\\"}, + }}, + "\\\\": Text{[]Span{ + Span{Format{}, "\\"}, + }}, + " ** // `` ": Text{[]Span{ + Span{Format{Bold: true}, " "}, + Span{Format{Bold: true, Italic: true}, " "}, + }}, + "**": Text{[]Span{}}, + "**``__//foo": Text{[]Span{ + Span{Format{Bold: true, Monospace: true, Underline: true, Italic: true}, "foo"}, + }}, + "**foo//bar**baz": Text{[]Span{ + Span{Format{Bold: true}, "foo"}, + Span{Format{Bold: true, Italic: true}, "bar"}, + Span{Format{Italic: true}, "baz"}, + }}, + "@@foo": Text{[]Span{ + Span{Format{Link: "foo"}, ""}, + }}, + "@@foo@@": Text{[]Span{ + Span{Format{Link: "foo"}, ""}, + }}, + "@@foo bar@@": Text{[]Span{ + Span{Format{Link: "foo"}, "bar"}, + }}, + "@@ foo": Text{[]Span{ + Span{Format{Link: "foo"}, ""}, + }}, + "@@foo ": Text{[]Span{ + Span{Format{Link: "foo"}, ""}, + }}, + "@@foo\\": Text{[]Span{ + Span{Format{Link: "foo\\"}, ""}, + }}, + "@@foo \\": Text{[]Span{ + Span{Format{Link: "foo"}, "\\"}, + }}, + "@@foo \\\\": Text{[]Span{ + Span{Format{Link: "foo"}, "\\"}, + }}, + "@@foo@": Text{[]Span{ + Span{Format{Link: "foo@"}, ""}, + }}, + "@@foo\\@@": Text{[]Span{ + Span{Format{Link: "foo@@"}, ""}, + }}, + "@@f\\\\o\\o\\n @": Text{[]Span{ + Span{Format{Link: "f\\o\\o\n"}, "@"}, + }}, + "@@http://example.com foo **bar @@baz**": Text{[]Span{ + Span{Format{Link: "http://example.com"}, "foo "}, + Span{Format{Bold: true, Link: "http://example.com"}, "bar "}, + Span{Format{Bold: true}, "baz"}, + }}, + "//@@http://example.com foo //bar @@": Text{[]Span{ + Span{Format{Italic: true, Link: "http://example.com"}, "foo "}, + Span{Format{Link: "http://example.com"}, "bar "}, + }}, + "__\\ asd \\ zxc\\ ": Text{[]Span{ + Span{Format{Underline: true, Monospace: false}, " asd zxc "}, + }}, + "@@/ test/@@": Text{[]Span{ + Span{Format{Link: "/"}, "test/"}, + }}, + "@@/ /test@@": Text{[]Span{ + Span{Format{Link: "/"}, "/test"}, + }}, + "/": Text{[]Span{ + Span{Format{}, "/"}, + }}, + "test/**": Text{[]Span{ + Span{Format{}, "test/"}, + }}, + "//test/": Text{[]Span{ + Span{Format{Italic: true}, "test/"}, + }}, + "/**test": Text{[]Span{ + Span{Format{}, "/"}, + Span{Format{Bold: true}, "test"}, + }}, +} + +func TestParseParagraph(t *testing.T) { + for k, v := range parseTests { + t.Run(k, func(t *testing.T) { + txt := ParseParagraph(k) + if !textEqual(txt, v) { + t.Errorf("ParseParagraph(%q):\nexpected: %#v\n got: %#v", k, v, txt) + } + }) + } +} + +func TestParse(t *testing.T) { + for k, v := range parseTests { + t.Run(k, func(t *testing.T) { + txts := Parse(k) + if len(txts) != 1 || !textEqual(txts[0], v) { + t.Errorf("Parse(%q):\nexpected: %#v\n got: %#v", k, []Text{v}, txts) + } + }) + } +} + +func textEqual(a, b Text) bool { + if len(a.Spans) != len(b.Spans) { + return false + } + for i := range a.Spans { + if a.Spans[i] != b.Spans[i] { + return false + } + } + return true +} + +var escapeTests = map[string]string{ + "\n\r\t\v\x00": "\\n\\r\\t\v\\x00", + "@@!!##__//__``**": "\\@\\@!!##\\_\\_\\/\\/\\_\\_\\`\\`\\*\\*", + `foo\@\@bar`: `foo\\\@\\\@bar`, +} + +func TestEscape(t *testing.T) { + for k, v := range escapeTests { + t.Run(k, func(t *testing.T) { + if e := Escape(k); e != v { + t.Errorf("Escape(%q): expected %q, got %q", k, v, e) + } + }) + } +} + +var parseTextTests = map[string]TextFmtContents{ + "foo ** bar\nbaz\n\n\nquux ** ": TextFmtContents{[]Text{ + Text{[]Span{ + Span{Format{}, "foo "}, + Span{Format{Bold: true}, " bar baz"}, + }}, + Text{[]Span{ + Span{Format{}, "quux "}, + }}, + }}, + + "\n": TextFmtContents{}, + + "foo": TextFmtContents{[]Text{ + Text{[]Span{ + Span{Format{}, "foo"}, + }}, + }}, + + "\n\n": TextFmtContents{}, + + "foo\n\t\t\t\t\nbar": TextFmtContents{[]Text{ + Text{[]Span{Span{Format{}, "foo"}}}, + Text{[]Span{Span{Format{}, "bar"}}}, + }}, + + "foo\n\t\t \f\r\t\nbar": TextFmtContents{[]Text{ + Text{[]Span{Span{Format{}, "foo"}}}, + Text{[]Span{Span{Format{}, "bar"}}}, + }}, + + `foo**bar\*\*baz\*\*quux**qweasd`: TextFmtContents{[]Text{Text{[]Span{ + Span{Format{}, "foo"}, + Span{Format{Bold: true}, "bar**baz**quux"}, + Span{Format{}, "qweasd"}, + }}}}, +} + +func TestParseTextFmt(t *testing.T) { + for k, v := range parseTextTests { + t.Run(k, func(t *testing.T) { + parser := cnm.NewParser(strings.NewReader(k)) + err := parser.Next() + if err != nil && err != io.EOF { + t.Fatalf("error parsing %q: %v", k, err) + } + content, err := parseTextFmt(parser, cnm.TopLevel) + if err != nil && err != io.EOF { + t.Fatalf("error parsing %q: %v", k, err) + } + tf, ok := content.(TextFmtContents) + if !ok { + t.Fatalf("%q: expected type %T, got %T", k, v, content) + } + if !paragraphsEqual(v.Paragraphs, tf.Paragraphs) { + t.Fatalf("%q:\nexpected: %#v\n got: %#v", k, v, tf) + } + txts := Parse(k) + if !paragraphsEqual(txts, v.Paragraphs) { + t.Fatalf("%q:\nexpected: %#v\n got: %#v", k, v.Paragraphs, txts) + } + }) + } +} + +func paragraphsEqual(a, b []Text) bool { + if len(a) != len(b) { + return false + } + for i := range a { + if !textEqual(a[i], b[i]) { + return false + } + } + return true +} + +var writeTests = map[string]TextFmtContents{ + "": TextFmtContents{}, + + "foo\n": TextFmtContents{[]Text{ + Text{[]Span{ + Span{Format{}, "foo"}, + }}, + }}, + + "**foo\n": TextFmtContents{[]Text{ + Text{[]Span{ + Span{Format{Bold: true}, "foo"}, + }}, + }}, + + "foo **bar baz\n\nquux\n": TextFmtContents{[]Text{ + Text{[]Span{ + Span{Format{}, "foo "}, + Span{Format{Bold: true}, "bar baz"}, + }}, + Text{[]Span{ + Span{Format{}, "quux"}, + }}, + }}, + + "foo**bar``baz**quux\n\n" + + "\\ __qwe\\ __//\\ asd \\ //``zxc``**\\ \n\n" + + "//@@http://example.com exa//mple@@ @@href text@@// test\n": TextFmtContents{[]Text{ + Text{[]Span{ + Span{Format{}, "foo"}, + Span{Format{Bold: true}, "bar"}, + Span{Format{Bold: true, Monospace: true}, "baz"}, + Span{Format{Monospace: true}, "quux"}, + }}, + Text{[]Span{ + Span{Format{}, " "}, + Span{Format{Underline: true}, "qwe "}, + Span{Format{Italic: true}, " asd "}, + Span{Format{Monospace: true}, "zxc"}, + Span{Format{Bold: true}, " "}, + }}, + Text{[]Span{ + Span{Format{Italic: true, Link: "http://example.com"}, "exa"}, + Span{Format{Link: "http://example.com"}, "mple"}, + Span{Format{}, " "}, + Span{Format{Link: "href"}, "text"}, + Span{Format{Italic: true}, " test"}, + }}, + }}, + + "foo**bar\\*\\*baz\\*\\*quux**qweasd\n": TextFmtContents{[]Text{Text{[]Span{ + Span{Format{}, "foo"}, + Span{Format{Bold: true}, "bar**baz**quux"}, + Span{Format{}, "qweasd"}, + }}}}, +} + +func TestWriteTextFmt(t *testing.T) { + for k, v := range writeTests { + t.Run(k, func(t *testing.T) { + var buf bytes.Buffer + err := v.WriteIndent(&buf, 0) + if err != nil { + t.Fatalf("WriteIndent error: %v", err) + } + w := buf.String() + t.Log("expected:\n" + k) + t.Log(" got:\n" + w) + if k != w { + t.Fatalf("WriteIndent: output did not match expected document:\nexpected: %q\n got: %q", k, w) + } + }) + } +} + +func TestWriteParseTextFmt(t *testing.T) { + for k, v := range writeTests { + t.Run(k, func(t *testing.T) { + var buf bytes.Buffer + err := v.WriteIndent(&buf, 0) + if err != nil { + t.Fatalf("WriteIndent error: %v", err) + } + w := buf.String() + + if w == "" { + w = "\n" + } + parser := cnm.NewParser(strings.NewReader(w)) + err = parser.Next() + if err != nil && err != io.EOF { + t.Fatalf("error parsing %q: %v", w, err) + } + content, err := parseTextFmt(parser, cnm.TopLevel) + if err != nil && err != io.EOF { + t.Fatalf("error parsing %q: %v", w, err) + } + tf, ok := content.(TextFmtContents) + if !ok { + t.Fatalf("%q: expected type %T, got %T", w, v, content) + } + if !paragraphsEqual(v.Paragraphs, tf.Paragraphs) { + t.Fatalf("%q:\nexpected: %#v\n got: %#v", k, v, tf) + } + }) + } +} + +func TestParseWriteTextFmt(t *testing.T) { + for k, v := range writeTests { + t.Run(k, func(t *testing.T) { + s := k + if s == "" { + s = "\n" + } + parser := cnm.NewParser(strings.NewReader(s)) + err := parser.Next() + if err != nil && err != io.EOF { + t.Fatalf("error parsing %q: %v", k, err) + } + + content, err := parseTextFmt(parser, cnm.TopLevel) + if err != nil && err != io.EOF { + t.Fatalf("error parsing %q: %v", k, err) + } + tf, ok := content.(TextFmtContents) + if !ok { + t.Fatalf("%q: expected type %T, got %T", k, v, content) + } + if !paragraphsEqual(tf.Paragraphs, v.Paragraphs) { + t.Fatalf("%q: expected %#v, got %#v", k, v, tf) + } + + var buf bytes.Buffer + err = tf.WriteIndent(&buf, 0) + if err != nil { + t.Fatalf("WriteIndent error: %v", err) + } + + w := buf.String() + /*if w == "\n" { + k = "" + }*/ + + if k != w { + t.Fatalf("%q:\nexpected: %#v\n got: %#v", k, k, w) + } + }) + } +} |