Skip to content

Commit e112d2e

Browse files
committed
Add Markdown YAML front matter
1 parent 03d9acc commit e112d2e

3 files changed

Lines changed: 148 additions & 8 deletions

File tree

convert/textout/render.go

Lines changed: 113 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"os"
99
"path/filepath"
1010
"slices"
11+
"strconv"
1112
"strings"
1213
"unicode"
1314
"unicode/utf8"
@@ -136,12 +137,14 @@ func (r *renderer) render() ([]byte, error) {
136137
}
137138

138139
func (r *renderer) renderFrontMatter() {
139-
info := r.c.Book.Description.TitleInfo
140-
title := strings.TrimSpace(info.BookTitle.Value)
141-
if title == "" {
142-
title = strings.TrimSuffix(r.c.SrcName, ".fb2")
140+
if r.format == formatMD {
141+
r.renderMarkdownFrontMatter()
142+
r.heading(r.plainInline(r.bookTitle()), 1)
143+
return
143144
}
144-
r.heading(r.plainInline(title), 1)
145+
146+
info := r.c.Book.Description.TitleInfo
147+
r.heading(r.plainInline(r.bookTitle()), 1)
145148

146149
lines := make([]string, 0, 6)
147150
if authors := formatAuthors(info.Authors); authors != "" {
@@ -162,6 +165,111 @@ func (r *renderer) renderFrontMatter() {
162165
r.paragraph(strings.Join(lines, "\n"))
163166
}
164167

168+
func (r *renderer) bookTitle() string {
169+
title := strings.TrimSpace(r.c.Book.Description.TitleInfo.BookTitle.Value)
170+
if title == "" {
171+
return strings.TrimSuffix(r.c.SrcName, ".fb2")
172+
}
173+
return title
174+
}
175+
176+
func (r *renderer) renderMarkdownFrontMatter() {
177+
info := r.c.Book.Description.TitleInfo
178+
lines := []string{"---"}
179+
if title := r.markdownMetaTitle(); title != "" {
180+
lines = append(lines, "title: "+yamlScalar(title))
181+
}
182+
if authors := r.markdownMetaAuthors(info.Authors); len(authors) > 0 {
183+
lines = append(lines, "authors:")
184+
for _, author := range authors {
185+
lines = append(lines, " - "+yamlScalar(author))
186+
}
187+
}
188+
if len(info.Sequences) > 0 {
189+
lines = append(lines, "series:")
190+
for _, seq := range info.Sequences {
191+
name := strings.TrimSpace(seq.Name)
192+
if name == "" {
193+
continue
194+
}
195+
lines = append(lines, " - name: "+yamlScalar(name))
196+
if seq.Number != nil {
197+
lines = append(lines, " number: "+strconv.Itoa(*seq.Number))
198+
}
199+
}
200+
}
201+
if lang := strings.TrimSpace(info.Lang.String()); lang != "" && lang != "und" {
202+
lines = append(lines, "language: "+yamlScalar(lang))
203+
}
204+
if date := formatDate(info.Date); date != "" {
205+
lines = append(lines, "date: "+yamlScalar(date))
206+
}
207+
if genres := markdownMetaGenres(info.Genres); len(genres) > 0 {
208+
lines = append(lines, "genres:")
209+
for _, genre := range genres {
210+
lines = append(lines, " - "+yamlScalar(genre))
211+
}
212+
}
213+
lines = append(lines, "---")
214+
r.block(strings.Join(lines, "\n"))
215+
}
216+
217+
func (r *renderer) markdownMetaTitle() string {
218+
templateText := strings.TrimSpace(r.cfg.Metainformation.TitleTemplate)
219+
if templateText != "" {
220+
title, err := r.c.Book.ExpandTemplateMetainfo(config.MetaTitleTemplateFieldName, templateText, r.c.SrcName, r.c.OutputFormat)
221+
if err == nil && strings.TrimSpace(title) != "" {
222+
return strings.TrimSpace(title)
223+
}
224+
}
225+
title := strings.TrimSpace(r.c.Book.Description.TitleInfo.BookTitle.Value)
226+
if title == "" {
227+
return strings.TrimSuffix(r.c.SrcName, ".fb2")
228+
}
229+
return title
230+
}
231+
232+
func (r *renderer) markdownMetaAuthors(authors []fb2.Author) []string {
233+
result := make([]string, 0, len(authors))
234+
templateText := strings.TrimSpace(r.cfg.Metainformation.CreatorNameTemplate)
235+
for i := range authors {
236+
name := ""
237+
if templateText != "" {
238+
expanded, err := r.c.Book.ExpandTemplateAuthorName(
239+
config.MetaCreatorNameTemplateFieldName,
240+
templateText,
241+
r.c.OutputFormat,
242+
i,
243+
&authors[i],
244+
)
245+
if err == nil {
246+
name = strings.TrimSpace(expanded)
247+
}
248+
}
249+
if name == "" {
250+
name = formatAuthor(authors[i])
251+
}
252+
if name != "" {
253+
result = append(result, name)
254+
}
255+
}
256+
return result
257+
}
258+
259+
func markdownMetaGenres(genres []fb2.GenreRef) []string {
260+
result := make([]string, 0, len(genres))
261+
for _, genre := range genres {
262+
if text := strings.TrimSpace(genre.Value); text != "" {
263+
result = append(result, text)
264+
}
265+
}
266+
return result
267+
}
268+
269+
func yamlScalar(text string) string {
270+
return strconv.Quote(text)
271+
}
272+
165273
func (r *renderer) renderAnnotation() {
166274
r.heading(r.plainInline(annotationTitle(r.cfg)), 2)
167275
r.renderFlow(r.c.Book.Description.TitleInfo.Annotation.Items, 0, blockNormal)

convert/textout/render_test.go

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,9 @@ func TestRenderMarkdownSemanticOutput(t *testing.T) {
5555
t.Fatalf("Render() error = %v", err)
5656
}
5757

58-
assertContains(t, got, "# Test Book")
59-
assertContains(t, got, "Authors: Ada Lovelace")
60-
assertContains(t, got, "Series: Series #1")
58+
assertContains(t, got, "---\ntitle: \"Test Book\"\nauthors:\n - \"Ada Lovelace\"\nseries:\n - name: \"Series\"\n number: 1")
59+
assertContains(t, got, "language: \"en\"")
60+
assertContains(t, got, "genres:\n - \"sf\"\n---\n\n# Test Book")
6161
assertContains(t, got, "## Chapter \\*One\\*")
6262
assertContains(t, got, "Hello **world** [site](https://example.com)")
6363
assertContains(t, got, "See [Chapter](#chapter-1) end")
@@ -67,6 +67,36 @@ func TestRenderMarkdownSemanticOutput(t *testing.T) {
6767
assertContains(t, got, "| --- | --- |")
6868
}
6969

70+
func TestRenderMarkdownFrontMatterUsesMetainformationTemplates(t *testing.T) {
71+
c := testContent(common.OutputFmtMd)
72+
noteNum := 2
73+
c.Book.Description.TitleInfo.Authors = []fb2.Author{{FirstName: "Ada", LastName: "Lovelace"}}
74+
c.Book.Description.TitleInfo.Sequences = []fb2.Sequence{{Name: "Analytical Engine", Number: &noteNum}}
75+
c.Book.Bodies = []fb2.Body{{
76+
Kind: fb2.BodyMain,
77+
Sections: []fb2.Section{{
78+
ID: "chapter-1",
79+
Title: title("Chapter"),
80+
Content: []fb2.FlowItem{paragraphItem(fb2.InlineSegment{Kind: fb2.InlineText, Text: "Text"})},
81+
}},
82+
}}
83+
cfg := testConfig()
84+
cfg.Metainformation.TitleTemplate = `{{ with first .Series }}{{ .Name }} #{{ .Number }}: {{ end }}{{ .Title }}`
85+
cfg.Metainformation.CreatorNameTemplate = `{{ .LastName }}, {{ .FirstName }}`
86+
87+
got, err := renderForTest(c, cfg)
88+
if err != nil {
89+
t.Fatalf("Render() error = %v", err)
90+
}
91+
92+
assertContains(t, got, "title: \"Analytical Engine #2: Test Book\"")
93+
assertContains(t, got, "authors:\n - \"Lovelace, Ada\"")
94+
assertContains(t, got, "---\n\n# Test Book")
95+
if strings.Contains(got, "# Analytical Engine #2: Test Book") || strings.Contains(got, "Authors: Ada Lovelace") {
96+
t.Fatalf("Markdown rendered legacy metadata block instead of YAML front matter:\n%s", got)
97+
}
98+
}
99+
70100
func TestRenderMarkdownTOCLinksToStableAnchors(t *testing.T) {
71101
c := testContent(common.OutputFmtMd)
72102
c.Book.Bodies = []fb2.Body{{

docs/formats/text.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ TXT output:
1717
Markdown output:
1818

1919
- Produces UTF-8 semantic Markdown with headings, paragraphs, links, tables, block quotes, fenced code blocks, and optional image output.
20+
- Starts with YAML front matter containing title, authors, series, language, date, and genres when available.
21+
- Uses `document.metainformation.title_template` and `creator_name_template` for YAML front matter title and author values.
2022
- Emits explicit anchors for section and note targets.
2123
- Uses Markdown pipe tables, including inside collected endnotes.
2224
- Preserves code listings as fenced code blocks when a paragraph is entirely code.

0 commit comments

Comments
 (0)