Completed markdown and HTML conversion.
continuous-integration/drone/push Build is passing
Details
continuous-integration/drone/push Build is passing
Details
This commit is contained in:
parent
cec3718bdd
commit
4c2630752f
|
@ -0,0 +1,20 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"tildegit.org/tjp/gus/gemtext"
|
||||
"tildegit.org/tjp/gus/gemtext/htmlconv"
|
||||
)
|
||||
|
||||
func main() {
|
||||
gmiDoc, err := gemtext.Parse(os.Stdin)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if err := htmlconv.Convert(os.Stdout, gmiDoc, nil); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,86 @@
|
|||
package htmlconv
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"io"
|
||||
|
||||
"tildegit.org/tjp/gus/gemtext"
|
||||
"tildegit.org/tjp/gus/gemtext/internal"
|
||||
)
|
||||
|
||||
// Convert writes markdown to a writer from the provided gemtext document.
|
||||
//
|
||||
// Templates can be provided to override the output for different line types.
|
||||
// The templates supported are:
|
||||
// - "header" is called before any lines and is passed the full Document.
|
||||
// - "footer" is called after the lines and is passed the full Document.
|
||||
// - "textline" is called once per line of text and is passed a gemtext.TextLine.
|
||||
// - "linkline" is called once per link line and is passed an object which wraps
|
||||
// a gemtext.LinkLine but also supports a ValidatedURL() method returning a
|
||||
// string which html/template will always allow as href attributes.
|
||||
// - "preformattedtextlines" is called once for a block of preformatted text and is
|
||||
// passed a slice of gemtext.PreformattedTextLines.
|
||||
// - "heading1line" is called once per h1 line and is passed a gemtext.Heading1Line.
|
||||
// - "heading2line" is called once per h2 line and is passed a gemtext.Heading2Line.
|
||||
// - "heading3line" is called once per h3 line and is passed a gemtext.Heading3Line.
|
||||
// - "listitemlines" is called once for a block of contiguous list item lines and
|
||||
// is passed a slice of gemtext.ListItemLines.
|
||||
// - "quoteline" is passed once per blockquote line and is passed a gemtext.QuoteLine.
|
||||
//
|
||||
// There exist default implementations of each of these templates, so the "overrides"
|
||||
// argument can be nil.
|
||||
func Convert(wr io.Writer, doc gemtext.Document, overrides *template.Template) error {
|
||||
if err := internal.ValidateLinks(doc); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tmpl, err := baseTmpl.Clone()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tmpl, err = internal.AddHTMLTemplates(tmpl, overrides)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, item := range internal.RenderItems(doc) {
|
||||
if err := tmpl.ExecuteTemplate(wr, item.Template, item.Object); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var baseTmpl = template.Must(template.New("htmlconv").Parse(`
|
||||
{{ define "header" }}<html><body>{{ end }}
|
||||
{{ define "textline" }}{{ if ne .String "\n" }}<p>{{ . }}</p>{{ end }}{{ end }}
|
||||
{{ define "linkline" -}}
|
||||
<p>=> <a href="{{ .ValidatedURL }}">{{ if eq .Label "" -}}
|
||||
{{ .URL }}
|
||||
{{- else -}}
|
||||
{{ .Label }}
|
||||
{{- end -}}
|
||||
</a></p>
|
||||
{{- end }}
|
||||
{{ define "preformattedtextlines" -}}
|
||||
<pre>
|
||||
{{- range . -}}
|
||||
{{ . }}
|
||||
{{- end -}}
|
||||
</pre>
|
||||
{{- end }}
|
||||
{{ define "heading1line" }}<h1>{{ .Body }}</h1>{{ end }}
|
||||
{{ define "heading2line" }}<h2>{{ .Body }}</h2>{{ end }}
|
||||
{{ define "heading3line" }}<h3>{{ .Body }}</h3>{{ end }}
|
||||
{{ define "listitemlines" -}}
|
||||
<ul>
|
||||
{{- range . -}}
|
||||
<li>{{ .Body }}</li>
|
||||
{{- end -}}
|
||||
</ul>
|
||||
{{- end }}
|
||||
{{ define "quoteline" }}<blockquote>{{ .Body }}</blockquote>{{ end }}
|
||||
{{ define "footer" }}</body></html>{{ end }}
|
||||
`))
|
|
@ -0,0 +1,46 @@
|
|||
package htmlconv_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"tildegit.org/tjp/gus/gemtext"
|
||||
"tildegit.org/tjp/gus/gemtext/htmlconv"
|
||||
)
|
||||
|
||||
var gmiDoc = `
|
||||
# top-level header line
|
||||
|
||||
## subtitle
|
||||
|
||||
This is some non-blank regular text.
|
||||
|
||||
* an
|
||||
* unordered
|
||||
* list
|
||||
|
||||
=> gemini://google.com/ as if
|
||||
=> https://google.com/
|
||||
|
||||
> this is a quote
|
||||
> -tjp
|
||||
|
||||
`[1:] + "```pre-formatted code\ndoc := gemtext.Parse(req.Body)\n```ignored closing alt-text\n"
|
||||
|
||||
func TestConvert(t *testing.T) {
|
||||
htmlDoc := `
|
||||
<html><body><h1>top-level header line</h1><h2>subtitle</h2><p>This is some non-blank regular text.
|
||||
</p><ul><li>an</li><li>unordered</li><li>list</li></ul><p>=> <a href="gemini://google.com/">as if</a></p><p>=> <a href="https://google.com/">https://google.com/</a></p><blockquote> this is a quote</blockquote><blockquote> -tjp</blockquote><pre>doc := gemtext.Parse(req.Body)
|
||||
</pre></body></html>`[1:]
|
||||
|
||||
doc, err := gemtext.Parse(bytes.NewBufferString(gmiDoc))
|
||||
require.Nil(t, err)
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
require.Nil(t, htmlconv.Convert(buf, doc, nil))
|
||||
|
||||
assert.Equal(t, htmlDoc, buf.String())
|
||||
}
|
|
@ -0,0 +1,150 @@
|
|||
package internal
|
||||
|
||||
import (
|
||||
htemplate "html/template"
|
||||
"net/url"
|
||||
"text/template"
|
||||
|
||||
"tildegit.org/tjp/gus/gemtext"
|
||||
)
|
||||
|
||||
var Renderers = map[gemtext.LineType]string{
|
||||
gemtext.LineTypeText: "textline",
|
||||
gemtext.LineTypeLink: "linkline",
|
||||
gemtext.LineTypeHeading1: "heading1line",
|
||||
gemtext.LineTypeHeading2: "heading2line",
|
||||
gemtext.LineTypeHeading3: "heading3line",
|
||||
gemtext.LineTypeQuote: "quoteline",
|
||||
}
|
||||
|
||||
func AddAllTemplates(base *template.Template, additions *template.Template) (*template.Template, error) {
|
||||
if additions == nil {
|
||||
return base, nil
|
||||
}
|
||||
|
||||
tmpl := base
|
||||
var err error
|
||||
for _, addition := range additions.Templates() {
|
||||
tmpl, err = tmpl.AddParseTree(addition.Name(), addition.Tree)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return tmpl, nil
|
||||
}
|
||||
|
||||
func AddHTMLTemplates(base *htemplate.Template, additions *htemplate.Template) (*htemplate.Template, error) {
|
||||
if additions == nil {
|
||||
return base, nil
|
||||
}
|
||||
|
||||
tmpl := base
|
||||
var err error
|
||||
for _, addition := range additions.Templates() {
|
||||
tmpl, err = tmpl.AddParseTree(addition.Name(), addition.Tree)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return tmpl, nil
|
||||
}
|
||||
|
||||
func ValidateLinks(doc gemtext.Document) error {
|
||||
for _, line := range doc {
|
||||
if linkLine, ok := line.(gemtext.LinkLine); ok {
|
||||
_, err := url.Parse(linkLine.URL())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type RenderItem struct {
|
||||
Template string
|
||||
Object any
|
||||
}
|
||||
|
||||
func RenderItems(doc gemtext.Document) []RenderItem {
|
||||
out := make([]RenderItem, 0, len(doc))
|
||||
out = append(out, RenderItem{
|
||||
Template: "header",
|
||||
Object: doc,
|
||||
})
|
||||
|
||||
inUL := false
|
||||
ulStart := 0
|
||||
inPF := false
|
||||
pfStart := 0
|
||||
|
||||
for i, line := range doc {
|
||||
switch line.Type() {
|
||||
case gemtext.LineTypeListItem:
|
||||
if !inUL {
|
||||
inUL = true
|
||||
ulStart = i
|
||||
}
|
||||
case gemtext.LineTypePreformatToggle:
|
||||
if inUL {
|
||||
inUL = false
|
||||
out = append(out, RenderItem{
|
||||
Template: "listitemlines",
|
||||
Object: doc[ulStart:i],
|
||||
})
|
||||
}
|
||||
if !inPF {
|
||||
inPF = true
|
||||
pfStart = i
|
||||
} else {
|
||||
inPF = false
|
||||
out = append(out, RenderItem{
|
||||
Template: "preformattedtextlines",
|
||||
Object: doc[pfStart+1 : i],
|
||||
})
|
||||
}
|
||||
case gemtext.LineTypePreformattedText:
|
||||
default:
|
||||
if inUL {
|
||||
inUL = false
|
||||
out = append(out, RenderItem{
|
||||
Template: "listitemlines",
|
||||
Object: doc[ulStart:i],
|
||||
})
|
||||
}
|
||||
|
||||
if linkLine, ok := line.(gemtext.LinkLine); ok {
|
||||
line = validatedLinkLine{linkLine}
|
||||
}
|
||||
|
||||
out = append(out, RenderItem{
|
||||
Template: Renderers[line.Type()],
|
||||
Object: line,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if inUL {
|
||||
out = append(out, RenderItem{
|
||||
Template: "listitemlines",
|
||||
Object: doc[ulStart:],
|
||||
})
|
||||
}
|
||||
|
||||
out = append(out, RenderItem{
|
||||
Template: "footer",
|
||||
Object: doc,
|
||||
})
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
type validatedLinkLine struct {
|
||||
gemtext.LinkLine
|
||||
}
|
||||
|
||||
func (vll validatedLinkLine) ValidatedURL() htemplate.URL {
|
||||
return htemplate.URL(vll.URL())
|
||||
}
|
|
@ -1,72 +1,72 @@
|
|||
package mdconv
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"text/template"
|
||||
|
||||
"tildegit.org/tjp/gus/gemtext"
|
||||
"tildegit.org/tjp/gus/gemtext/internal"
|
||||
)
|
||||
|
||||
// Convert writes markdown to a writer from the provided gemtext document.
|
||||
//
|
||||
// Templates can be provided to override the output for different line types.
|
||||
// The templates supported are:
|
||||
// - "header" is called before any lines and is passed the full Document.
|
||||
// - "footer" is called after the lines and is passed the full Document.
|
||||
// - "textline" is called once per line of text and is passed a gemtext.TextLine.
|
||||
// - "linkline" is called once per link line and is passed a gemtext.LinkLine.
|
||||
// - "preformattedtextlines" is called once for a block of preformatted text and is
|
||||
// passed a slice of gemtext.PreformattedTextLines.
|
||||
// - "heading1line" is called once per h1 line and is passed a gemtext.Heading1Line.
|
||||
// - "heading2line" is called once per h2 line and is passed a gemtext.Heading2Line.
|
||||
// - "heading3line" is called once per h3 line and is passed a gemtext.Heading3Line.
|
||||
// - "listitemlines" is called once for a block of contiguous list item lines and
|
||||
// is passed a slice of gemtext.ListItemLines.
|
||||
// - "quoteline" is passed once per blockquote line and is passed a gemtext.QuoteLine.
|
||||
//
|
||||
// There exist default implementations of each of these templates, so the "overrides"
|
||||
// argument can be nil.
|
||||
func Convert(wr io.Writer, doc gemtext.Document, overrides *template.Template) error {
|
||||
if err := internal.ValidateLinks(doc); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tmpl, err := baseTmpl.Clone()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if overrides != nil {
|
||||
for _, override := range overrides.Templates() {
|
||||
tmpl, err = tmpl.AddParseTree(override.Name(), override.Tree)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tmpl, err = internal.AddAllTemplates(tmpl, overrides)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, item := range internal.RenderItems(doc) {
|
||||
if err := tmpl.ExecuteTemplate(wr, item.Template, item.Object); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return tmpl.ExecuteTemplate(wr, "mdconv", doc)
|
||||
return nil
|
||||
}
|
||||
|
||||
var baseTmpl = template.Must(template.New("mdconv").Parse(fmt.Sprintf((`
|
||||
{{block "header" .}}{{end -}}
|
||||
{{range . -}}
|
||||
{{if .Type | eq %d}}{{block "textline" . -}}
|
||||
{{. -}}
|
||||
{{end -}}
|
||||
{{else if .Type | eq %d}}{{block "linkline" . -}}
|
||||
=> [{{if eq .Label ""}}{{.URL}}{{else}}{{.Label}}{{end}}]({{.URL}})
|
||||
{{end -}}
|
||||
{{else if .Type | eq %d}}{{block "preformattoggleline" . -}}
|
||||
` + "```" + `
|
||||
{{end -}}
|
||||
{{else if .Type | eq %d}}{{block "preformattedtextline" . -}}
|
||||
{{. -}}
|
||||
{{end -}}
|
||||
{{else if .Type | eq %d}}{{block "heading1line" . -}}
|
||||
# {{.Body}}
|
||||
{{end -}}
|
||||
{{else if .Type | eq %d}}{{block "heading2line" . -}}
|
||||
## {{.Body}}
|
||||
{{end -}}
|
||||
{{else if .Type | eq %d}}{{block "heading3line" . -}}
|
||||
### {{.Body}}
|
||||
{{end -}}
|
||||
{{else if .Type | eq %d}}{{block "listitemline" . -}}
|
||||
* {{.Body}}
|
||||
{{end -}}
|
||||
{{else if .Type | eq %d}}{{block "quoteline" . -}}
|
||||
> {{.Body}}
|
||||
{{end -}}
|
||||
{{end -}}
|
||||
{{end -}}
|
||||
{{block "footer" .}}{{end -}}
|
||||
`)[1:],
|
||||
gemtext.LineTypeText,
|
||||
gemtext.LineTypeLink,
|
||||
gemtext.LineTypePreformatToggle,
|
||||
gemtext.LineTypePreformattedText,
|
||||
gemtext.LineTypeHeading1,
|
||||
gemtext.LineTypeHeading2,
|
||||
gemtext.LineTypeHeading3,
|
||||
gemtext.LineTypeListItem,
|
||||
gemtext.LineTypeQuote,
|
||||
)))
|
||||
var baseTmpl = template.Must(template.New("mdconv").Parse(`
|
||||
{{ define "header" }}{{ end }}
|
||||
{{ define "textline" }}{{ . }}{{ end }}
|
||||
{{ define "linkline" -}}
|
||||
=> [{{ if eq .Label "" }}{{ .URL }}{{ else }}{{ .Label }}{{ end }}]({{ .URL }})
|
||||
{{ end }}
|
||||
{{ define "preformattedtextlines" }}` + "```\n" + `{{ range . }}{{ . }}{{ end }}` + "```\n" + `{{ end }}
|
||||
{{ define "heading1line" }}# {{ .Body }}
|
||||
{{ end }}
|
||||
{{ define "heading2line" }}## {{ .Body }}
|
||||
{{ end }}
|
||||
{{ define "heading3line" }}### {{ .Body }}
|
||||
{{ end }}
|
||||
{{ define "listitemlines" }}{{ range . }}* {{ .Body }}
|
||||
{{ end }}{{ end }}
|
||||
{{ define "quoteline" }}> {{ .Body }}
|
||||
{{ end }}
|
||||
{{ define "footer" }}{{ end }}
|
||||
`))
|
||||
|
|
|
@ -78,17 +78,16 @@ text:
|
|||
> quote: this is a quote
|
||||
> quote: -tjp
|
||||
text:
|
||||
`[1:] + "pftoggle: ```\npf: doc := gemtext.Parse(req.Body)\npftoggle: ```\n"
|
||||
`[1:] + "```\npf: doc := gemtext.Parse(req.Body)\n```\n"
|
||||
|
||||
overrides := template.Must(template.New("overrides").Parse((`
|
||||
{{define "textline"}}text: {{.}}{{end}}
|
||||
{{define "linkline"}}=> link: [{{if eq .Label ""}}{{.URL}}{{else}}{{.Label}}{{end}}]({{.URL}})` + "\n" + `{{end}}
|
||||
{{define "preformattoggleline"}}pftoggle: ` + "```\n" + `{{end}}
|
||||
{{define "preformattedtextline"}}pf: {{.}}{{end}}
|
||||
{{define "preformattedtextlines"}}` + "```\n" + `{{range . }}pf: {{.}}{{end}}` + "```\n" + `{{end}}
|
||||
{{define "heading1line"}}# h1: {{.Body}}` + "\n" + `{{end}}
|
||||
{{define "heading2line"}}## h2: {{.Body}}` + "\n" + `{{end}}
|
||||
{{define "heading3line"}}### h3: {{.Body}}` + "\n" + `{{end}}
|
||||
{{define "listitemline"}}* li: {{.Body}}` + "\n" + `{{end}}
|
||||
{{define "listitemlines"}}{{range .}}* li: {{.Body}}` + "\n" + `{{end}}{{end}}
|
||||
{{define "quoteline"}}> quote: {{.Body}}` + "\n" + `{{end}}
|
||||
`)[1:]))
|
||||
|
||||
|
|
Reference in New Issue