// Package gitea ... // Taken from caddy source code (https://github.com/mholt/caddy/) // Copyright 2015 Matthew Holt and The Caddy Authors // // 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. package gitea import ( "bytes" "encoding/json" "fmt" "strings" "unicode" "github.com/BurntSushi/toml" chromahtml "github.com/alecthomas/chroma/v2/formatters/html" "github.com/yuin/goldmark" highlighting "github.com/yuin/goldmark-highlighting/v2" "github.com/yuin/goldmark/extension" "github.com/yuin/goldmark/parser" gmhtml "github.com/yuin/goldmark/renderer/html" "gopkg.in/yaml.v3" ) func extractFrontMatter(input string) (map[string]any, string, error) { // get the bounds of the first non-empty line var firstLineStart, firstLineEnd int lineEmpty := true for i, b := range input { if b == '\n' { firstLineStart = firstLineEnd if firstLineStart > 0 { firstLineStart++ // skip newline character } firstLineEnd = i if !lineEmpty { break } continue } lineEmpty = lineEmpty && unicode.IsSpace(b) } firstLine := input[firstLineStart:firstLineEnd] // ensure residue windows carriage return byte is removed firstLine = strings.TrimSpace(firstLine) // see what kind of front matter there is, if any var closingFence []string var fmParser func([]byte) (map[string]any, error) for _, fmType := range supportedFrontMatterTypes { if firstLine == fmType.FenceOpen { closingFence = fmType.FenceClose fmParser = fmType.ParseFunc } } if fmParser == nil { // no recognized front matter; whole document is body return nil, input, nil } // find end of front matter var fmEndFence string fmEndFenceStart := -1 for _, fence := range closingFence { index := strings.Index(input[firstLineEnd:], "\n"+fence) if index >= 0 { fmEndFenceStart = index fmEndFence = fence break } } if fmEndFenceStart < 0 { return nil, "", fmt.Errorf("unterminated front matter") } fmEndFenceStart += firstLineEnd + 1 // add 1 to account for newline // extract and parse front matter frontMatter := input[firstLineEnd:fmEndFenceStart] fm, err := fmParser([]byte(frontMatter)) if err != nil { return nil, "", err } // the rest is the body body := input[fmEndFenceStart+len(fmEndFence):] return fm, body, nil } func yamlFrontMatter(input []byte) (map[string]any, error) { m := make(map[string]any) err := yaml.Unmarshal(input, &m) return m, err } func tomlFrontMatter(input []byte) (map[string]any, error) { m := make(map[string]any) err := toml.Unmarshal(input, &m) return m, err } func jsonFrontMatter(input []byte) (map[string]any, error) { input = append([]byte{'{'}, input...) input = append(input, '}') m := make(map[string]any) err := json.Unmarshal(input, &m) return m, err } type parsedMarkdownDoc struct { Meta map[string]any `json:"meta,omitempty"` Body string `json:"body,omitempty"` } type frontMatterType struct { FenceOpen string FenceClose []string ParseFunc func(input []byte) (map[string]any, error) } var supportedFrontMatterTypes = []frontMatterType{ { FenceOpen: "---", FenceClose: []string{"---", "..."}, ParseFunc: yamlFrontMatter, }, { FenceOpen: "+++", FenceClose: []string{"+++"}, ParseFunc: tomlFrontMatter, }, { FenceOpen: "{", FenceClose: []string{"}"}, ParseFunc: jsonFrontMatter, }, } func markdown(input []byte) ([]byte, error) { md := goldmark.New( goldmark.WithExtensions( extension.GFM, extension.Footnote, highlighting.NewHighlighting( highlighting.WithFormatOptions( chromahtml.WithClasses(true), ), ), ), goldmark.WithParserOptions( parser.WithAutoHeadingID(), ), goldmark.WithRendererOptions( gmhtml.WithUnsafe(), // TODO: this is not awesome, maybe should be configurable? ), ) buf := bufPool.Get().(*bytes.Buffer) buf.Reset() defer bufPool.Put(buf) if err := md.Convert(input, buf); err != nil { return input, err } return buf.Bytes(), nil }