Hugo-to-Gemini Markdown converter
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

108 lines
3.3 KiB

  1. // This file is part of gmnhg.
  2. // gmnhg is free software: you can redistribute it and/or modify
  3. // it under the terms of the GNU General Public License as published by
  4. // the Free Software Foundation, either version 3 of the License, or
  5. // (at your option) any later version.
  6. // gmnhg is distributed in the hope that it will be useful,
  7. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  8. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  9. // GNU General Public License for more details.
  10. // You should have received a copy of the GNU General Public License
  11. // along with gmnhg. If not, see <https://www.gnu.org/licenses/>.
  12. // Package gemini provides functions to convert Markdown files to
  13. // Gemtext. It supports the use of YAML front matter in Markdown.
  14. package gemini
  15. import (
  16. "bytes"
  17. "errors"
  18. "fmt"
  19. "time"
  20. "git.tdem.in/tdemin/gmnhg/internal/gemini"
  21. "github.com/gomarkdown/markdown"
  22. "github.com/gomarkdown/markdown/parser"
  23. "gopkg.in/yaml.v2"
  24. )
  25. // HugoMetadata implements gemini.Metadata, providing the bare minimum
  26. // of possible post props.
  27. type HugoMetadata struct {
  28. PostTitle string `yaml:"title"`
  29. PostIsDraft bool `yaml:"draft"`
  30. PostLayout string `yaml:"layout"`
  31. PostDate time.Time `yaml:"date"`
  32. }
  33. // Title returns post title.
  34. func (h HugoMetadata) Title() string {
  35. return h.PostTitle
  36. }
  37. // Date returns post date.
  38. func (h HugoMetadata) Date() time.Time {
  39. return h.PostDate
  40. }
  41. var yamlDelimiter = []byte("---\n")
  42. // ErrPostIsDraft indicates the post rendered is a draft and is not
  43. // supposed to be rendered.
  44. var ErrPostIsDraft = errors.New("post is draft")
  45. // MetadataSetting defines whether or not metadata is included in the
  46. // rendered text.
  47. type MetadataSetting int
  48. // Metadata settings control the inclusion of metadata in the rendered
  49. // text.
  50. const (
  51. WithMetadata MetadataSetting = iota
  52. WithoutMetadata
  53. )
  54. // RenderMarkdown converts Markdown text to text/gemini using
  55. // gomarkdown, appending Hugo YAML front matter data if any is present
  56. // to the post header.
  57. //
  58. // Only a subset of front matter data parsed by Hugo is included in the
  59. // final document. At this point it's just title and date.
  60. //
  61. // Draft posts are still rendered, but with an error of type
  62. // ErrPostIsDraft.
  63. func RenderMarkdown(md []byte, metadataSetting MetadataSetting) (geminiText []byte, metadata HugoMetadata, err error) {
  64. var (
  65. blockEnd int
  66. yamlContent []byte
  67. )
  68. // only allow front matter at file start
  69. if bytes.Index(md, yamlDelimiter) != 0 {
  70. goto parse
  71. }
  72. blockEnd = bytes.Index(md[len(yamlDelimiter):], yamlDelimiter)
  73. if blockEnd == -1 {
  74. goto parse
  75. }
  76. yamlContent = md[len(yamlDelimiter) : blockEnd+len(yamlDelimiter)]
  77. if err := yaml.Unmarshal(yamlContent, &metadata); err != nil {
  78. return nil, metadata, fmt.Errorf("invalid front matter: %w", err)
  79. }
  80. md = md[blockEnd+len(yamlDelimiter)*2:]
  81. parse:
  82. ast := markdown.Parse(md, parser.NewWithExtensions(parser.CommonExtensions))
  83. var geminiContent []byte
  84. if metadataSetting == WithMetadata && metadata.PostTitle != "" {
  85. geminiContent = markdown.Render(ast, gemini.NewRendererWithMetadata(metadata))
  86. } else {
  87. geminiContent = markdown.Render(ast, gemini.NewRenderer())
  88. }
  89. if metadata.PostIsDraft {
  90. return geminiContent, metadata, fmt.Errorf("%s: %w", metadata.PostTitle, ErrPostIsDraft)
  91. }
  92. return geminiContent, metadata, nil
  93. }