Implement the gemtext to HTML converter, along with a standalone executable

This commit is contained in:
Erica Z 2021-10-18 18:11:53 +02:00
parent 44374e37c0
commit 79c8c3134b
4 changed files with 262 additions and 0 deletions

View File

@ -43,6 +43,7 @@ var (
"mmark",
"org",
"pandoc", "pdc",
"gmi",
}
contentFileExtensionsSet map[string]bool

224
markup/gemtext/convert.go Normal file
View File

@ -0,0 +1,224 @@
// Copyright 2019 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package gemtext converts Gemtext (text/gemini) to HTML.
package gemtext
import (
"fmt"
"html"
"strings"
"github.com/gohugoio/hugo/identity"
"github.com/gohugoio/hugo/markup/converter"
)
/// Provider is the package entry point.
var Provider converter.ProviderProvider = provider{}
type provider struct {
}
func (p provider) New(cfg converter.ProviderConfig) (converter.Provider, error) {
return converter.NewProvider("gemtext", func(ctx converter.DocumentContext) (converter.Converter, error) {
return &gemtextConverter{
ctx: ctx,
cfg: cfg,
}, nil
}), nil
}
type gemtextConverter struct {
ctx converter.DocumentContext
cfg converter.ProviderConfig
}
func (c *gemtextConverter) Convert(ctx converter.RenderContext) (converter.Result, error) {
return &gemtextResult{
output: Convert(string(ctx.Src)),
}, nil
}
func Convert(src string) string {
var output strings.Builder
const (
stateDefault = iota
stateParagraph
stateQuote
stateList
stateLinks
statePreformatted
)
state := stateDefault
switchState := func(newState int) {
switch state {
case stateParagraph:
output.WriteString("</p>\n")
case stateQuote:
output.WriteString("</p>\n</blockquote>\n")
case statePreformatted:
output.WriteString("</pre>\n</figure>\n")
case stateList:
fallthrough
case stateLinks:
output.WriteString("</ul>\n")
}
state = newState
}
// iterate line by line
for _, line := range strings.Split(src, "\n") {
if state == statePreformatted {
if strings.HasPrefix(line, "```") {
switchState(stateDefault)
} else {
output.WriteString(html.EscapeString(line))
output.WriteString("\n")
}
continue
}
// check line type
if strings.HasPrefix(line, "###") {
switchState(stateDefault)
stripped := strings.TrimPrefix(line, "###")
output.WriteString("<h3>")
output.WriteString(html.EscapeString(stripped))
output.WriteString("</h3>\n")
} else if strings.HasPrefix(line, "##") {
switchState(stateDefault)
stripped := strings.TrimPrefix(line, "##")
output.WriteString("<h2>")
output.WriteString(html.EscapeString(stripped))
output.WriteString("</h2>\n")
} else if strings.HasPrefix(line, "#") {
switchState(stateDefault)
stripped := strings.TrimPrefix(line, "#")
output.WriteString("<h1>")
output.WriteString(html.EscapeString(stripped))
output.WriteString("</h1>\n")
} else if strings.HasPrefix(line, "=>") {
// link line
if state != stateLinks {
switchState(stateLinks)
output.WriteString(`<ul class="links">`)
output.WriteString("\n")
}
// extract url and link name
url := ""
linkName := ""
withoutPrefix := strings.TrimSpace(strings.TrimPrefix(line, "=>"))
split := strings.SplitN(withoutPrefix, " ", 2)
if len(split) >= 1 {
url = split[0]
}
if len(split) >= 2 {
linkName = strings.TrimSpace(split[1])
}
output.WriteString("<li>")
// output as an <a> tag
output.WriteString(fmt.Sprintf(`<a href="%s">`, html.EscapeString(url)))
if len(linkName) > 0 {
output.WriteString(html.EscapeString(linkName))
} else {
output.WriteString(html.EscapeString(url))
}
output.WriteString("</a>\n")
} else if strings.HasPrefix(line, "* ") {
// list item line
if state != stateList {
switchState(stateList)
output.WriteString(`<ul class="items">`)
output.WriteString("\n")
}
withoutPrefix := strings.TrimPrefix(line, "* ")
output.WriteString("<li>")
output.WriteString(html.EscapeString(withoutPrefix))
output.WriteString("\n")
} else if strings.HasPrefix(line, "```") {
// preformatted toggle
switchState(statePreformatted)
output.WriteString("<figure>\n")
alt := strings.TrimSpace(strings.TrimPrefix(line, "```"))
if len(alt) > 0 {
output.WriteString("<figcaption>")
output.WriteString(html.EscapeString(alt))
output.WriteString("</figcaption>\n")
}
output.WriteString("<pre>\n")
} else if strings.HasPrefix(line, ">") {
// quoted line
withoutPrefix := strings.TrimSpace(strings.TrimPrefix(line, ">"))
if len(withoutPrefix) == 0 {
output.WriteString("</p>\n<p>\n")
} else if state == stateQuote {
output.WriteString("<br>\n")
} else {
switchState(stateQuote)
output.WriteString("<blockquote>\n")
output.WriteString("<p>\n")
}
output.WriteString(html.EscapeString(withoutPrefix))
output.WriteString("\n")
} else if len(strings.TrimSpace(line)) == 0 {
// empty line, go back to default state
switchState(stateDefault)
} else {
// fallback to text line
if state == stateParagraph {
output.WriteString("<br>\n")
} else {
switchState(stateParagraph)
output.WriteString("<p>\n")
}
output.WriteString(html.EscapeString(line))
output.WriteString("\n")
}
}
// ensure no tags are left open
switchState(stateDefault)
return output.String()
}
func (c *gemtextConverter) Supports(feature identity.Identity) bool {
return false
}
type gemtextResult struct {
output string
}
func (r gemtextResult) Bytes() []byte {
return []byte(r.output)
}

View File

@ -0,0 +1,33 @@
// Copyright 2019 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// This program reads gemtext from standard input and outputs its conversion
// to HTML.
package main
import (
"fmt"
"io/ioutil"
"os"
"github.com/gohugoio/hugo/markup/gemtext"
)
func main() {
input, err := ioutil.ReadAll(os.Stdin)
if err != nil {
panic(err)
}
output := gemtext.Convert(string(input))
fmt.Print(output)
}

View File

@ -30,6 +30,7 @@ import (
"github.com/gohugoio/hugo/markup/mmark"
"github.com/gohugoio/hugo/markup/pandoc"
"github.com/gohugoio/hugo/markup/rst"
"github.com/gohugoio/hugo/markup/gemtext"
)
func NewConverterProvider(cfg converter.ProviderConfig) (ConverterProvider, error) {
@ -88,6 +89,9 @@ func NewConverterProvider(cfg converter.ProviderConfig) (ConverterProvider, erro
if err := add(org.Provider); err != nil {
return nil, err
}
if err := add(gemtext.Provider, "gmi"); err != nil {
return nil, err
}
return &converterRegistry{
config: cfg,