From ecb46a4b0960bebb8a77fcdbbcc13117e2757993 Mon Sep 17 00:00:00 2001 From: Arpad Ryszka Date: Tue, 28 Oct 2025 02:48:55 +0100 Subject: [PATCH] initial html implementation --- go.mod | 2 + go.sum | 2 + html.go | 306 +++++++++++++++++++++++++++++++++++++++++++++++++++ html_test.go | 1 + lib.go | 9 +- markdown.go | 4 +- runoff.go | 4 +- teletype.go | 4 +- 8 files changed, 322 insertions(+), 10 deletions(-) create mode 100644 html.go create mode 100644 html_test.go diff --git a/go.mod b/go.mod index d03268c..45b0bdc 100644 --- a/go.mod +++ b/go.mod @@ -3,3 +3,5 @@ module code.squareroundforest.org/arpio/textfmt go 1.25.0 require code.squareroundforest.org/arpio/notation v0.0.0-20250826181910-5140794b16b2 + +require code.squareroundforest.org/arpio/html v0.0.0-20251011102613-70f77954001f // indirect diff --git a/go.sum b/go.sum index a66f3d0..017d044 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,4 @@ +code.squareroundforest.org/arpio/html v0.0.0-20251011102613-70f77954001f h1:Ep/POhkmvOfSkQklPIpeA4n2FTD2SoFxthjF0SJbsCU= +code.squareroundforest.org/arpio/html v0.0.0-20251011102613-70f77954001f/go.mod h1:LX+Fwqu/a7nDayuDNhXA56cVb+BNrkz4M/WCqvw9YFQ= code.squareroundforest.org/arpio/notation v0.0.0-20250826181910-5140794b16b2 h1:S4mjQHL70CuzFg1AGkr0o0d+4M+ZWM0sbnlYq6f0b3I= code.squareroundforest.org/arpio/notation v0.0.0-20250826181910-5140794b16b2/go.mod h1:ait4Fvg9o0+bq5hlxi9dAcPL5a+/sr33qsZPNpToMLY= diff --git a/html.go b/html.go new file mode 100644 index 0000000..13dd097 --- /dev/null +++ b/html.go @@ -0,0 +1,306 @@ +package textfmt + +import ( + "code.squareroundforest.org/arpio/html" + "code.squareroundforest.org/arpio/html/tag" + "errors" + "fmt" + "io" + "strings" +) + +func htmlText(t Txt) []any { + if len(t.cat) > 0 { + var c []any + for _, ti := range t.cat { + c = append(c, htmlText(ti)...) + } + + return c + } + + if t.link != "" { + return []any{tag.A(html.Attr("href", t.link), t.text)} + } + + return []any{t.text} +} + +func htmlTitle(e Entry) html.Tag { + h := tag.H6 + switch e.titleLevel { + case 0: + h = tag.H1 + case 1: + h = tag.H2 + case 2: + h = tag.H3 + case 3: + h = tag.H4 + case 4: + h = tag.H5 + } + + return h(htmlText(e.text)...) +} + +func htmlParagraph(e Entry) html.Tag { + return tag.P(htmlText(e.text)...) +} + +func htmlList(e Entry) html.Tag { + list := tag.Ul + for _, item := range e.items { + list = list(tag.Li(htmlText(item.text)...)) + } + + return list +} + +func htmlNumberedList(e Entry) html.Tag { + list := tag.Ol + for _, item := range e.items { + list = list(tag.Li(htmlText(item.text)...)) + } + + return list +} + +func htmlDefinitions(e Entry) html.Tag { + list := tag.Dl + for _, definition := range e.definitions { + list = list( + tag.Dt(htmlText(definition.name)...), + tag.Dd(htmlText(definition.value)...), + ) + } + + return list +} + +func htmlNumberedDefinitions(e Entry) html.Tag { + list := tag.Dl + for i, definition := range e.definitions { + list = list( + tag.Dt(append([]any{fmt.Sprintf("%d. ", i+1)}, htmlText(definition.name)...)...), + tag.Dd(htmlText(definition.value)...), + ) + } + + return list +} + +func htmlTable(e Entry) html.Tag { + table := tag.Table + for _, r := range e.rows { + row := tag.Tr + cell := tag.Td + if r.header { + cell = tag.Th + } + + for _, c := range r.cells { + row = row(cell(htmlText(c.text)...)) + } + + table = table(row) + } + + return table +} + +func htmlCode(e Entry) html.Tag { + return tag.Pre(tag.Code(htmlText(e.text)...)) +} + +func htmlMultiple(s SyntaxItem) string { + s.topLevel = false + s.multiple = false + return fmt.Sprintf("%s...", htmlSyntaxItem(s)) +} + +func htmlRequired(s SyntaxItem) string { + s.delimited = true + s.topLevel = false + s.required = false + return fmt.Sprintf("<%s>", htmlSyntaxItem(s)) +} + +func htmlOptional(s SyntaxItem) string { + s.delimited = true + s.topLevel = false + s.optional = false + return fmt.Sprintf("[%s]", htmlSyntaxItem(s)) +} + +func htmlSequence(s SyntaxItem) string { + ss := htmlSyntaxItems(s.sequence) + if s.delimited || s.topLevel { + return strings.Join(ss, " ") + } + + return fmt.Sprintf("(%s)", strings.Join(ss, " ")) +} + +func htmlChoice(s SyntaxItem) string { + ss := htmlSyntaxItems(s.sequence) + if s.topLevel { + return strings.Join(ss, "\n") + } + + if s.delimited { + return strings.Join(ss, "|") + } + + return fmt.Sprintf("(%s)", strings.Join(ss, "|")) +} + +func htmlSymbol(s SyntaxItem) string { + return s.symbol +} + +func htmlSyntaxItem(s SyntaxItem) string { + switch { + + // foo... + case s.multiple: + return htmlMultiple(s) + + // + case s.required: + return htmlRequired(s) + + // [foo] + case s.optional: + return htmlOptional(s) + + // foo bar baz or (foo bar baz) + case len(s.sequence) > 0: + return htmlSequence(s) + + // foo|bar|baz or (foo|bar|baz) + case len(s.choice) > 0: + return htmlChoice(s) + + // foo + default: + return htmlSymbol(s) + } +} + +func htmlSyntaxItems(s []SyntaxItem) []string { + var ss []string + for _, si := range s { + si.delimited = false + ss = append(ss, htmlSyntaxItem(si)) + } + + return ss +} + +func htmlSyntax(e Entry) html.Tag { + s := e.syntax + s.topLevel = true + return tag.Pre(htmlSyntaxItem(s)) +} + +func htmlTag(e Entry) (html.Tag, error) { + switch e.typ { + case title: + return htmlTitle(e), nil + case paragraph: + return htmlParagraph(e), nil + case list: + return htmlList(e), nil + case numberedList: + return htmlNumberedList(e), nil + case definitions: + return htmlDefinitions(e), nil + case numberedDefinitions: + return htmlNumberedDefinitions(e), nil + case table: + return htmlTable(e), nil + case code: + return htmlCode(e), nil + case syntax: + return htmlSyntax(e), nil + default: + return nil, errors.New("invalid entry") + } +} + +func htmlTags(e []Entry) ([]html.Tag, error) { + var tags []html.Tag + for _, ei := range e { + tag, err := htmlTag(ei) + if err != nil { + return nil, err + } + + if tag != nil { + tags = append(tags, tag) + } + } + + return tags, nil +} + +func renderHTMLFragment(out io.Writer, doc Document) error { + tags, err := htmlTags(doc.entries) + if err != nil { + return err + } + + for i, tag := range tags { + indent := html.Indentation{ + Indent: "\t", + PWidth: 120, + MinPWidth: 60, + } + + if doc.entries[i].wrapWidth != 0 { + indent.PWidth = doc.entries[i].wrapWidth + indent.MinPWidth = indent.PWidth / 2 + } + + if doc.entries[i].indent != 0 { + indent.Indent = timesn(" ", doc.entries[i].indent) + } + + if err := html.RenderIndent(out, indent, tag); err != nil { + return err + } + } + + return nil +} + +func renderHTML(out io.Writer, doc Document, lang string) error { + tags, err := htmlTags(doc.entries) + if err != nil { + return err + } + + head := tag.Head(tag.Meta(html.Attr("charset", "utf-8"))) + if len(doc.entries) > 0 && doc.entries[0].typ == title && doc.entries[0].titleLevel == 0 { + head = head(tag.Title(htmlText(doc.entries[0].text)...)) + } + + body := tag.Body + for _, tag := range tags { + body = body(tag) + } + + htmlDoc := tag.Html(head, body) + if lang != "" { + htmlDoc = htmlDoc(html.Attr("lang", lang)) + } + + indent := html.Indentation{ + Indent: "\t", + PWidth: 120, + MinPWidth: 60, + } + + return html.RenderIndent(out, indent, tag.Doctype("html"), htmlDoc) +} diff --git a/html_test.go b/html_test.go new file mode 100644 index 0000000..49b8ef8 --- /dev/null +++ b/html_test.go @@ -0,0 +1 @@ +package textfmt_test diff --git a/lib.go b/lib.go index 4baff9e..1b4a7f2 100644 --- a/lib.go +++ b/lib.go @@ -284,12 +284,13 @@ func Markdown(out io.Writer, d Document) error { return renderMarkdown(out, d) } -func HTML(io.Writer, Document) error { +func HTMLFragment(out io.Writer, doc Document) error { // with the won HTML library - return nil + return renderHTMLFragment(out, doc) } -func HTMLFragment(io.Writer, Document) error { +// if lang is empty, the lang attribute will be omitted +func HTML(out io.Writer, doc Document, lang string) error { // with the won HTML library - return nil + return renderHTML(out, doc, lang) } diff --git a/markdown.go b/markdown.go index 41ea307..1c6c83b 100644 --- a/markdown.go +++ b/markdown.go @@ -429,8 +429,6 @@ func renderMarkdown(out io.Writer, d Document) error { } switch e.typ { - case invalid: - return errors.New("invalid entry") case title: renderMDTitle(&w, e) case paragraph: @@ -449,6 +447,8 @@ func renderMarkdown(out io.Writer, d Document) error { renderMDCode(&w, e) case syntax: renderMDSyntax(&w, e) + default: + return errors.New("invalid entry") } } diff --git a/runoff.go b/runoff.go index 03c072c..043d70e 100644 --- a/runoff.go +++ b/runoff.go @@ -463,8 +463,6 @@ func renderRoff(out io.Writer, d Document) error { } switch e.typ { - case invalid: - return errors.New("invalid entry") case title: renderRoffTitle(&w, e) case paragraph: @@ -483,6 +481,8 @@ func renderRoff(out io.Writer, d Document) error { renderRoffCode(&w, e) case syntax: renderRoffSyntax(&w, e) + default: + return errors.New("invalid entry") } } diff --git a/teletype.go b/teletype.go index 458554a..f5f7d0b 100644 --- a/teletype.go +++ b/teletype.go @@ -402,8 +402,6 @@ func renderTeletype(out io.Writer, d Document) error { } switch e.typ { - case invalid: - return errors.New("invalid entry") case title: renderTTYTitle(&w, e) case paragraph: @@ -422,6 +420,8 @@ func renderTeletype(out io.Writer, d Document) error { renderTTYCode(&w, e) case syntax: renderTTYSyntax(&w, e) + default: + return errors.New("invalid entry") } }