Parse metadata in all Hugo formats
This makes gmnhg parse TOML/YAML/JSON/org-mode front matter in Hugo posts. This also makes the library no longer render metadata in posts, removing the API to do so. The metadata parsing code itself moves to internal/gmnhg. As the library no longer has a preset to include metadata in rendered document, md2gmn will from now on silently discard front matter if it encounters any. Fixes #28. Unblocks #13.
This commit is contained in:
parent
2de6e634d6
commit
9778ada128
8 changed files with 265 additions and 129 deletions
|
@ -102,17 +102,11 @@
|
|||
// config option in config.toml:
|
||||
//
|
||||
// ignoreFiles = [ "_index\\.gmi\\.md$" ]
|
||||
//
|
||||
// Limitations:
|
||||
//
|
||||
// * For now, the program will only recognize YAML front matter, while
|
||||
// Hugo supports it in TOML, YAML, JSON, and org-mode formats.
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
|
@ -322,11 +316,13 @@ func main() {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
gemText, metadata, err := gemini.RenderMarkdown(fileContent, gemini.Defaults)
|
||||
content, metadata := gmnhg.ParseMetadata(fileContent)
|
||||
// skip drafts from rendering
|
||||
if errors.Is(err, gemini.ErrPostIsDraft) {
|
||||
if metadata.IsDraft {
|
||||
return nil
|
||||
} else if err != nil {
|
||||
}
|
||||
gemText, err := gemini.RenderMarkdown(content, gemini.Defaults)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// skip headless leaves from rendering
|
||||
|
@ -387,7 +383,7 @@ func main() {
|
|||
// render posts to files
|
||||
for fileName, post := range posts {
|
||||
var tmpl = singleTemplate
|
||||
if pl := post.Metadata.PostLayout; pl != "" {
|
||||
if pl := post.Metadata.Layout; pl != "" {
|
||||
t, ok := templates[pl]
|
||||
if !ok {
|
||||
// no point trying to render pages with no layout
|
||||
|
@ -413,15 +409,17 @@ func main() {
|
|||
if !hasTmpl {
|
||||
continue
|
||||
}
|
||||
content, err := ioutil.ReadFile(path.Join(contentBase, dirname, indexMdFilename))
|
||||
fileContent, err := ioutil.ReadFile(path.Join(contentBase, dirname, indexMdFilename))
|
||||
if err != nil {
|
||||
// skip unreadable index files
|
||||
continue
|
||||
}
|
||||
gemtext, _, err := gemini.RenderMarkdown(content, gemini.Defaults)
|
||||
if errors.Is(err, gemini.ErrPostIsDraft) {
|
||||
content, metadata := gmnhg.ParseMetadata(fileContent)
|
||||
if metadata.IsDraft {
|
||||
continue
|
||||
} else if err != nil {
|
||||
}
|
||||
gemtext, err := gemini.RenderMarkdown(content, gemini.Defaults)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
cnt := map[string]interface{}{
|
||||
|
@ -446,8 +444,9 @@ func main() {
|
|||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
gemtext, _, err := gemini.RenderMarkdown(indexContent, gemini.Defaults)
|
||||
if err != nil && !errors.Is(err, gemini.ErrPostIsDraft) {
|
||||
content, _ := gmnhg.ParseMetadata(indexContent)
|
||||
gemtext, err := gemini.RenderMarkdown(content, gemini.Defaults)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
buf := bytes.Buffer{}
|
||||
|
|
|
@ -13,8 +13,7 @@
|
|||
// You should have received a copy of the GNU General Public License
|
||||
// along with gmnhg. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// md2gmn converts Markdown text files to text/gemini. It panics on
|
||||
// invalid input.
|
||||
// md2gmn converts Markdown text files to text/gemini.
|
||||
package main
|
||||
|
||||
import (
|
||||
|
@ -23,6 +22,7 @@ import (
|
|||
"os"
|
||||
|
||||
gemini "github.com/tdemin/gmnhg"
|
||||
"github.com/tdemin/gmnhg/internal/gmnhg"
|
||||
)
|
||||
|
||||
var version = "v0+HEAD"
|
||||
|
@ -56,7 +56,8 @@ func main() {
|
|||
panic(err)
|
||||
}
|
||||
|
||||
geminiContent, _, err := gemini.RenderMarkdown(text, gemini.WithMetadata)
|
||||
content, _ := gmnhg.ParseMetadata(text)
|
||||
geminiContent, err := gemini.RenderMarkdown(content, gemini.Defaults)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
|
1
go.mod
1
go.mod
|
@ -7,6 +7,7 @@ require (
|
|||
github.com/Masterminds/sprig/v3 v3.2.2
|
||||
github.com/gomarkdown/markdown v0.0.0-20210514010506-3b9f47219fe7
|
||||
github.com/mattn/go-runewidth v0.0.13 // indirect
|
||||
github.com/niklasfasching/go-org v1.5.0
|
||||
github.com/olekukonko/tablewriter v0.0.5
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
)
|
||||
|
|
24
go.sum
24
go.sum
|
@ -6,9 +6,16 @@ github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030I
|
|||
github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
|
||||
github.com/Masterminds/sprig/v3 v3.2.2 h1:17jRggJu518dr3QaafizSXOjKYp94wKfABxUmyxvxX8=
|
||||
github.com/Masterminds/sprig/v3 v3.2.2/go.mod h1:UoaO7Yp8KlPnJIYWTFkMaqPUYKTfGFPhxNuwnnxkKlk=
|
||||
github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38/go.mod h1:r7bzyVFMNntcxPZXK3/+KdruV1H5KSlyVY0gc+NgInI=
|
||||
github.com/alecthomas/chroma v0.8.2/go.mod h1:sko8vR34/90zvl5QdcUdvzL3J8NKjAUx9va9jPuFNoM=
|
||||
github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721/go.mod h1:QO9JBoKquHd+jz9nshCh40fOfO+JzsoXy8qTHF68zU0=
|
||||
github.com/alecthomas/kong v0.2.4/go.mod h1:kQOmtJgV+Lb4aj+I2LEn40cbtawdWJ9Y8QLq+lElKxE=
|
||||
github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897/go.mod h1:xTS7Pm1pD1mvyM075QCDSRqH6qRLXylzS24ZTpRiSzQ=
|
||||
github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964/go.mod h1:Xd9hchkHSWYkEqJwUGisez3G1QY8Ryz0sdWrLPMGjLk=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dlclark/regexp2 v1.2.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
|
||||
github.com/gomarkdown/markdown v0.0.0-20210514010506-3b9f47219fe7 h1:oKYOfNR7Hp6XpZ4JqolL5u642Js5Z0n7psPVl+S5heo=
|
||||
github.com/gomarkdown/markdown v0.0.0-20210514010506-3b9f47219fe7/go.mod h1:aii0r/K0ZnHv7G0KF7xy1v0A7s2Ljrb5byB7MO5p6TU=
|
||||
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
|
||||
|
@ -17,6 +24,8 @@ github.com/huandu/xstrings v1.3.1 h1:4jgBlKK6tLKFvO8u5pmYjG91cqytmDCDvGh7ECVFfFs
|
|||
github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
||||
github.com/imdario/mergo v0.3.11 h1:3tnifQM4i+fbajXKBHXWEH+KvNHqojZ778UH75j3bGA=
|
||||
github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
|
||||
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=
|
||||
github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
|
||||
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
|
@ -24,18 +33,24 @@ github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMK
|
|||
github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
|
||||
github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/IfikLNY=
|
||||
github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
|
||||
github.com/niklasfasching/go-org v1.5.0 h1:V8IwoSPm/d61bceyWFxxnQLtlvNT+CjiYIhtZLdnMF0=
|
||||
github.com/niklasfasching/go-org v1.5.0/go.mod h1:sSb8ylwnAG+h8MGFDB3R1D5bxf8wA08REfhjShg3kjA=
|
||||
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
|
||||
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||
github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ=
|
||||
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
|
||||
github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng=
|
||||
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
golang.org/dl v0.0.0-20190829154251-82a15e2f2ead/go.mod h1:IUMfjQLJQd4UTqG1Z90tenwKoCX93Gn3MAQJMOSBsDQ=
|
||||
|
@ -43,9 +58,18 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
|
|||
golang.org/x/crypto v0.0.0-20200414173820-0848c9571904 h1:bXoxMPcSLOq08zI3/c5dEBT6lE4eh+jOh886GHrn6V8=
|
||||
golang.org/x/crypto v0.0.0-20200414173820-0848c9571904/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20201224014010-6772e930b67b h1:iFwSg7t5GZmB/Q5TjiEAsdoLDrdJRC1RiF2WhuV29Qw=
|
||||
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200413165638-669c56c373c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
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/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/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
|
|
121
internal/gmnhg/org.go
Normal file
121
internal/gmnhg/org.go
Normal file
|
@ -0,0 +1,121 @@
|
|||
package gmnhg
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/niklasfasching/go-org/org"
|
||||
)
|
||||
|
||||
// for strings "true", "false", and int/float numbers will try to
|
||||
// convert them to Go values; will simply return value on fail or any
|
||||
// other kind of input
|
||||
func parseValue(value interface{}) interface{} {
|
||||
switch value := value.(type) {
|
||||
case string:
|
||||
num, err := strconv.Atoi(value)
|
||||
if err == nil {
|
||||
return num
|
||||
}
|
||||
float, err := strconv.ParseFloat(value, 64)
|
||||
if err == nil {
|
||||
return float
|
||||
}
|
||||
boolean, err := strconv.ParseBool(value)
|
||||
if err == nil {
|
||||
return boolean
|
||||
}
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
// 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) {
|
||||
defer func() {
|
||||
if e := recover(); e != nil {
|
||||
err = fmt.Errorf("recovered from panic: %v", e)
|
||||
}
|
||||
}()
|
||||
v := reflect.ValueOf(mapOrStruct).Elem()
|
||||
switch kind := v.Kind(); kind {
|
||||
case reflect.Map:
|
||||
v.SetMapIndex(reflect.ValueOf(key), reflect.ValueOf(value))
|
||||
case reflect.Struct:
|
||||
var fieldName string
|
||||
t := v.Type()
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
field := t.Field(i)
|
||||
v, ok := field.Tag.Lookup(tag)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if v != key {
|
||||
continue
|
||||
}
|
||||
fieldName = field.Name
|
||||
}
|
||||
if fieldName == "" {
|
||||
return fmt.Errorf("cannot find tag %v with key %v in struct", tag, key)
|
||||
}
|
||||
v.FieldByName(fieldName).Set(reflect.ValueOf(parseValue(value)))
|
||||
default:
|
||||
return fmt.Errorf("cannot set key of %v", kind.String())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func unmarshalORG(data []byte, p interface{}) (err error) {
|
||||
parser := org.New()
|
||||
document := parser.Parse(bytes.NewReader(data), "")
|
||||
if document.Error != nil {
|
||||
return document.Error
|
||||
}
|
||||
for k, v := range document.BufferSettings {
|
||||
var (
|
||||
key string = k
|
||||
value interface{} = v
|
||||
)
|
||||
if strings.HasSuffix(k, "[]") {
|
||||
key = k[:len(k)-2]
|
||||
value = strings.Fields(v)
|
||||
} else if k == "tags" || k == "categories" || k == "aliases" {
|
||||
value = strings.Fields(v)
|
||||
} else if k == "date" {
|
||||
value = parseORGDate(v)
|
||||
}
|
||||
if err := reflectSetKey(p, "org", strings.ToLower(key), value); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Some Org parsing code below was originally taken from Hugo and was
|
||||
// tweaked for gmnhg purposes.
|
||||
//
|
||||
// Copyright 2018 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.
|
||||
|
||||
var orgDateRegex = regexp.MustCompile(`[<\[](\d{4}-\d{2}-\d{2}) .*[>\]]`)
|
||||
|
||||
func parseORGDate(s string) string {
|
||||
if m := orgDateRegex.FindStringSubmatch(s); m != nil {
|
||||
return m[1]
|
||||
}
|
||||
return s
|
||||
}
|
|
@ -1,10 +1,18 @@
|
|||
package gmnhg
|
||||
|
||||
import gemini "github.com/tdemin/gmnhg"
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"regexp"
|
||||
"time"
|
||||
|
||||
"github.com/BurntSushi/toml"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
type Post struct {
|
||||
Post []byte
|
||||
Metadata gemini.HugoMetadata
|
||||
Metadata Metadata
|
||||
Link string
|
||||
}
|
||||
|
||||
|
@ -16,7 +24,7 @@ func (p Posts) Len() int {
|
|||
}
|
||||
|
||||
func (p Posts) Less(i, j int) bool {
|
||||
return p[i].Metadata.PostDate.Before(p[j].Metadata.PostDate)
|
||||
return p[i].Metadata.Date.Before(p[j].Metadata.Date)
|
||||
}
|
||||
|
||||
func (p Posts) Swap(i, j int) {
|
||||
|
@ -24,3 +32,76 @@ func (p Posts) Swap(i, j int) {
|
|||
p[i] = p[j]
|
||||
p[j] = t
|
||||
}
|
||||
|
||||
// Metadata contains all recognized Hugo properties.
|
||||
type Metadata struct {
|
||||
Title string `yaml:"title" toml:"title" json:"title" org:"title"`
|
||||
IsDraft bool `yaml:"draft" toml:"draft" json:"draft" org:"draft"`
|
||||
Layout string `yaml:"layout" toml:"layout" json:"layout" org:"layout"`
|
||||
Date time.Time `yaml:"date" toml:"date" json:"date" org:"date"`
|
||||
Summary string `yaml:"summary" toml:"summary" json:"summary" org:"summary"`
|
||||
IsHeadless bool `yaml:"headless" toml:"headless" json:"headless" org:"headless"`
|
||||
}
|
||||
|
||||
var (
|
||||
yamlDelimiter = []byte("---\n")
|
||||
tomlDelimiter = []byte("+++\n")
|
||||
jsonObjectRegex = regexp.MustCompile(`\A(\{[\s\S]*\})\n\n`)
|
||||
orgModeRegex = regexp.MustCompile(`\A((?:#\+\w+: ?\S*\n)*)`)
|
||||
)
|
||||
|
||||
// ParseMetadata extracts TOML/JSON/YAML/org-mode format front matter
|
||||
// from Markdown text. If no metadata is found, markdown will be equal
|
||||
// to source.
|
||||
//
|
||||
// TOML front matter is identified as +++ symbols at the very start of
|
||||
// the text, followed by TOML content, followed by another +++ (YAML is
|
||||
// the same, but with ---). JSON front matter is identified as a JSON
|
||||
// object followed by two newline symbols. org-mode front matter is
|
||||
// identified as a set of #+KEY: VALUE lines, the first line started
|
||||
// with anything else but #+ ends the front matter.
|
||||
func ParseMetadata(source []byte) (markdown []byte, metadata Metadata) {
|
||||
var (
|
||||
blockEnd int
|
||||
metadataContent []byte
|
||||
)
|
||||
markdown = source
|
||||
// block start is always 0, as front matter is only permitted at the
|
||||
// very start of the file
|
||||
if bytes.Index(source, yamlDelimiter) == 0 {
|
||||
blockEnd = bytes.Index(source[len(yamlDelimiter):], yamlDelimiter)
|
||||
if blockEnd == -1 {
|
||||
return
|
||||
}
|
||||
metadataContent = source[len(yamlDelimiter) : blockEnd+len(yamlDelimiter)]
|
||||
if err := yaml.Unmarshal(metadataContent, &metadata); err != nil {
|
||||
return
|
||||
}
|
||||
markdown = source[blockEnd+len(yamlDelimiter)*2:]
|
||||
} else if bytes.Index(source, tomlDelimiter) == 0 {
|
||||
blockEnd = bytes.Index(source[len(tomlDelimiter):], tomlDelimiter)
|
||||
if blockEnd == -1 {
|
||||
return
|
||||
}
|
||||
metadataContent = source[len(tomlDelimiter) : blockEnd+len(tomlDelimiter)]
|
||||
if err := toml.Unmarshal(metadataContent, &metadata); err != nil {
|
||||
return
|
||||
}
|
||||
markdown = source[blockEnd+len(yamlDelimiter)*2:]
|
||||
} else if match := jsonObjectRegex.FindIndex(source); match != nil {
|
||||
blockEnd = match[1]
|
||||
metadataContent = source[:blockEnd]
|
||||
if err := json.Unmarshal(metadataContent, &metadata); err != nil {
|
||||
return
|
||||
}
|
||||
markdown = source[blockEnd+1:] // JSON end + \n\n - 1
|
||||
} else if match := orgModeRegex.FindIndex(source); match != nil {
|
||||
blockEnd = match[1]
|
||||
metadataContent = source[:blockEnd]
|
||||
if err := unmarshalORG(metadataContent, &metadata); err != nil {
|
||||
return
|
||||
}
|
||||
markdown = source[blockEnd:]
|
||||
}
|
||||
return
|
||||
}
|
||||
|
|
|
@ -13,9 +13,9 @@
|
|||
// You should have received a copy of the GNU General Public License
|
||||
// along with gmnhg. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// Package gemini contains an implementation of markdown => text/gemini
|
||||
// Package renderer contains an implementation of markdown => text/gemini
|
||||
// renderer for github.com/gomarkdown/markdown.
|
||||
package gemini
|
||||
package renderer
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
|
@ -23,7 +23,6 @@ import (
|
|||
"io"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gomarkdown/markdown/ast"
|
||||
"github.com/olekukonko/tablewriter"
|
||||
|
@ -52,30 +51,14 @@ var (
|
|||
// matches a FULL string that contains no non-whitespace characters
|
||||
var emptyLineRegex = regexp.MustCompile(`\A[\s]*\z`)
|
||||
|
||||
const timestampFormat = "2006-01-02 15:04"
|
||||
|
||||
// Metadata provides data necessary for proper post rendering.
|
||||
type Metadata interface {
|
||||
Title() string
|
||||
Date() time.Time
|
||||
}
|
||||
|
||||
// Renderer implements markdown.Renderer.
|
||||
type Renderer struct {
|
||||
Metadata Metadata
|
||||
}
|
||||
type Renderer struct{}
|
||||
|
||||
// NewRenderer returns a new Renderer.
|
||||
func NewRenderer() Renderer {
|
||||
return Renderer{}
|
||||
}
|
||||
|
||||
// NewRendererWithMetadata returns a new Renderer initialized with post
|
||||
// metadata.
|
||||
func NewRendererWithMetadata(m Metadata) Renderer {
|
||||
return Renderer{Metadata: m}
|
||||
}
|
||||
|
||||
func getNodeDelimiter(node ast.Node) []byte {
|
||||
switch node.(type) {
|
||||
case *ast.Code:
|
||||
|
@ -528,15 +511,8 @@ func (r Renderer) RenderNode(w io.Writer, node ast.Node, entering bool) ast.Walk
|
|||
return ast.GoToNext
|
||||
}
|
||||
|
||||
// RenderHeader implements Renderer.RenderHeader(). It renders metadata
|
||||
// at the top of the post if any has been provided.
|
||||
func (r Renderer) RenderHeader(w io.Writer, node ast.Node) {
|
||||
if r.Metadata != nil {
|
||||
// TODO: Renderer.RenderHeader: check whether date is mandatory
|
||||
// in Hugo
|
||||
w.Write([]byte(fmt.Sprintf("# %s\n\n%s\n\n", r.Metadata.Title(), r.Metadata.Date().Format(timestampFormat))))
|
||||
}
|
||||
}
|
||||
// RenderHeader implements Renderer.RenderHeader().
|
||||
func (r Renderer) RenderHeader(w io.Writer, node ast.Node) {}
|
||||
|
||||
// RenderFooter implements Renderer.RenderFooter().
|
||||
func (r Renderer) RenderFooter(w io.Writer, node ast.Node) {}
|
89
render.go
89
render.go
|
@ -14,112 +14,45 @@
|
|||
// along with gmnhg. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// Package gemini provides functions to convert Markdown files to
|
||||
// Gemtext. It supports the use of YAML front matter in Markdown.
|
||||
// Gemtext.
|
||||
package gemini
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/gomarkdown/markdown"
|
||||
"github.com/gomarkdown/markdown/parser"
|
||||
"github.com/tdemin/gmnhg/internal/gemini"
|
||||
"gopkg.in/yaml.v2"
|
||||
"github.com/tdemin/gmnhg/internal/renderer"
|
||||
)
|
||||
|
||||
// HugoMetadata implements gemini.Metadata, providing the bare minimum
|
||||
// of possible post props.
|
||||
type HugoMetadata struct {
|
||||
PostTitle string `yaml:"title"`
|
||||
PostIsDraft bool `yaml:"draft"`
|
||||
PostLayout string `yaml:"layout"`
|
||||
PostDate time.Time `yaml:"date"`
|
||||
PostSummary string `yaml:"summary"`
|
||||
IsHeadless bool `yaml:"headless"`
|
||||
}
|
||||
|
||||
// Title returns post title.
|
||||
func (h HugoMetadata) Title() string {
|
||||
return h.PostTitle
|
||||
}
|
||||
|
||||
// Date returns post date.
|
||||
func (h HugoMetadata) Date() time.Time {
|
||||
return h.PostDate
|
||||
}
|
||||
|
||||
var (
|
||||
yamlDelimiter = []byte("---\n")
|
||||
trailing = []byte("\n\n")
|
||||
)
|
||||
|
||||
// ErrPostIsDraft indicates the post rendered is a draft and is not
|
||||
// supposed to be rendered.
|
||||
var ErrPostIsDraft = errors.New("post is draft")
|
||||
|
||||
// Settings is a bitmask for renderer preferences.
|
||||
type Settings uint
|
||||
|
||||
// Has uses AND to check whether a flag is set.
|
||||
// Has returns true if a flag or a set of flags are all set.
|
||||
func (s Settings) Has(setting Settings) bool {
|
||||
return (s & setting) != 0
|
||||
return (s & setting) == setting
|
||||
}
|
||||
|
||||
const (
|
||||
// Defaults simply renders the document.
|
||||
Defaults Settings = 0
|
||||
// WithMetadata indicates that the metadata should be included in
|
||||
// the text produced by the renderer.
|
||||
WithMetadata Settings = 1 << iota
|
||||
)
|
||||
|
||||
var trailing = []byte("\n\n")
|
||||
|
||||
// RenderMarkdown converts Markdown text to Gemtext using gomarkdown. It
|
||||
// appends Hugo YAML front matter data to the post header if
|
||||
// WithMetadata is set.
|
||||
//
|
||||
// Only a subset of front matter data parsed by Hugo is included in the
|
||||
// final document. At this point it's just title and date.
|
||||
//
|
||||
// Draft posts are still rendered, but with an error of type
|
||||
// ErrPostIsDraft.
|
||||
func RenderMarkdown(md []byte, settings Settings) (geminiText []byte, metadata HugoMetadata, err error) {
|
||||
var (
|
||||
blockEnd int
|
||||
yamlContent []byte
|
||||
)
|
||||
// only allow front matter at file start
|
||||
if bytes.Index(md, yamlDelimiter) != 0 {
|
||||
goto parse
|
||||
}
|
||||
blockEnd = bytes.Index(md[len(yamlDelimiter):], yamlDelimiter)
|
||||
if blockEnd == -1 {
|
||||
goto parse
|
||||
}
|
||||
yamlContent = md[len(yamlDelimiter) : blockEnd+len(yamlDelimiter)]
|
||||
if err := yaml.Unmarshal(yamlContent, &metadata); err != nil {
|
||||
return nil, metadata, fmt.Errorf("invalid front matter: %w", err)
|
||||
}
|
||||
md = md[blockEnd+len(yamlDelimiter)*2:]
|
||||
parse:
|
||||
// ignores front matter if any has been provided in the text.
|
||||
func RenderMarkdown(md []byte, settings Settings) (geminiText []byte, err error) {
|
||||
ast := markdown.Parse(md, parser.NewWithExtensions(parser.CommonExtensions|
|
||||
parser.NoEmptyLineBeforeBlock|
|
||||
parser.Footnotes))
|
||||
var content []byte
|
||||
if settings.Has(WithMetadata) && metadata.PostTitle != "" {
|
||||
content = markdown.Render(ast, gemini.NewRendererWithMetadata(metadata))
|
||||
} else {
|
||||
content = markdown.Render(ast, gemini.NewRenderer())
|
||||
}
|
||||
content := markdown.Render(ast, renderer.NewRenderer())
|
||||
// strip trailing newlines if any
|
||||
for li := bytes.LastIndex(content, trailing); li != -1; li = bytes.LastIndex(content, trailing) {
|
||||
if li != len(content)-len(trailing) {
|
||||
break
|
||||
}
|
||||
content = content[:len(content)-1]
|
||||
}
|
||||
if metadata.PostIsDraft {
|
||||
return content, metadata, fmt.Errorf("%s: %w", metadata.PostTitle, ErrPostIsDraft)
|
||||
}
|
||||
return content, metadata, nil
|
||||
return content, nil
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue