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("\\~"),
}
for i := 0; i > len(additional); i += 2 {
for i := 0; i < len(additional); i += 2 {
r := []rune(additional[i])
if len(r) != 1 {
panic(errors.New(invalidAdditional))

View File

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

View File

@ -321,15 +321,46 @@ lines.</p>
})
t.Run("title", func(t *testing.T) {
var b bytes.Buffer
doc := textfmt.Doc(textfmt.Title(0, "This is a title"))
if err := textfmt.HTMLFragment(&b, doc); err != nil {
t.Fatal(err)
}
t.Run("basic", func(t *testing.T) {
var b bytes.Buffer
doc := textfmt.Doc(textfmt.Title(0, "This is a title"))
if err := textfmt.HTMLFragment(&b, doc); err != nil {
t.Fatal(err)
}
if b.String() != "<h1>This is a title</h1>\n" {
t.Fatal(b.String())
}
if b.String() != "<h1>This is a title</h1>\n" {
t.Fatal(b.String())
}
})
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) {
@ -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) {
doc := textfmt.Doc(
textfmt.NumberedDefinitionList(

1
lib.go
View File

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

View File

@ -54,10 +54,6 @@ func renderMDText(w io.Writer, text Txt) {
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 {
write(w, "**")
}
@ -76,21 +72,30 @@ func renderMDText(w io.Writer, text Txt) {
}
}()
if text.link != "" {
if text.text != "" {
write(w, "[")
write(w, text.text)
write(w, "](")
write(w, text.link)
write(w, ")")
return
}
write(w, text.link)
if text.link == "" {
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, "[")
w, f := writeWith(w, singleLine(), escapeMarkdown())
write(w, text.text)
w, _ = f()
write(w, "](")
w, f = writeWith(w, singleLine(), escapeMarkdown())
write(w, text.link)
w, _ = f()
write(w, ")")
w, _ = f()
}
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) {
doc := textfmt.Doc(
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) {
doc := textfmt.Doc(
textfmt.Paragraph(

View File

@ -4,6 +4,7 @@ import (
"bytes"
"code.squareroundforest.org/arpio/textfmt"
"testing"
"time"
)
func TestRoff(t *testing.T) {
@ -230,8 +231,16 @@ textfmt supports the following entries:
})
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(
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.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)
}
const expect = `.TH "Example Text" 1 "" "" ""
const expect = `.TH "Example Text" 1 "November 2025" "v1" "User Command"
.br
.sp 1v
.in 0
@ -634,20 +643,38 @@ Some sample text... on multiple lines.
})
t.Run("title", func(t *testing.T) {
var b bytes.Buffer
doc := textfmt.Doc(textfmt.Title(0, "This is a title"))
if err := textfmt.Runoff(&b, doc); err != nil {
t.Fatal(err)
}
t.Run("basic", func(t *testing.T) {
var b bytes.Buffer
doc := textfmt.Doc(textfmt.Title(0, "This is a title"))
if err := textfmt.Runoff(&b, doc); err != nil {
t.Fatal(err)
}
const expect = `.in 0
const expect = `.in 0
.ti 0
\fBThis is a title\fR
`
if b.String() != expect {
t.Fatal(b.String())
}
if b.String() != expect {
t.Fatal(b.String())
}
})
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) {

View File

@ -316,14 +316,28 @@ Entry explanations:
})
t.Run("failing writer", func(t *testing.T) {
w := &failingWriter{failAfter: 15}
doc := textfmt.Doc(
textfmt.Paragraph(textfmt.Text("Some sample text...\non multiple lines.")),
)
t.Run("once", func(t *testing.T) {
w := &failingWriter{failAfter: 15}
doc := textfmt.Doc(
textfmt.Paragraph(textfmt.Text("Some sample text...\non multiple lines.")),
)
if err := textfmt.Teletype(w, doc); err == nil {
t.Fatal("failed to fail")
}
if err := textfmt.Teletype(w, doc); err == nil {
t.Fatal("failed to fail")
}
})
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) {