parser

package module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 29, 2025 License: Apache-2.0 Imports: 5 Imported by: 0

README

Gemtext Parser

A library for parsing, transforming, generating, and converting gemtext.

If you're unfamiliar with gemtext, you can read the quick introduction or the specification.

For the full documentation, go to https://pkg.go.dev/github.com/mcecode/gemtext-parser.

Installation

go get github.com/mcecode/gemtext-parser

Usage

Convert gemtext to HTML
Input (testdata/example.gmi)
# Some Heading
Some text...
=> gemini://example.org Some link
main.go
package main

import (
	"fmt"

	p "github.com/mcecode/gemtext-parser"
)

func main() {
	asl, _ := p.ParseFile("testdata/example.gmi")
	html := p.ToHTML(asl)
	fmt.Print(html)
}
Output
<h1>Some Heading</h1>
<p>Some text...</p>
<p><a href="gemini://example.org">Some link</a></p>
Transform gemtext
Input (testdata/example.gmi)
# Some Heading
Some text...
=> gemini://example.org Some link
main.go
package main

import (
	"fmt"
	"strings"

	p "github.com/mcecode/gemtext-parser"
)

func main() {
	asl, _ := p.ParseFile("testdata/example.gmi")

	asl.DeleteAll(func(e p.Element) bool {
		if e.Type() == p.ElementTypes.Text() {
			return true
		}

		return false
	})
	asl.InsertAfter(
		[]p.Element{p.NewHeading("Some Subheading", p.HeadingLevels.SubHeading())},
		func(e p.Element) bool {
			if e.Type() == p.ElementTypes.Heading() &&
				e.Level() == p.HeadingLevels.Heading() {
				return true
			}

			return false
		},
	)
	asl.Replace(
		[]p.Element{p.NewLink("Some other link", "gemini://example.com")},
		func(e p.Element) bool {
			if e.Type() == p.ElementTypes.Link() &&
				strings.HasSuffix(e.URL(), ".org") {
				return true
			}

			return false
		},
	)

	gemtext := p.ToGemtext(asl)
	fmt.Print(gemtext)
}
Output
# Some Heading
## Some Subheading
=> gemini://example.com Some other link
Programmatically generate gemtext
main.go
package main

import (
	"fmt"

	p "github.com/mcecode/gemtext-parser"
)

func main() {
	asl := p.NewASL(
		p.NewHeading("Main Heading", p.HeadingLevels.Heading()),
		p.NewText("Lorem ipsum..."),
		p.NewHeading("Subheading", p.HeadingLevels.Heading()),
		p.NewText("Lorem ipsum..."),
		p.NewLink("Sample link", "gemini://example.org"),
	)
	gemtext := p.ToGemtext(asl)
	fmt.Print(gemtext)
}
Output
# Main Heading
Lorem ipsum...
# Subheading
Lorem ipsum...
=> gemini://example.org Sample link

License

Copyright 2025-present Matthew Espino

This project is licensed under the Apache 2.0 license.

Documentation

Overview

Package parser implements gemtext specification concepts by defining various structures and methods for manipulating gemtext.

To represent a gemtext document which is line-oriented, flat, and nonhierarchical, this package uses an Abstract Syntax List (ASL) instead of the usual Abstract Syntax Tree (AST). Under the hood, ASLs are just slices.

There are different ways to use this package. You can parse a gemtext document using Parse or ParseFile. From there, you can convert it to HTML with ToHTML or ToHTMLFile, or transform it using the different ASL methods before generating a new gemtext document with ToGemtext or ToGemtextFile. You may even want to programmatically create a gemtext document by passing the different Element types, Text, Link, Heading, List, Quote, and PreformatToggle, to NewASL.

Index

Examples

Constants

This section is empty.

Variables

View Source
var ElementTypes = elementTypes{
	// contains filtered or unexported fields
}

ElementTypes exposes the different element types:

