summaryrefslogtreecommitdiffstats
path: root/cnmfmt
diff options
context:
space:
mode:
Diffstat (limited to 'cnmfmt')
-rw-r--r--cnmfmt/cnmfmt.go525
-rw-r--r--cnmfmt/cnmfmt_test.go457
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)
+ }
+ })
+ }
+}