code.gitea.io/gitea@v1.19.3/modules/markup/markdown/meta.go (about)

     1  // Copyright 2020 The Gitea Authors. All rights reserved.
     2  // SPDX-License-Identifier: MIT
     3  
     4  package markdown
     5  
     6  import (
     7  	"bytes"
     8  	"errors"
     9  	"unicode"
    10  	"unicode/utf8"
    11  
    12  	"gopkg.in/yaml.v3"
    13  )
    14  
    15  func isYAMLSeparator(line []byte) bool {
    16  	idx := 0
    17  	for ; idx < len(line); idx++ {
    18  		if line[idx] >= utf8.RuneSelf {
    19  			r, sz := utf8.DecodeRune(line[idx:])
    20  			if !unicode.IsSpace(r) {
    21  				return false
    22  			}
    23  			idx += sz
    24  			continue
    25  		}
    26  		if line[idx] != ' ' {
    27  			break
    28  		}
    29  	}
    30  	dashCount := 0
    31  	for ; idx < len(line); idx++ {
    32  		if line[idx] != '-' {
    33  			break
    34  		}
    35  		dashCount++
    36  	}
    37  	if dashCount < 3 {
    38  		return false
    39  	}
    40  	for ; idx < len(line); idx++ {
    41  		if line[idx] >= utf8.RuneSelf {
    42  			r, sz := utf8.DecodeRune(line[idx:])
    43  			if !unicode.IsSpace(r) {
    44  				return false
    45  			}
    46  			idx += sz
    47  			continue
    48  		}
    49  		if line[idx] != ' ' {
    50  			return false
    51  		}
    52  	}
    53  	return true
    54  }
    55  
    56  // ExtractMetadata consumes a markdown file, parses YAML frontmatter,
    57  // and returns the frontmatter metadata separated from the markdown content
    58  func ExtractMetadata(contents string, out interface{}) (string, error) {
    59  	body, err := ExtractMetadataBytes([]byte(contents), out)
    60  	return string(body), err
    61  }
    62  
    63  // ExtractMetadata consumes a markdown file, parses YAML frontmatter,
    64  // and returns the frontmatter metadata separated from the markdown content
    65  func ExtractMetadataBytes(contents []byte, out interface{}) ([]byte, error) {
    66  	var front, body []byte
    67  
    68  	start, end := 0, len(contents)
    69  	idx := bytes.IndexByte(contents[start:], '\n')
    70  	if idx >= 0 {
    71  		end = start + idx
    72  	}
    73  	line := contents[start:end]
    74  
    75  	if !isYAMLSeparator(line) {
    76  		return contents, errors.New("frontmatter must start with a separator line")
    77  	}
    78  	frontMatterStart := end + 1
    79  	for start = frontMatterStart; start < len(contents); start = end + 1 {
    80  		end = len(contents)
    81  		idx := bytes.IndexByte(contents[start:], '\n')
    82  		if idx >= 0 {
    83  			end = start + idx
    84  		}
    85  		line := contents[start:end]
    86  		if isYAMLSeparator(line) {
    87  			front = contents[frontMatterStart:start]
    88  			if end+1 < len(contents) {
    89  				body = contents[end+1:]
    90  			}
    91  			break
    92  		}
    93  	}
    94  
    95  	if len(front) == 0 {
    96  		return contents, errors.New("could not determine metadata")
    97  	}
    98  
    99  	if err := yaml.Unmarshal(front, out); err != nil {
   100  		return contents, err
   101  	}
   102  	return body, nil
   103  }