p.ElementTypes.Text()
p.ElementTypes.Link()
p.ElementTypes.Heading()
p.ElementTypes.List()
p.ElementTypes.Quote()
p.ElementTypes.PreformatToggle()

Their names can be accessed as strings:

p.ElementTypes.Text().Name() // Text

It can be used to check what type an Element is:

func example(e p.Element) {
	if e.Type() == p.ElementTypes.Text() {
		// Do something
	}
}

-

View Source
var HeadingLevels = headingLevels{
	// contains filtered or unexported fields
}

HeadingLevels exposes the different heading levels:

p.HeadingLevels.Heading()
p.HeadingLevels.SubHeading()
p.HeadingLevels.SubSubHeading()
p.HeadingLevels.NotHeading()

Their values can be accessed as int8:

p.HeadingLevels.Heading().Value() // 1

It can be used to check what level a Heading is:

func example(h p.Heading) {
	if h.Level() == p.HeadingLevels.Heading() {
		// Do something
	}
}

Functions

func ToGemtext

func ToGemtext(asl ASL) string

ToGemtext turns asl into gemtext.

Example
package main

import (
	"fmt"

	p "github.com/mcecode/gemtext-parser"
)

func main() {
	asl := p.NewASL(
		p.NewHeading("Some Heading", p.HeadingLevels.Heading()),
		p.NewText("Some text..."),
		p.NewLink("Some link", "gemini://example.org"),
	)
	gemtext := p.ToGemtext(asl)
	fmt.Println(gemtext)
}
Output:

# Some Heading
Some text...
=> gemini://example.org Some link

func ToGemtextFile

func ToGemtextFile(name string, asl ASL, perm os.FileMode) error

ToGemtextFile turns asl into gemtext, then writes it into the named file. Returns an error if there's an error while writing the file.

Example
package main

import (
	p "github.com/mcecode/gemtext-parser"
)

func main() {
	asl := p.NewASL(
		p.NewHeading("Some Heading", p.HeadingLevels.Heading()),
		p.NewText("Some text..."),
		p.NewLink("Some link", "gemini://example.org"),
	)
	p.ToGemtextFile("testdata/example.gmi", asl, 0600)
}

func ToHTML

func ToHTML(asl ASL) string

ToHTML turns asl into HTML.

Example
package main

import (
	"fmt"

	p "github.com/mcecode/gemtext-parser"
)

func main() {
	asl := p.NewASL(
		p.NewHeading("Some Heading", p.HeadingLevels.Heading()),
		p.NewText("Some text..."),
		p.NewLink("Some link", "gemini://example.org"),
	)
	html := p.ToHTML(asl)
	fmt.Println(html)
}
Output:

<h1>Some Heading</h1>
<p>Some text...</p>
<p><a href="gemini://example.org">Some link</a></p>

func ToHTMLFile

func ToHTMLFile(name string, asl ASL, perm os.FileMode) error

ToHTMLFile turns asl into HTML, then writes it into the named file. Returns an error if there's an error while writing the file.

Example
package main

import (
	p "github.com/mcecode/gemtext-parser"
)

func main() {
	asl := p.NewASL(
		p.NewHeading("Some Heading", p.HeadingLevels.Heading()),
		p.NewText("Some text..."),
		p.NewLink("Some link", "gemini://example.org"),
	)
	p.ToGemtextFile("testdata/example.html", asl, 0600)
}

Types

type ASL

type ASL struct {
	// contains filtered or unexported fields
}

ASL represents an Abstract Syntax List of a gemtext document.

func NewASL

func NewASL(elems ...Element) ASL
Example
package main

import (
	"github.com/sanity-io/litter"

	p "github.com/mcecode/gemtext-parser"
)

var dumper = litter.Options{StripPackageNames: true, HidePrivateFields: false}

func main() {
	empty := p.NewASL()
	dumper.Dump(empty)

	asl := p.NewASL(p.NewText("Some text..."))
	dumper.Dump(asl)
}
Output:

