This commit is contained in:
Josip Tišljar Mataušić
2026-05-20 16:02:46 +02:00
commit c5fb1169b0
5 changed files with 266 additions and 0 deletions
+5
View File
@@ -0,0 +1,5 @@
module git.radioz.org/gemtext2gophermap
go 1.26.3
require github.com/mcecode/gemtext-parser v0.1.0 // indirect
+2
View File
@@ -0,0 +1,2 @@
github.com/mcecode/gemtext-parser v0.1.0 h1:yyZoQHi0tLxGUI4pIrJH5GpzETCAE8A9GupaQwH1/as=
github.com/mcecode/gemtext-parser v0.1.0/go.mod h1:wj59tK8UABx4+Bp62e4Xp6OfokYCev2izvZmXpm/8Xk=
+105
View File
@@ -0,0 +1,105 @@
package main
import (
"slices"
"mime"
"strings"
"path/filepath"
)
type GopherTypeCharParams struct {
GophermapFiles []string
GophermapExtensions []string
PromptFileExtensions []string
}
var plainTextExtensions = []string{
".txt", ".md", ".markdown", ".rst", ".adoc", ".text", ".asc", ".utxt",
".json", ".jsonl", ".yaml", ".yml", ".toml", ".xml", ".xsd", ".xsl", ".xslt",
".ini", ".conf", ".cfg", ".cnf", ".env", ".properties", ".prefs", ".opts",
".csv", ".tsv", ".tab", ".log", ".sql", ".ddl", ".dump",
".go", ".c", ".cpp", ".h", ".hpp", ".cc", ".hh", ".cxx", ".hxx", ".h++",
".cs", ".fs", ".fsx", ".fsi", ".java", ".kt", ".kts", ".rs", ".swift",
".py", ".pyw", ".rb", ".rbw", ".pl", ".pm", ".tcl", ".lua", ".php", ".phtml",
".js", ".mjs", ".cjs", ".jsx", ".ts", ".tsx", ".vue", ".svelte", ".dart",
".sh", ".bash", ".zsh", ".fish", ".bat", ".cmd", ".ps1", ".psm1", ".psd1",
".css", ".scss", ".sass", ".less", ".styl", ".html", ".htm", ".shtml", ".xhtml",
".dockerfile", ".makefile", ".cmake", ".gradle", ".gitignore", ".gitconfig",
".gitattributes", ".editorconfig", ".npmrc", ".babelrc", ".eslintrc",
".diff", ".patch", ".latex", ".tex", ".bib", ".cls", ".sty",
".erl", ".hrl", ".ex", ".exs", ".hs", ".lhs", ".scala", ".sc", ".clj", ".cljs",
".edn", ".lisp", ".lsp", ".scm", ".ss", ".r", ".rmd", ".f", ".f90", ".f95",
".m", ".matlab", ".jl", ".v", ".vhdl", ".sv", ".proto", ".thrift",
".tpl", ".tmpl", ".twig", ".blade", ".erb", ".haml", ".pug", ".jade",
".srt", ".vtt", ".sub",
}
var areAdditionalMimeTypesRegistered = false
var archives = []string{
"application/x-archive", "application/x-cpio", "application/x-shar",
"application/x-iso9660-image", "application/x-sbx", "application/x-tar",
"application/x-brotli", "application/x-bzip2", "application/vnd.genozip",
"application/gzip", "application/lzip", "application/x-lzma",
"application/x-lzop", "application/x-snappy-framed", "application/x-xz",
"application/x-compress", "application/x-compress", "application/zstd",
"application/x-7z-compressed", "application/x-7z-compressed",
"application/x-ace-compressed", "application/x-astrotite-afa",
"application/x-alz-compressed", "application/x-freearc", "application/x-arj",
"application/x-b1", "application/vnd.ms-cab-compressed",
"application/x-cfs-compressed", "application/x-dar", "application/x-dgc-compressed",
"application/x-apple-diskimage", "application/x-gca-compressed",
"application/x-lzh", "application/x-lzx", "application/x-rar-compressed",
"application/x-stuffit", "application/x-stuffitx", "application/x-gtar",
"application/x-ms-wim", "application/x-xar", "application/zip", "application/x-zoo",
}
func (params *GopherTypeCharParams) GopherTypeChar(path string, isDir bool) string {
switch {
case strings.HasPrefix(path, "http://") || strings.HasPrefix(path, "https://"):
return "h"
case strings.HasPrefix(path, "telnet://"):
return "8"
}
extension := filepath.Ext(path)
if isDir || slices.Contains(params.GophermapFiles, filepath.Base(path)) || slices.Contains(params.GophermapExtensions, extension) {
return "1"
}
if slices.Contains(params.PromptFileExtensions, extension) {
return "7"
}
if !areAdditionalMimeTypesRegistered {
for _, ext := range plainTextExtensions {
mime.AddExtensionType(ext, "text/plain")
}
areAdditionalMimeTypesRegistered = true
}
mimeType := mime.TypeByExtension(extension)
switch {
case strings.HasPrefix(mimeType, "text/plain"):
return "0"
case slices.Contains(archives, mimeType):
return "5"
case mimeType == "image/gif":
return "g"
case strings.HasPrefix(mimeType, "image/"):
return "I"
case strings.HasPrefix(mimeType, "audio/"):
return "s"
case mimeType == "application/pdf":
return "P"
case strings.HasPrefix(mimeType, "text/html"):
return "h"
case strings.HasPrefix(mimeType, "text/rtf"):
return "r"
case strings.HasPrefix(mimeType, "text/xml") || strings.HasPrefix(mimeType, "application/xml"):
return "X"
default:
return "9"
}
}
+114
View File
@@ -0,0 +1,114 @@
package main
import(
"fmt"
"os"
"io"
"strings"
"strconv"
p "github.com/mcecode/gemtext-parser"
)
type State struct {
out io.Writer
maxLineLength int
isInCodeBlock bool
wasPreviousLink bool
}
func main() {
if len(os.Args) < 3 {
fmt.Printf("Usage: %s input.gmi output_gophermap [max line length (default 69)]\n", os.Args[0])
return
}
var asl p.ASL
var err error
if os.Args[1] != "-" {
asl, err = p.ParseFile(os.Args[1])
if err != nil {
fmt.Fprintln(os.Stderr, err)
return
}
} else {
stdin, err := io.ReadAll(os.Stdin)
if err != nil {
fmt.Fprintln(os.Stderr, err)
return
}
asl = p.Parse(string(stdin))
}
var outputWriter io.Writer
if os.Args[2] != "-" {
file, err := os.Create(os.Args[2])
if err != nil {
fmt.Fprintln(os.Stderr, err)
return
}
defer file.Close()
outputWriter = file
} else {
outputWriter = os.Stdout
}
s := State{outputWriter, 69, false, false}
if len(os.Args) >= 4 {
maxLineLength, err := strconv.Atoi(os.Args[3])
if err != nil {
fmt.Fprintln(os.Stderr, err)
return
}
s.maxLineLength = maxLineLength
}
asl.Visit(s.handleElement)
}
var gopherTypeCharParams = GopherTypeCharParams{
GophermapFiles: []string{"gophermap"},
GophermapExtensions: []string{},
PromptFileExtensions: []string{},
}
func (s *State) handleElement(e p.Element) {
switch (e.Type()) {
case p.ElementTypes.Text():
if e.Text() == "" { return }
if !s.isInCodeBlock { s.out.Write([]byte("\n")) }
s.out.Write([]byte(JoinGophermapLines("i", WrapLines(e.Text(), s.maxLineLength, " "), "\t/FAKE\tNULL\t0")))
s.wasPreviousLink = false
case p.ElementTypes.Link():
if !s.wasPreviousLink {
s.out.Write([]byte("\n"))
}
isUrl := strings.Contains(e.URL(), "://")
linkPrefix := ""
if isUrl {
linkPrefix = "URL:"
}
typeChar := gopherTypeCharParams.GopherTypeChar(e.URL(), strings.HasSuffix(e.URL(), "/"))
s.out.Write([]byte(typeChar + e.Text() + "\t" + linkPrefix + e.URL()))
s.wasPreviousLink = true
case p.ElementTypes.Heading():
s.out.Write([]byte("\n"))
if e.Level() == p.HeadingLevels.Heading() || e.Level() == p.HeadingLevels.SubHeading() {
s.out.Write([]byte("\n\n"))
}
if e.Level() == p.HeadingLevels.SubSubHeading() {
s.out.Write([]byte("\n"))
}
s.out.Write([]byte(JoinGophermapLines("i", WrapLines(e.Text(), s.maxLineLength, " "), "\t/FAKE\tNULL\t0")))
s.wasPreviousLink = false
case p.ElementTypes.List():
s.out.Write([]byte(JoinGophermapLines("i- ", WrapLines(e.Text(), s.maxLineLength - 2, " "), "\t/FAKE\tNULL\t0")))
s.wasPreviousLink = false
case p.ElementTypes.Quote():
s.out.Write([]byte(JoinGophermapLines("i> ", WrapLines(e.Text(), s.maxLineLength - 2, " "), "\t/FAKE\tNULL\t0")))
s.wasPreviousLink = false
case p.ElementTypes.PreformatToggle():
s.isInCodeBlock = !s.isInCodeBlock
s.wasPreviousLink = false
}
s.out.Write([]byte("\n"))
}
+40
View File
@@ -0,0 +1,40 @@
package main
import (
"strings"
)
// wrapLines returns a slice with every element representing a new line
func WrapLines(input string, maxLineLength int, breakupDelimiter string) []string {
breakupDelimiterLength := len([]rune(breakupDelimiter))
output := []string{}
previousExistingLineIndex := -1
for existingLineIndex, existingLine := range strings.Split(input, "\n") {
words := strings.Split(existingLine, breakupDelimiter)
length := 0
for _, word := range words {
wordLength := len([]rune(word))
length += breakupDelimiterLength + wordLength
if length > maxLineLength || len(output) == 0 || previousExistingLineIndex != existingLineIndex {
output = append(output, word)
length = wordLength
} else {
output[len(output) - 1] += breakupDelimiter + word
}
previousExistingLineIndex = existingLineIndex
}
}
return output
}
func JoinGophermapLines(typeCharacter string, lines []string, lineEnding string) string {
out := ""
for index, line := range lines {
out += typeCharacter + line + lineEnding
if index < len(lines) - 1 { out += "\n" }
}
return out
}