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.