ASL{
  elems: nil,
}
ASL{
  elems: []Element{
    Text{
      text: "Some text...",
    },
  },
}

func Parse

func Parse(s string) ASL

Parse parses s into an ASL.

Example
package main

import (
	"fmt"

	"github.com/sanity-io/litter"

	p "github.com/mcecode/gemtext-parser"
)

var dumper = litter.Options{StripPackageNames: true, HidePrivateFields: false}

func main() {
	s := fmt.Sprint(
		"# Some Heading\n",
		"Some text...\n",
		"=> gemini://example.org Some link\n",
	)
	asl := p.Parse(s)
	dumper.Dump(asl)
}
Output:

ASL{
  elems: []Element{
    Heading{
      text: "Some Heading",
      level: headingLevel{
        value: 1,
      },
    },
    Text{
      text: "Some text...",
    },
    Link{
      text: "Some link",
      url: "gemini://example.org",
    },
  },
}

func ParseFile

func ParseFile(name string) (ASL, error)

ParseFile reads the named file, then parses it into an ASL. Returns an error if there's an error while reading the file.

Example

Contents of testdata/example.gmi:

# Some Heading
Some text...
=> gemini://example.org Some link
package main

import (
	"github.com/sanity-io/litter"

	p "github.com/mcecode/gemtext-parser"
)

var dumper = litter.Options{StripPackageNames: true, HidePrivateFields: false}

func main() {
	asl, _ := p.ParseFile("testdata/example.gmi")
	dumper.Dump(asl)
}
Output:

ASL{
  elems: []Element{
    Heading{
      text: "Some Heading",
      level: headingLevel{
        value: 1,
      },
    },
    Text{
      text: "Some text...",
    },
    Link{
      text: "Some link",
      url: "gemini://example.org",
    },
  },
}

func (*ASL) Clone

func (asl *ASL) Clone() ASL

Clone returns a clone of asl.

Example
package main

import (
	"github.com/sanity-io/litter"

	p "github.com/mcecode/gemtext-parser"
)

var dumper = litter.Options{StripPackageNames: true, HidePrivateFields: false}

func main() {
	asl := p.NewASL(p.NewText("Some text..."))
	dumper.Dump(asl)

	clone := asl.Clone()
	dumper.Dump(clone)
}
Output:

ASL{
  elems: []Element{
    Text{
      text: "Some text...",
    },
  },
}
ASL{
  elems: []Element{
    Text{
      text: "Some text...",
    },
  },
}

func (*ASL) DeleteAll

func (asl *ASL) DeleteAll(f func(Element) bool)

DeleteAll deletes all elements where f returns true. Does nothing if f returns false for all elements.

Example
package main

import (
	"github.com/sanity-io/litter"

	p "github.com/mcecode/gemtext-parser"
)

var dumper = litter.Options{StripPackageNames: true, HidePrivateFields: false}

func main() {
	asl := p.NewASL(
		p.NewHeading("Some Heading", p.HeadingLevels.Heading()),
		p.NewText("Some text..."),
		p.NewLink("Some link", "gemini://example.org"),
	)
	asl.DeleteAll(func(e p.Element) bool {
		if e.Type() == p.ElementTypes.Heading() ||
			e.Type() == p.ElementTypes.Link() {
			return true
		}

		return false
	})
	dumper.Dump(asl)
}
Output:

ASL{
  elems: []Element{
    Text{
      text: "Some text...",
    },
  },
}

func (*ASL) DeleteFirst

func (asl *ASL) DeleteFirst(n uint) error

DeleteFirst deletes the first n elements of the list. Returns an error if n is larger than the number of elements asl currently has.

Example
package main

import (
	"github.com/sanity-io/litter"

	p "github.com/mcecode/gemtext-parser"
)

var dumper = litter.Options{StripPackageNames: true, HidePrivateFields: false}

