1
0
This commit is contained in:
Arpad Ryszka 2025-11-02 22:15:31 +01:00
parent 7a1029d1e5
commit 82b8d4c082
8 changed files with 227 additions and 42 deletions

View File

@ -45,7 +45,7 @@ func escapeRoffEdit(additional ...string) func(rune, bool) ([]rune, bool) {
'\u00a0': []rune("\\~"), '\u00a0': []rune("\\~"),
} }
for i := 0; i > len(additional); i += 2 { for i := 0; i < len(additional); i += 2 {
r := []rune(additional[i]) r := []rune(additional[i])
if len(r) != 1 { if len(r) != 1 {
panic(errors.New(invalidAdditional)) panic(errors.New(invalidAdditional))

View File

@ -266,6 +266,12 @@ func renderHTMLFragment(out io.Writer, doc Document) error {
} }
for i, tag := range tags { for i, tag := range tags {
if i > 0 {
if _, err := fmt.Fprintln(out); err != nil {
return err
}
}
indent := html.Indentation{ indent := html.Indentation{
Indent: "\t", Indent: "\t",
PWidth: 120, PWidth: 120,

View File

@ -321,6 +321,7 @@ lines.</p>
}) })
t.Run("title", func(t *testing.T) { t.Run("title", func(t *testing.T) {
t.Run("basic", func(t *testing.T) {
var b bytes.Buffer var b bytes.Buffer
doc := textfmt.Doc(textfmt.Title(0, "This is a title")) doc := textfmt.Doc(textfmt.Title(0, "This is a title"))
if err := textfmt.HTMLFragment(&b, doc); err != nil { if err := textfmt.HTMLFragment(&b, doc); err != nil {
@ -332,6 +333,36 @@ lines.</p>
} }
}) })
t.Run("every level", func(t *testing.T) {
var b bytes.Buffer
doc := textfmt.Doc(
textfmt.Title(0, "H1"),
textfmt.Title(1, "H2"),
textfmt.Title(2, "H3"),
textfmt.Title(3, "H4"),
textfmt.Title(4, "H5"),
textfmt.Title(5, "H6"),
)
if err := textfmt.HTMLFragment(&b, doc); err != nil {
t.Fatal(err)
}
const expect = `
<h1>H1</h1>
<h2>H2</h2>
<h3>H3</h3>
<h4>H4</h4>
<h5>H5</h5>
<h6>H6</h6>
`
if "\n" + b.String() != expect {
t.Fatal("\n" + b.String())
}
})
})
t.Run("paragraph", func(t *testing.T) { t.Run("paragraph", func(t *testing.T) {
t.Run("unwrapped", func(t *testing.T) { t.Run("unwrapped", func(t *testing.T) {
var b bytes.Buffer var b bytes.Buffer
@ -673,6 +704,46 @@ lines.</p>
} }
}) })
t.Run("wrapped and different indents", func(t *testing.T) {
doc := textfmt.Doc(
textfmt.Wrap(
textfmt.Indent(
textfmt.List(textfmt.Item(textfmt.Text("foo bar baz"))),
4,
0,
),
24,
),
textfmt.Wrap(
textfmt.Indent(
textfmt.List(textfmt.Item(textfmt.Text("foo bar baz"))),
8,
0,
),
24,
),
)
var b bytes.Buffer
if err := textfmt.HTMLFragment(&b, doc); err != nil {
t.Fatal(err)
}
const expect = `
<ul>
<li>foo bar baz</li>
</ul>
<ul>
<li>foo bar baz
</li>
</ul>
`
if "\n"+b.String() != expect {
t.Fatal("\n" + b.String())
}
})
t.Run("long numbered definition list", func(t *testing.T) { t.Run("long numbered definition list", func(t *testing.T) {
doc := textfmt.Doc( doc := textfmt.Doc(
textfmt.NumberedDefinitionList( textfmt.NumberedDefinitionList(

1
lib.go
View File

@ -241,6 +241,7 @@ func Sequence(items ...SyntaxItem) SyntaxItem {
return SyntaxItem{sequence: items} return SyntaxItem{sequence: items}
} }
// top level on separate lines without delimiter
func Choice(items ...SyntaxItem) SyntaxItem { func Choice(items ...SyntaxItem) SyntaxItem {
return SyntaxItem{choice: items} return SyntaxItem{choice: items}
} }

View File

@ -54,10 +54,6 @@ func renderMDText(w io.Writer, text Txt) {
return return
} }
text.text = editString(text.text, singleLine())
text.text = editString(text.text, escapeMarkdown())
text.link = editString(text.link, singleLine())
text.link = editString(text.link, escapeMarkdown())
if text.bold { if text.bold {
write(w, "**") write(w, "**")
} }
@ -76,21 +72,30 @@ func renderMDText(w io.Writer, text Txt) {
} }
}() }()
if text.link != "" { if text.link == "" {
if text.text != "" { w, f := writeWith(w, singleLine(), escapeMarkdown())
write(w, text.text)
w, _ = f()
return
}
if text.text == "" {
w, f := writeWith(w, singleLine(), escapeMarkdown())
write(w, text.link)
w, _ = f()
return
}
write(w, "[") write(w, "[")
w, f := writeWith(w, singleLine(), escapeMarkdown())
write(w, text.text) write(w, text.text)
w, _ = f()
write(w, "](") write(w, "](")
w, f = writeWith(w, singleLine(), escapeMarkdown())
write(w, text.link) write(w, text.link)
w, _ = f()
write(w, ")") write(w, ")")
return w, _ = f()
}
write(w, text.link)
return
}
write(w, text.text)
} }
func renderMDTitle(w io.Writer, e Entry) { func renderMDTitle(w io.Writer, e Entry) {

View File

@ -394,6 +394,50 @@ textfmt supports the following entries:
} }
}) })
t.Run("link", func(t *testing.T) {
t.Run("normal", func(t *testing.T) {
doc := textfmt.Doc(
textfmt.Paragraph(
textfmt.Cat(
textfmt.Text("This is a "),
textfmt.Link("link", "https://foo.bar"),
textfmt.Text("alright."),
),
),
)
var b bytes.Buffer
if err := textfmt.Markdown(&b, doc); err != nil {
t.Fatal(err)
}
if b.String() != "This is a [link](https://foo.bar) alright.\n" {
t.Fatal(b.String())
}
})
t.Run("without label", func(t *testing.T) {
doc := textfmt.Doc(
textfmt.Paragraph(
textfmt.Cat(
textfmt.Text("This is a "),
textfmt.Link("", "https://foo.bar"),
textfmt.Text("alright."),
),
),
)
var b bytes.Buffer
if err := textfmt.Markdown(&b, doc); err != nil {
t.Fatal(err)
}
if b.String() != "This is a https://foo.bar alright.\n" {
t.Fatal(b.String())
}
})
})
t.Run("escape", func(t *testing.T) { t.Run("escape", func(t *testing.T) {
doc := textfmt.Doc( doc := textfmt.Doc(
textfmt.Paragraph( textfmt.Paragraph(
@ -428,6 +472,23 @@ textfmt supports the following entries:
} }
}) })
t.Run("escape non link", func(t *testing.T) {
doc := textfmt.Doc(
textfmt.Paragraph(
textfmt.Text("[looks-like-a-link] but it's not"),
),
)
var b bytes.Buffer
if err := textfmt.Markdown(&b, doc); err != nil {
t.Fatal(err)
}
if b.String() != "\\[looks-like-a-link\\] but it's not\n" {
t.Fatal(b.String())
}
})
t.Run("escape negative number on line start", func(t *testing.T) { t.Run("escape negative number on line start", func(t *testing.T) {
doc := textfmt.Doc( doc := textfmt.Doc(
textfmt.Paragraph( textfmt.Paragraph(

View File

@ -4,6 +4,7 @@ import (
"bytes" "bytes"
"code.squareroundforest.org/arpio/textfmt" "code.squareroundforest.org/arpio/textfmt"
"testing" "testing"
"time"
) )
func TestRoff(t *testing.T) { func TestRoff(t *testing.T) {
@ -230,8 +231,16 @@ textfmt supports the following entries:
}) })
t.Run("example man", func(t *testing.T) { t.Run("example man", func(t *testing.T) {
releaseDate := time.Date(2025, 11, 2, 15, 36, 18, 0, time.FixedZone("CET", 3600))
doc := textfmt.Doc( doc := textfmt.Doc(
textfmt.Title(0, "Example Text", textfmt.ManSection(1)), textfmt.Title(
0,
"Example Text",
textfmt.ManSection(1),
textfmt.ManCategory("User Command"),
textfmt.ReleaseDate(releaseDate),
textfmt.ReleaseVersion("v1"),
),
textfmt.Indent( textfmt.Indent(
textfmt.Paragraph(textfmt.Text("Below you can find some test text, with various text items.")), textfmt.Paragraph(textfmt.Text("Below you can find some test text, with various text items.")),
@ -315,7 +324,7 @@ textfmt supports the following entries:
t.Fatal(err) t.Fatal(err)
} }
const expect = `.TH "Example Text" 1 "" "" "" const expect = `.TH "Example Text" 1 "November 2025" "v1" "User Command"
.br .br
.sp 1v .sp 1v
.in 0 .in 0
@ -634,6 +643,7 @@ Some sample text... on multiple lines.
}) })
t.Run("title", func(t *testing.T) { t.Run("title", func(t *testing.T) {
t.Run("basic", func(t *testing.T) {
var b bytes.Buffer var b bytes.Buffer
doc := textfmt.Doc(textfmt.Title(0, "This is a title")) doc := textfmt.Doc(textfmt.Title(0, "This is a title"))
if err := textfmt.Runoff(&b, doc); err != nil { if err := textfmt.Runoff(&b, doc); err != nil {
@ -650,6 +660,23 @@ Some sample text... on multiple lines.
} }
}) })
t.Run("escaped man", func(t *testing.T) {
var b bytes.Buffer
doc := textfmt.Doc(textfmt.Title(0, "This is a title \"\\\"", textfmt.ManSection(1)))
if err := textfmt.Runoff(&b, doc); err != nil {
t.Fatal(err)
}
const expect = `
.TH "This is a title \(dq\\\(dq" 1 "" "" ""
`
if "\n" + b.String() != expect {
t.Fatal("\n" + b.String())
}
})
})
t.Run("paragraph", func(t *testing.T) { t.Run("paragraph", func(t *testing.T) {
t.Run("unwrapped", func(t *testing.T) { t.Run("unwrapped", func(t *testing.T) {
var b bytes.Buffer var b bytes.Buffer

View File

@ -316,6 +316,7 @@ Entry explanations:
}) })
t.Run("failing writer", func(t *testing.T) { t.Run("failing writer", func(t *testing.T) {
t.Run("once", func(t *testing.T) {
w := &failingWriter{failAfter: 15} w := &failingWriter{failAfter: 15}
doc := textfmt.Doc( doc := textfmt.Doc(
textfmt.Paragraph(textfmt.Text("Some sample text...\non multiple lines.")), textfmt.Paragraph(textfmt.Text("Some sample text...\non multiple lines.")),
@ -326,6 +327,19 @@ Entry explanations:
} }
}) })
t.Run("multple times", func(t *testing.T) {
w := &failingWriter{failAfter: 15}
doc := textfmt.Doc(
textfmt.Paragraph(textfmt.Text("Some sample text...\non multiple lines.")),
textfmt.Paragraph(textfmt.Text("Some more sample text...\non multiple lines.")),
)
if err := textfmt.Teletype(w, doc); err == nil {
t.Fatal("failed to fail")
}
})
})
t.Run("concatenate", func(t *testing.T) { t.Run("concatenate", func(t *testing.T) {
doc := textfmt.Doc( doc := textfmt.Doc(
textfmt.Paragraph( textfmt.Paragraph(