hugo-to-gemini/internal/gmnhg/post.go
Timur Demin fc76187e13
Implement renderer test suite
This implements a Markdown / Gemtext suite, testing the entire renderer
at complex Markdown documents, containing the entirety of current
Markdown features accessible with gmnhg. The test suite can be expanded
by adding a pair of .md/.gmi files.

Minor bug fixes in JSON/Org metadata parsing where bugs were detected
with the test suite are also included in this patch.

Fixes #13.
2021-09-30 18:01:46 +05:00

122 lines
3.8 KiB
Go

// 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 <https://www.gnu.org/licenses/>.
package gmnhg
import (
"bytes"
"encoding/json"
"regexp"
"time"
"github.com/BurntSushi/toml"
"gopkg.in/yaml.v2"
)
type Post struct {
Post []byte
Metadata Metadata
Link string
}
// Posts implements sort.Interface.
type Posts []Post
func (p Posts) Len() int {
return len(p)
}
func (p Posts) Less(i, j int) bool {
return p[i].Metadata.Date.Before(p[j].Metadata.Date)
}
func (p Posts) Swap(i, j int) {
t := p[i]
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+\[?\]?: ?[^\n\r]*\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:]
} 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
}