func main() {
	asl := p.NewASL(
		p.NewHeading("Some Heading", p.HeadingLevels.Heading()),
		p.NewText("Some text..."),
		p.NewLink("Some link", "gemini://example.org"),
	)
	asl.DeleteFirst(2)
	dumper.Dump(asl)
}
Output:

ASL{
  elems: []Element{
    Link{
      text: "Some link",
      url: "gemini://example.org",
    },
  },
}

func (*ASL) DeleteLast

func (asl *ASL) DeleteLast(n uint) error

DeleteLast deletes the last n elements of the list. Returns an error if n is larger than the number of elements asl currently has.

Example
package main

import (
	"github.com/sanity-io/litter"

	p "github.com/mcecode/gemtext-parser"
)

var dumper = litter.Options{StripPackageNames: true, HidePrivateFields: false}

func main() {
	asl := p.NewASL(
		p.NewHeading("Some Heading", p.HeadingLevels.Heading()),
		p.NewText("Some text..."),
		p.NewLink("Some link", "gemini://example.org"),
	)
	asl.DeleteLast(2)
	dumper.Dump(asl)
}
Output:

ASL{
  elems: []Element{
    Heading{
      text: "Some Heading",
      level: headingLevel{
        value: 1,
      },
    },
  },
}

func (*ASL) InsertAfter

func (asl *ASL) InsertAfter(elems []Element, f func(Element) bool)

InsertAfter inserts elems after the first element where f returns true. Does nothing if f returns false for all elements.

Example
package main

import (
	"github.com/sanity-io/litter"

	p "github.com/mcecode/gemtext-parser"
)

var dumper = litter.Options{StripPackageNames: true, HidePrivateFields: false}

func main() {
	asl := p.NewASL(
		p.NewHeading("Some Heading", p.HeadingLevels.Heading()),
		p.NewText("Some text..."),
		p.NewLink("Some link", "gemini://example.org"),
	)
	asl.InsertAfter(
		[]p.Element{p.NewQuote("Some quote")},
		func(e p.Element) bool {
			if e.Type() == p.ElementTypes.Text() {
				return true
			}

			return false
		},
	)
	dumper.Dump(asl)
}
Output:

ASL{
  elems: []Element{
    Heading{
      text: "Some Heading",
      level: headingLevel{
        value: 1,
      },
    },
    Text{
      text: "Some text...",
    },
    Quote{
      text: "Some quote",
    },
    Link{
      text: "Some link",
      url: "gemini://example.org",
    },
  },
}

func (*ASL) InsertBefore

func (asl *ASL) InsertBefore(elems []Element, f func(Element) bool)

InsertBefore inserts elems before the first element where f returns true. Does nothing if f returns false for all elements.

Example
package main

import (
	"github.com/sanity-io/litter"

	p "github.com/mcecode/gemtext-parser"
)

var dumper = litter.Options{StripPackageNames: true, HidePrivateFields: false}

func main() {
	asl := p.NewASL(
		p.NewHeading("Some Heading", p.HeadingLevels.Heading()),
		p.NewText("Some text..."),
		p.NewLink("Some link", "gemini://example.org"),
	)
	asl.InsertBefore(
		[]p.Element{p.NewQuote("Some quote")},
		func(e p.Element) bool {
			if e.Type() == p.ElementTypes.Text() {
				return true
			}

			return false
		},
	)
	dumper.Dump(asl)
}
Output:

ASL{
  elems: []Element{
    Heading{
      text: "Some Heading",
      level: headingLevel{
        value: 1,
      },
    },
    Quote{
      text: "Some quote",
    },
    Text{
      text: "Some text...",
    },
    Link{
      text: "Some link",
      url: "gemini://example.org",
    },
  },
}

func (*ASL) InsertFirst

func (asl *ASL) InsertFirst(elems ...Element)

InsertFirst inserts elems at the start of the list.

Example
package main

import (
	"github.com/sanity-io/litter"

	p "github.com/mcecode/gemtext-parser"
)

