From 79c8c3134b21d63a3b88a41662e36c174419fbd7 Mon Sep 17 00:00:00 2001
From: Erica Z
Date: Mon, 18 Oct 2021 18:11:53 +0200
Subject: [PATCH] Implement the gemtext to HTML converter, along with a
standalone executable
---
hugofs/files/classifier.go | 1 +
markup/gemtext/convert.go | 224 ++++++++++++++++++++++++++++++
markup/gemtext/standalone/main.go | 33 +++++
markup/markup.go | 4 +
4 files changed, 262 insertions(+)
create mode 100644 markup/gemtext/convert.go
create mode 100644 markup/gemtext/standalone/main.go
diff --git a/hugofs/files/classifier.go b/hugofs/files/classifier.go
index f0e0911a..72614648 100644
--- a/hugofs/files/classifier.go
+++ b/hugofs/files/classifier.go
@@ -43,6 +43,7 @@ var (
"mmark",
"org",
"pandoc", "pdc",
+ "gmi",
}
contentFileExtensionsSet map[string]bool
diff --git a/markup/gemtext/convert.go b/markup/gemtext/convert.go
new file mode 100644
index 00000000..9a4cf320
--- /dev/null
+++ b/markup/gemtext/convert.go
@@ -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("
\n")
+ case stateQuote:
+ output.WriteString("\n\n")
+ case statePreformatted:
+ output.WriteString("\n\n")
+ case stateList:
+ fallthrough
+ case stateLinks:
+ output.WriteString("\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("")
+ output.WriteString(html.EscapeString(stripped))
+ output.WriteString("
\n")
+ } else if strings.HasPrefix(line, "##") {
+ switchState(stateDefault)
+
+ stripped := strings.TrimPrefix(line, "##")
+
+ output.WriteString("")
+ output.WriteString(html.EscapeString(stripped))
+ output.WriteString("
\n")
+ } else if strings.HasPrefix(line, "#") {
+ switchState(stateDefault)
+
+ stripped := strings.TrimPrefix(line, "#")
+
+ output.WriteString("")
+ output.WriteString(html.EscapeString(stripped))
+ output.WriteString("
\n")
+ } else if strings.HasPrefix(line, "=>") {
+ // link line
+ if state != stateLinks {
+ switchState(stateLinks)
+ output.WriteString(``)
+ 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("- ")
+ // output as an tag
+ output.WriteString(fmt.Sprintf(``, html.EscapeString(url)))
+ if len(linkName) > 0 {
+ output.WriteString(html.EscapeString(linkName))
+ } else {
+ output.WriteString(html.EscapeString(url))
+ }
+ output.WriteString("\n")
+ } else if strings.HasPrefix(line, "* ") {
+ // list item line
+ if state != stateList {
+ switchState(stateList)
+ output.WriteString(`
`)
+ output.WriteString("\n")
+ }
+
+ withoutPrefix := strings.TrimPrefix(line, "* ")
+ output.WriteString("- ")
+ output.WriteString(html.EscapeString(withoutPrefix))
+ output.WriteString("\n")
+ } else if strings.HasPrefix(line, "```") {
+ // preformatted toggle
+ switchState(statePreformatted)
+
+ output.WriteString("