diff --git a/README.md b/README.md
index 7609910..0b6fd62 100644
--- a/README.md
+++ b/README.md
@@ -40,6 +40,9 @@ The renderer will also treat lists of links and paragraphs consisting of
links only the special way: it will render only the links block for
them.
+To get a better idea of how source Markdown looks like after the
+conversion to Gemtext, see [testdata](testdata) directory.
+
[gomarkdown]: https://github.com/gomarkdown/markdown
## gmnhg
diff --git a/go.mod b/go.mod
index 79477f0..89b7e44 100644
--- a/go.mod
+++ b/go.mod
@@ -7,8 +7,10 @@ require (
github.com/Masterminds/sprig/v3 v3.2.2
github.com/gomarkdown/markdown v0.0.0-20210915032930-fe0e174ee09a
github.com/google/uuid v1.3.0 // indirect
+ github.com/hexops/gotextdiff v1.0.3
github.com/huandu/xstrings v1.3.2 // indirect
github.com/imdario/mergo v0.3.12 // indirect
+ github.com/kr/pretty v0.1.0 // indirect
github.com/mattn/go-runewidth v0.0.13 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/niklasfasching/go-org v1.5.0
@@ -16,5 +18,6 @@ require (
github.com/spf13/cast v1.4.1 // indirect
golang.org/x/crypto v0.0.0-20210915214749-c084706c2272 // indirect
golang.org/x/net v0.0.0-20210917163549-3c21e5b27794 // indirect
+ gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
gopkg.in/yaml.v2 v2.4.0
)
diff --git a/go.sum b/go.sum
index abcbd21..d3ede79 100644
--- a/go.sum
+++ b/go.sum
@@ -21,12 +21,19 @@ github.com/gomarkdown/markdown v0.0.0-20210915032930-fe0e174ee09a/go.mod h1:JDGc
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
+github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
github.com/huandu/xstrings v1.3.2 h1:L18LIDzqlW6xN2rEkpdV8+oL/IXWJ1APd+vsdYy4Wdw=
github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU=
github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
+github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
@@ -81,8 +88,9 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
-gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
+gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
diff --git a/internal/gmnhg/org.go b/internal/gmnhg/org.go
index e41b9bb..5e62eeb 100644
--- a/internal/gmnhg/org.go
+++ b/internal/gmnhg/org.go
@@ -17,6 +17,7 @@ package gmnhg
import (
"bytes"
+ "errors"
"fmt"
"reflect"
"regexp"
@@ -48,6 +49,8 @@ func parseValue(value interface{}) interface{} {
return value
}
+var errKeyNotFound = errors.New("cannot find tagged key in struct")
+
// for key "key" will set either map key "key" or struct field tagged
// `tag:"key"` with value; expects a pointer
func reflectSetKey(mapOrStruct interface{}, tag, key string, value interface{}) (err error) {
@@ -75,7 +78,7 @@ func reflectSetKey(mapOrStruct interface{}, tag, key string, value interface{})
fieldName = field.Name
}
if fieldName == "" {
- return fmt.Errorf("cannot find tag %v with key %v in struct", tag, key)
+ return fmt.Errorf("%v: %v %w", tag, key, errKeyNotFound)
}
v.FieldByName(fieldName).Set(reflect.ValueOf(parseValue(value)))
default:
@@ -103,7 +106,7 @@ func unmarshalORG(data []byte, p interface{}) (err error) {
} else if k == "date" {
value = parseORGDate(v)
}
- if err := reflectSetKey(p, "org", strings.ToLower(key), value); err != nil {
+ if err := reflectSetKey(p, "org", strings.ToLower(key), value); err != nil && !errors.Is(err, errKeyNotFound) {
return err
}
}
diff --git a/internal/gmnhg/post.go b/internal/gmnhg/post.go
index 8e9b9d5..4747574 100644
--- a/internal/gmnhg/post.go
+++ b/internal/gmnhg/post.go
@@ -62,7 +62,7 @@ var (
yamlDelimiter = []byte("---\n")
tomlDelimiter = []byte("+++\n")
jsonObjectRegex = regexp.MustCompile(`\A(\{[\s\S]*\})\n\n`)
- orgModeRegex = regexp.MustCompile(`\A((?:#\+\w+: ?\S*\n)*)`)
+ orgModeRegex = regexp.MustCompile(`\A((?:#\+\w+\[?\]?: ?[^\n\r]*\n)+)`)
)
// ParseMetadata extracts TOML/JSON/YAML/org-mode format front matter
@@ -109,7 +109,7 @@ func ParseMetadata(source []byte) (markdown []byte, metadata Metadata) {
if err := json.Unmarshal(metadataContent, &metadata); err != nil {
return
}
- markdown = source[blockEnd+1:] // JSON end + \n\n - 1
+ markdown = source[blockEnd:]
} else if match := orgModeRegex.FindIndex(source); match != nil {
blockEnd = match[1]
metadataContent = source[:blockEnd]
diff --git a/render_test.go b/render_test.go
new file mode 100644
index 0000000..c4b0930
--- /dev/null
+++ b/render_test.go
@@ -0,0 +1,77 @@
+// This file is part of gmnhg.
+
+// gmnhg is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// gmnhg is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License
+// along with gmnhg. If not, see .
+
+package gemini
+
+import (
+ "bytes"
+ "io/ioutil"
+ "os"
+ "path"
+ "regexp"
+ "testing"
+
+ "github.com/hexops/gotextdiff"
+ "github.com/hexops/gotextdiff/myers"
+ "github.com/hexops/gotextdiff/span"
+ "github.com/tdemin/gmnhg/internal/gmnhg"
+)
+
+var fileList []string
+
+var (
+ mdFilenameRegex = regexp.MustCompile(`^(.+)\.md$`)
+)
+
+func TestMain(m *testing.M) {
+ // go test implicitly sets cwd to tested package directory; sadly,
+ // this fact is undocumented
+ files, err := ioutil.ReadDir("testdata")
+ if err != nil {
+ panic(err)
+ }
+ for _, fileInfo := range files {
+ if match := mdFilenameRegex.FindStringSubmatch(fileInfo.Name()); !fileInfo.IsDir() && match != nil {
+ fileList = append(fileList, match[1])
+ }
+ }
+ os.Exit(m.Run())
+}
+
+func TestRenderer(t *testing.T) {
+ for _, testName := range fileList {
+ t.Logf("testing %s", testName)
+ mdContents, err := ioutil.ReadFile(path.Join("testdata", testName+".md"))
+ if err != nil {
+ t.Fatalf("failed to open Markdown test %s: %v", testName, err)
+ }
+ gmiContents, err := ioutil.ReadFile(path.Join("testdata", testName+".gmi"))
+ if err != nil {
+ t.Logf("%s: cannot open Gemtext file, skipping: %v", testName, err)
+ continue
+ }
+ content, _ := gmnhg.ParseMetadata(mdContents)
+ geminiContent, err := RenderMarkdown(content, Defaults)
+ if err != nil {
+ t.Errorf("failed to convert %s Markdown to Gemtext: %v", testName, err)
+ }
+ if !bytes.Equal(geminiContent, gmiContents) {
+ diff := myers.ComputeEdits(span.URIFromPath("a.gmi"),
+ string(geminiContent), string(gmiContents))
+ t.Errorf("content mismatch on %s, diff:\n%s", testName,
+ gotextdiff.ToUnified("a.gmi", "b.gmi", string(geminiContent), diff))
+ }
+ }
+}
diff --git a/testdata/front_matter_json.gmi b/testdata/front_matter_json.gmi
new file mode 100644
index 0000000..8c5abe4
--- /dev/null
+++ b/testdata/front_matter_json.gmi
@@ -0,0 +1 @@
+gmnhg test suite should parse the metadata above but disregard it.
diff --git a/testdata/front_matter_json.md b/testdata/front_matter_json.md
new file mode 100644
index 0000000..a5d0a44
--- /dev/null
+++ b/testdata/front_matter_json.md
@@ -0,0 +1,6 @@
+{
+ "title": "JSON front matter test",
+ "draft": true
+}
+
+gmnhg test suite should parse the metadata above but disregard it.
diff --git a/testdata/front_matter_org.gmi b/testdata/front_matter_org.gmi
new file mode 100644
index 0000000..8c5abe4
--- /dev/null
+++ b/testdata/front_matter_org.gmi
@@ -0,0 +1 @@
+gmnhg test suite should parse the metadata above but disregard it.
diff --git a/testdata/front_matter_org.md b/testdata/front_matter_org.md
new file mode 100644
index 0000000..20a840e
--- /dev/null
+++ b/testdata/front_matter_org.md
@@ -0,0 +1,5 @@
+#+title: "ORG front matter test"
+#+draft: true
+#+tags[]: org,gmnhg,emacs
+
+gmnhg test suite should parse the metadata above but disregard it.
diff --git a/testdata/front_matter_toml.gmi b/testdata/front_matter_toml.gmi
new file mode 100644
index 0000000..8c5abe4
--- /dev/null
+++ b/testdata/front_matter_toml.gmi
@@ -0,0 +1 @@
+gmnhg test suite should parse the metadata above but disregard it.
diff --git a/testdata/front_matter_toml.md b/testdata/front_matter_toml.md
new file mode 100644
index 0000000..ab568be
--- /dev/null
+++ b/testdata/front_matter_toml.md
@@ -0,0 +1,6 @@
++++
+title = "TOML front matter test"
+draft = true
++++
+
+gmnhg test suite should parse the metadata above but disregard it.
diff --git a/testdata/front_matter_yaml.gmi b/testdata/front_matter_yaml.gmi
new file mode 100644
index 0000000..8c5abe4
--- /dev/null
+++ b/testdata/front_matter_yaml.gmi
@@ -0,0 +1 @@
+gmnhg test suite should parse the metadata above but disregard it.
diff --git a/testdata/front_matter_yaml.md b/testdata/front_matter_yaml.md
new file mode 100644
index 0000000..794ffc3
--- /dev/null
+++ b/testdata/front_matter_yaml.md
@@ -0,0 +1,6 @@
+---
+title: "YAML front matter test"
+draft: true
+---
+
+gmnhg test suite should parse the metadata above but disregard it.
diff --git a/testdata/general_text.gmi b/testdata/general_text.gmi
new file mode 100644
index 0000000..326f879
--- /dev/null
+++ b/testdata/general_text.gmi
@@ -0,0 +1,89 @@
+# General text
+
+Paragraphs are printed verbatim in gmnhg.
+
+Single newlines (like in this multi-line paragraph) will get replaced by a space, as Gemini specification p. 5.4.1 recommends this for soft-wrapping text by clients.
+
+Inline formatting bits (like this **bold** text, *emphasized* text, ~~strikethrough~~ text, `preformatted text`) are kept to make sure Gemini readers still have the stylistic context of your text.
+
+## Blockquotes
+
+Newlines in blockquote paragraphs, unlike usual paragraphs, aren't replaced with a space. This facilitates appending authorship information to the quote, or using blockquotes to write poems.
+
+> "Never trouble another for what you can do yourself"
+> — Thomas Jefferson, 3rd president of the US
+
+> "Wow, writing comprehensive test suites is hard!"
+> — Timur Demin, while writing this very test file
+
+> "Somehow I know these two paragraphs will be broken into two separate
+> blockquotes by gmnhg. I think my knowledge of that comes from being
+> the author of this program."
+
+> — also Timur Demin, in the process of writing this test file
+
+## Code
+
+gmnhg will use Gemtext preformatted blocks for that. Markdown alt-text for preformatted blocks is supported, and is used to render alt-text as specified by Gemini spec p. 5.4.3.
+
+```go
+package main
+
+func main() {
+ println("gmnhg is awesome!")
+}
+```
+
+Preformatted Markdown of course isn't rendered:
+
+```
+# I am a test Markdown document
+
+I contain text in **bold**.
+```
+
+## Links
+
+gmnhg supports links, images, and footnotes. Links are a very interesting topic on itself; see a separate document for those.
+
+=> links.md separate document
+
+## Lists
+
+Definition lists, numbered and ordered lists are all supported in gmnhg. There's also a separate document displaying those.
+
+=> lists.md separate document
+
+## Tables
+
+Markdown tables are supported in gmnhg, and are better displayed by a separate document.
+
+=> tables.md separate document
+
+## Headings
+
+Gemini specification allows up to three heading levels, with an optional space after the last heading symbol, `#`. With Markdown, you get 6; gmnhg will simply print the relevant number of #-s, making the client up to parse more heading levels and keeping context of the source document.
+
+Since clients like Lagrange treat the fourth and the rest of #-s as heading content, it's best to avoid using H4-H6 in Gemini-aware Markdown entirely. Headings from H3 to H6 are provided below so you can test how your client handles that.
+
+### Heading 3
+
+#### Heading 4
+
+##### Heading 5
+
+###### Heading 6
+
+## Misc
+
+Inline HTML is currently stripped, but HTML contents remain on-screen. This may change in the future.
+
+> There's currently a bug in gmnhg which prevents it from
+> stripping HTML in certain scenarios. HTML is noticeably still present
+> inside blockquotes.
+
+=> https://github.com/tdemin/gmnhg/issues/6 bug in gmnhg
+
+---
+
+The Markdown horizontal line above is rendered as triple dashes.
diff --git a/testdata/general_text.md b/testdata/general_text.md
new file mode 100644
index 0000000..f9af9b9
--- /dev/null
+++ b/testdata/general_text.md
@@ -0,0 +1,102 @@
+# General text
+
+Paragraphs are printed verbatim in gmnhg.
+
+Single newlines (like in this multi-line paragraph) will get replaced by
+a space, as Gemini specification p. 5.4.1 recommends this for
+soft-wrapping text by clients.
+
+Inline formatting bits (like this **bold** text, _emphasized_ text,
+~~strikethrough~~ text, `preformatted text`) are kept to make sure
+Gemini readers still have the stylistic context of your text.
+
+## Blockquotes
+
+Newlines in blockquote paragraphs, unlike usual paragraphs, aren't
+replaced with a space. This facilitates appending authorship information
+to the quote, or using blockquotes to write poems.
+
+> "Never trouble another for what you can do yourself"
+> — Thomas Jefferson, 3rd president of the US
+
+> "Wow, writing comprehensive test suites is hard!"
+> — Timur Demin, while writing this very test file
+
+> "Somehow I know these two paragraphs will be broken into two separate
+> blockquotes by gmnhg. I think my knowledge of that comes from being
+> the author of this program."
+>
+> — also Timur Demin, in the process of writing this test file
+
+## Code
+
+gmnhg will use Gemtext preformatted blocks for that. Markdown alt-text
+for preformatted blocks is supported, and is used to render alt-text as
+specified by Gemini spec p. 5.4.3.
+
+```go
+package main
+
+func main() {
+ println("gmnhg is awesome!")
+}
+```
+
+Preformatted Markdown of course isn't rendered:
+
+```
+# I am a test Markdown document
+
+I contain text in **bold**.
+```
+
+## Links
+
+gmnhg supports links, images, and footnotes. Links are a very
+interesting topic on itself; see a [separate document](links.md) for
+those.
+
+## Lists
+
+Definition lists, numbered and ordered lists are all supported in gmnhg.
+There's also a [separate document](lists.md) displaying those.
+
+## Tables
+
+Markdown tables are supported in gmnhg, and are better displayed by a
+[separate document](tables.md).
+
+## Headings
+
+Gemini specification allows up to three heading levels, with an optional
+space after the last heading symbol, `#`. With Markdown, you get 6;
+gmnhg will simply print the relevant number of #-s, making the client up
+to parse more heading levels and keeping context of the source document.
+
+Since clients like Lagrange treat the fourth and the rest of #-s as
+heading content, it's best to avoid using H4-H6 in Gemini-aware Markdown
+entirely. Headings from H3 to H6 are provided below so you can test how
+your client handles that.
+
+### Heading 3
+
+#### Heading 4
+
+##### Heading 5
+
+###### Heading 6
+
+## Misc
+
+Inline HTML is currently stripped, but HTML
+contents remain on-screen. This may change in the future.
+
+> There's currently a [bug in gmnhg][bug] which prevents it from
+> stripping HTML in certain scenarios. HTML is noticeably still present
+> inside blockquotes.
+
+***
+
+The Markdown horizontal line above is rendered as triple dashes.
+
+[bug]: https://github.com/tdemin/gmnhg/issues/6
diff --git a/testdata/links.gmi b/testdata/links.gmi
new file mode 100644
index 0000000..ceef428
--- /dev/null
+++ b/testdata/links.gmi
@@ -0,0 +1,69 @@
+# Links
+
+gmnhg supports links, images, and footnotes. These are extracted from paragraphs and other block elements recursively.
+
+As there's no inline links in Gemtext, gmnhg instead renders links blocks after paragraphs. Links blocks are sorted by type: first footnotes, then images, then links, blocks of a distinct type separated by a single newline.
+
+## Inline links & images
+
+For inline Markdown links, the text inside the square brackets is used as link title: for instance, the link to Gemini specification along with a link to the current CommonMark spec will generate a block of two links.
+
+=> https://gemini.circumlunar.space/docs/specification.gmi Gemini specification
+=> https://spec.commonmark.org/0.30/ CommonMark spec
+
+gomarkdown works with reference-style links as well. Unused reference links are ignored.
+
+=> https://github.com/gomarkdown/markdown gomarkdown
+
+xkcd #1853 serves quite well as an inline image example.
+
+=> https://imgs.xkcd.com/comics/once_per_day.png xkcd #1853
+
+Other container elements can contain inline links as well. For instance, this is an example of a link inside a blockquote:
+
+> OTR has significant usability drawbacks for inter-client mobility.
+> — XEP-0384
+
+=> https://xmpp.org/extensions/xep-0384.html XEP-0384
+
+## Footnotes
+
+gmnhg supports footnotes, written like this[^1]. Footnotes can use any references, including alphanumeric ones[^2]; alphanumeric references will be replaced with numeric IDs on render.
+
+[^1]: Footnotes can only consist of a single source line due to a quirk of gomarkdown.
+[^2]: Footnotes can contain any kind of inline **formatting** paragraphs do. For instance, this is a link to GitHub.
+
+=> https://github.com GitHub
+
+This line looks like it would belong to footnote 1, but it actually doesn't, and is therefore treated as a new paragraph.
+
+## Lists of links
+
+gmnhg additionally supports a special kind of lists: lists consisting solely of links. For these, content rendering will be skipped entirely, and a links block will be rendered instead.
+
+### Markdown lists
+
+Links-only lists can be of any type, but they can only be of level 1. The two lists below will get rendered as links blocks:
+
+=> https://gemini.circumlunar.space/docs/specification.gmi Gemini specification
+=> https://github.com/tdemin/gmnhg gmnhg
+
+=> https://gemini.circumlunar.space/docs/best-practices.gmi Best practices for Gemini implementers
+=> https://gemini.circumlunar.space/docs/faq.gmi Project Gemini FAQ
+
+The list below contains other meaningful text in its items, and will get rendered as a regular list:
+
+* Gemini specification is a must-read for a Gemini developer.
+
+=> https://gemini.circumlunar.space/docs/specification.gmi Gemini specification
+
+### Series of links
+
+A series of inline links in a single paragraph, if the paragraph contains no extra meaningful symbols (aside from spaces and newlines), will also get rendered as a single links block:
+
+=> https://github.com/tdemin/gmnhg gmnhg
+=> https://gemini.circumlunar.space/docs/specification.gmi Gemini specification
+
+This also works for single-link paragraphs:
+
+=> https://gemini.circumlunar.space/docs/specification.gmi Gemini specification
diff --git a/testdata/links.md b/testdata/links.md
new file mode 100644
index 0000000..be307d7
--- /dev/null
+++ b/testdata/links.md
@@ -0,0 +1,80 @@
+# Links
+
+gmnhg supports links, images, and footnotes. These are extracted from
+paragraphs and other block elements recursively.
+
+As there's no inline links in Gemtext, gmnhg instead renders links
+blocks after paragraphs. Links blocks are sorted by type: first
+footnotes, then images, then links, blocks of a distinct type separated
+by a single newline.
+
+## Inline links & images
+
+For inline Markdown links, the text inside the square brackets is used
+as link title: for instance, the link to [Gemini specification][gemspec]
+along with a link to the current [CommonMark spec][cmark] will generate
+a block of two links.
+
+[gemspec]: https://gemini.circumlunar.space/docs/specification.gmi "This alt text will never be printed, as there's no tools in Gemtext for that"
+[cmark]: https://spec.commonmark.org/0.30/
+
+[gomarkdown](https://github.com/gomarkdown/markdown) works with
+reference-style links as well. Unused reference links are ignored.
+
+[gmnhg]: https://github.com/tdemin/gmnhg "This link will get entirely ignored"
+
+![xkcd #1853](https://imgs.xkcd.com/comics/once_per_day.png) serves
+quite well as an inline image example.
+
+Other container elements can contain inline links as well. For instance,
+this is an example of a link inside a blockquote:
+
+> OTR has significant usability drawbacks for inter-client mobility.
+> — [XEP-0384](https://xmpp.org/extensions/xep-0384.html)
+
+## Footnotes
+
+gmnhg supports footnotes, written like this[^1]. Footnotes can use any
+references, including alphanumeric ones[^foo]; alphanumeric references
+will be replaced with numeric IDs on render.
+
+[^1]: Footnotes can only consist of a single source line due to a quirk of gomarkdown.
+This line looks like it would belong to footnote 1, but it actually
+doesn't, and is therefore treated as a new paragraph.
+
+[^foo]: Footnotes can contain any kind of inline **formatting** paragraphs do. For instance, this is a link to [GitHub](https://github.com).
+
+## Lists of links
+
+gmnhg additionally supports a special kind of lists: lists consisting
+solely of links. For these, content rendering will be skipped entirely,
+and a links block will be rendered instead.
+
+### Markdown lists
+
+Links-only lists can be of any type, but they can only be of level 1.
+The two lists below will get rendered as links blocks:
+
+* [Gemini specification][gemspec]
+* [gmnhg](https://github.com/tdemin/gmnhg)
+
+1. [Best practices for Gemini implementers](https://gemini.circumlunar.space/docs/best-practices.gmi)
+2. [Project Gemini FAQ](https://gemini.circumlunar.space/docs/faq.gmi)
+
+The list below contains other meaningful text in its items, and will get
+rendered as a regular list:
+
+* [Gemini specification][gemspec] is a must-read for a Gemini developer.
+
+### Series of links
+
+A series of inline links in a single paragraph, if the paragraph
+contains no extra meaningful symbols (aside from spaces and newlines),
+will also get rendered as a single links block:
+
+[gmnhg](https://github.com/tdemin/gmnhg)
+[Gemini specification][gemspec]
+
+This also works for single-link paragraphs:
+
+[Gemini specification][gemspec]
diff --git a/testdata/lists.gmi b/testdata/lists.gmi
new file mode 100644
index 0000000..cca5ae5
--- /dev/null
+++ b/testdata/lists.gmi
@@ -0,0 +1,47 @@
+# Lists
+
+Definition lists, numbered and ordered lists are all supported in gmnhg.
+
+## Definition lists
+
+The lists of definitions get converted into regular unordered lists, prefixed with a star (`*`) as specified by Gemini spec p. 5.5.2.
+
+gmnhg
+* a program to generate a Gemini site from an existing Hugo site
+* a library converting Markdown to Gemtext, based on gomarkdown
+
+md2gmn
+* a program to convert Markdown to Gemtext
+* a wrapper to the gmnhg library
+
+## Normal lists
+
+* This is the first item of an unordered list.
+* This is its second item.
+* This is a list item that was using the `+` sign. Gemini readers should see this item as the continuation of the previous list.
+
+1. This is an ordered list first item.
+2. This is the second item.
+
+## Lists containing a sub-list
+
+As there's no indented list line type in Gemtext, gmnhg will indent these with tabs. The tabs number is equivalent to list level minus one (e.g. single tab for second list level).
+
+Unordered lists can be children of ordered lists, and vice versa.
+
+* This item contains a child ordered list.
+ 1. This ordered list item should get picked up as regular text.
+ 2. Whether or not this looks nicely depends on the client.
+* This item contains a child definition list.
+ Markdown
+ * an overly complex text markup format invented in 2004 whose sole specification of CommonMark lacks both tables and footnotes
+ * a text format that has zero parsers completely compatible between each other.
+
+1. This item contains a child unordered list.
+ * This whole list should get treated as plain text by clients.
+
+## Links of lists
+
+A special case of lists consisting solely of links to something is documented in the links test document.
+
+=> links.md links test document
diff --git a/testdata/lists.md b/testdata/lists.md
new file mode 100644
index 0000000..d506304
--- /dev/null
+++ b/testdata/lists.md
@@ -0,0 +1,53 @@
+# Lists
+
+Definition lists, numbered and ordered lists are all supported in gmnhg.
+
+## Definition lists
+
+The lists of definitions get converted into regular unordered lists,
+prefixed with a star (`*`) as specified by Gemini spec p. 5.5.2.
+
+gmnhg
+: a program to generate a Gemini site from an existing Hugo site
+: a library converting Markdown to Gemtext, based on gomarkdown
+
+md2gmn
+: a program to convert Markdown to Gemtext
+: a wrapper to the gmnhg library
+
+## Normal lists
+
+* This is the first item of an unordered list.
+* This is its second item.
+
++ This is a list item that was using the `+` sign. Gemini readers should
+ see this item as the continuation of the previous list.
+
+1. This is an ordered list first item.
+2. This is the second item.
+
+## Lists containing a sub-list
+
+As there's no indented list line type in Gemtext, gmnhg will indent
+these with tabs. The tabs number is equivalent to list level minus one
+(e.g. single tab for second list level).
+
+Unordered lists can be children of ordered lists, and vice versa.
+
+* This item contains a child ordered list.
+ 1. This ordered list item should get picked up as regular text.
+ 2. Whether or not this looks nicely depends on the client.
+* This item contains a child definition list.
+ Markdown
+ : an overly complex text markup format invented in 2004 whose sole
+ specification of CommonMark lacks both tables and footnotes
+ : a text format that has zero parsers completely compatible between
+ each other.
+
+1. This item contains a child unordered list.
+ * This whole list should get treated as plain text by clients.
+
+## Links of lists
+
+A special case of lists consisting solely of links to something is
+documented in the [links test document](links.md).
diff --git a/testdata/tables.gmi b/testdata/tables.gmi
new file mode 100644
index 0000000..87db193
--- /dev/null
+++ b/testdata/tables.gmi
@@ -0,0 +1,52 @@
+# Tables
+
+gmnhg uses preformatted text blocks to render ASCII text tables.
+
+## Simple table example
+
+```
++-----------+-------------+
+| Syntax | Description |
++-----------+-------------+
+| Header | Title |
+| Paragraph | Text |
++-----------+-------------+
+```
+
+## Empty rows or cells
+
+These are picked up as well.
+
+```
++-------+------+
+| test | nice |
++-------+------+
+| `est` | |
++-------+------+
+```
+
+```
++------+------+
+| test | nice |
++------+------+
+| | |
++------+------+
+```
+
+## Formatting inside tables
+
+Text formatting is fully supported inside tables. Links will also get picked up, and a links block will appear after the parent table if needed.
+
+```
++----------+----------+--------------+
+| Header 1 | Header 2 | Header 3[^1] |
++----------+----------+--------------+
+| Item 1 | Item 2 | Item 3 |
+| Item 1a | Item 2a | Item 3a |
++----------+----------+--------------+
+```
+
+[^1]: Example footnote that explains header 3.
+
+=> https://example.tld Header 2
+=> https://www.example.com Item 2
diff --git a/testdata/tables.md b/testdata/tables.md
new file mode 100644
index 0000000..ba0d637
--- /dev/null
+++ b/testdata/tables.md
@@ -0,0 +1,35 @@
+# Tables
+
+gmnhg uses preformatted text blocks to render ASCII text tables.
+
+## Simple table example
+
+| Syntax | Description |
+| ----------- | ----------- |
+| Header | Title |
+| Paragraph | Text |
+
+## Empty rows or cells
+
+These are picked up as well.
+
+| test | nice |
+|------|------|
+| `est` | |
+
+| test | nice |
+|------|------|
+| | |
+
+## Formatting inside tables
+
+Text formatting is fully supported inside tables. Links will also get
+picked up, and a links block will appear after the parent table if
+needed.
+
+| Header 1 | [Header 2](https://example.tld) | Header 3[^foo] |
+|----------|----------|----------|
+| Item 1 | [Item 2](https://www.example.com) | Item 3 |
+| Item 1a | Item 2a | Item 3a |
+
+[^foo]: Example footnote that explains header 3.