var dumper = litter.Options{StripPackageNames: true, HidePrivateFields: false}

func main() {
	asl := p.NewASL(p.NewText("Some text..."))
	asl.InsertFirst(
		p.NewHeading("Some Heading", p.HeadingLevels.Heading()),
		p.NewText(""),
	)
	dumper.Dump(asl)
}
Output:

ASL{
  elems: []Element{
    Heading{
      text: "Some Heading",
      level: headingLevel{
        value: 1,
      },
    },
    Text{
      text: "",
    },
    Text{
      text: "Some text...",
    },
  },
}

func (*ASL) InsertLast

func (asl *ASL) InsertLast(elems ...Element)

InsertLast inserts elems at the end of the list.

Example
package main

import (
	"github.com/sanity-io/litter"

	p "github.com/mcecode/gemtext-parser"
)

var dumper = litter.Options{StripPackageNames: true, HidePrivateFields: false}

func main() {
	asl := p.NewASL(p.NewText("Some text..."))
	asl.InsertLast(
		p.NewText(""),
		p.NewLink("Some link", "gemini://example.org"),
	)
	dumper.Dump(asl)
}
Output:

ASL{
  elems: []Element{
    Text{
      text: "Some text...",
    },
    Text{
      text: "",
    },
    Link{
      text: "Some link",
      url: "gemini://example.org",
    },
  },
}

func (*ASL) Replace

func (asl *ASL) Replace(elems []Element, f func(Element) bool)

Replace replaces the first element where f returns true with elems. Does nothing if f returns false for all elements.

Example
package main

import (
	"github.com/sanity-io/litter"

	p "github.com/mcecode/gemtext-parser"
)

var dumper = litter.Options{StripPackageNames: true, HidePrivateFields: false}

func main() {
	asl := p.NewASL(
		p.NewHeading("Some Heading", p.HeadingLevels.Heading()),
		p.NewText("Some text..."),
		p.NewLink("Some link", "gemini://example.org"),
	)
	asl.Replace(
		[]p.Element{p.NewText("Some other text...")},
		func(e p.Element) bool {
			if e.Type() == p.ElementTypes.Text() {
				return true
			}

			return false
		},
	)
	dumper.Dump(asl)
}
Output:

ASL{
  elems: []Element{
    Heading{
      text: "Some Heading",
      level: headingLevel{
        value: 1,
      },
    },
    Text{
      text: "Some other text...",
    },
    Link{
      text: "Some link",
      url: "gemini://example.org",
    },
  },
}

func (*ASL) Visit

func (asl *ASL) Visit(f func(Element))

Visit iterates over all elements that asl has, passing each element to f.

Example
package main

import (
	"fmt"

	p "github.com/mcecode/gemtext-parser"
)

func main() {
	asl := p.NewASL(
		p.NewHeading("Some Heading", p.HeadingLevels.Heading()),
		p.NewText("Some text..."),
		p.NewLink("Some link", "gemini://example.org"),
	)
	asl.Visit(func(e p.Element) {
		fmt.Printf("Got a %s!\n", e.Type().Name())
	})
}
Output:

Got a Heading!
Got a Text!
Got a Link!

type Element

type Element interface {
	Type() elementType
	Text() string
	URL() string
	Level() headingLevel
}

Element is the interface that all elements implement. It represents an abstraction of the different gemtext line types.

Type() returns the type of the element. It can be compared with ElementTypes:

func example(e p.Element) {
	if e.Type() == p.ElementTypes.Text() {
		// Do something
	}
}

Text() returns any text associated with the element. For Text, Heading, List, and Quote, it returns the main text of the element. For Link, it returns the user-friendly link name. For PreformatToggle, it returns the alt text.

URL() returns a URL associated with the element. Only Link actually returns a URL, other elements will return an empty string.

Level() returns the heading level associated with the element. Only Heading actually returns a heading level, other elements will return HeadingLevels.NotHeading(). It can be compared with HeadingLevels:

func example(h p.Heading) {
	if h.Level() == p.HeadingLevels.Heading() {
		// Do something
	}
}

type Heading

type Heading struct {
	// contains filtered or unexported fields
}

Heading represents a heading line.

func NewHeading

func NewHeading(text string, level headingLevel) Heading
Example
package main

import (
	"fmt"

	p "github.com/mcecode/gemtext-parser"
)

func main() {
	h := p.NewHeading("Example Heading", p.HeadingLevels.SubHeading())
	fmt.Println(h.Type().Name())
	fmt.Println(h.Text())
	fmt.Println(h.Level().Value())
}
Output:

Heading
Example Heading
2

func (Heading) Level

func (h Heading) Level() headingLevel

func (Heading) Text

func (h Heading) Text() string

func (Heading) Type

func (h Heading) Type() elementType

func (Heading) URL

func (h Heading) URL() string
type Link struct {
	// contains filtered or unexported fields
}

Link represents a link line.

func NewLink(text string, url string) Link

func (Link) Level

func (l Link) Level() headingLevel

func (Link) Text

func (l Link) Text() string

func (Link) Type

func (l Link) Type() elementType

func (Link) URL

func (l Link) URL() string

type List

type List struct {
	// contains filtered or unexported fields
}

List represents a list item.

func NewList

func NewList(text string) List
Example
package main

import (
	"fmt"

	p "github.com/mcecode/gemtext-parser"
)

func main() {
	l := p.NewList("Example List Item")
	fmt.Println(l.Type().Name())
	fmt.Println(l.Text())
}
Output:

List
Example List Item

func (List) Level

func (l List) Level() headingLevel

func (List) Text

func (l List) Text() string

func (List) Type

func (l List) Type() elementType

func (List) URL

func (l List) URL() string

type PreformatToggle

type PreformatToggle struct {
	// contains filtered or unexported fields
}

PreformatToggle represents a preformat toggle line.

func NewPreformatToggle

func NewPreformatToggle(text string) PreformatToggle
Example
package main

import (
	"fmt"

	p "github.com/mcecode/gemtext-parser"
)

func main() {
	pt := p.NewPreformatToggle("Example Alt Text")
	fmt.Println(pt.Type().Name())
	fmt.Println(pt.Text())
}
Output:

PreformatToggle
Example Alt Text

func (PreformatToggle) Level

func (pt PreformatToggle) Level() headingLevel

func (PreformatToggle) Text

func (pt PreformatToggle) Text() string

func (PreformatToggle) Type

func (pt PreformatToggle) Type() elementType

func (PreformatToggle) URL

func (pt PreformatToggle) URL() string

type Quote

type Quote struct {
	// contains filtered or unexported fields
}

Quote represents a quote line.

func NewQuote

func NewQuote(text string) Quote
Example
package main

import (
	"fmt"

	p "github.com/mcecode/gemtext-parser"
)

func main() {
	q := p.NewQuote("Example Quote")
	fmt.Println(q.Type().Name())
	fmt.Println(q.Text())
}
Output:

Quote
Example Quote

func (Quote) Level

func (q Quote) Level() headingLevel

func (Quote) Text

func (q Quote) Text() string

func (Quote) Type

func (q Quote) Type() elementType

func (Quote) URL

func (q Quote) URL() string

type Text

type Text struct {
	// contains filtered or unexported fields
}

Text represents a text line.

func NewText

func NewText(text string) Text
Example
package main

import (
	"fmt"

	p "github.com/mcecode/gemtext-parser"
)

func main() {
	t := p.NewText("Example Text")
	fmt.Println(t.Type().Name())
	fmt.Println(t.Text())
}
Output:

Text
Example Text

func (Text) Level

func (t Text) Level() headingLevel

func (Text) Text

func (t Text) Text() string

func (Text) Type

func (t Text) Type() elementType

func (Text) URL

func (t Text) URL() string

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL