Use markdown frontmatter to provide Table of contents, language and frontmatter rendering (#11047)
* Add control for the rendering of the frontmatter * Add control to include a TOC * Add control to set language - allows control of ToC header and CJK glyph choice. Signed-off-by: Andrew Thornton art27@cantab.nettokarchuk/v1.17
parent
d3fc9c08c8
commit
812cfd0ad9
@ -0,0 +1,107 @@ |
|||||||
|
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package markdown |
||||||
|
|
||||||
|
import "github.com/yuin/goldmark/ast" |
||||||
|
|
||||||
|
// Details is a block that contains Summary and details
|
||||||
|
type Details struct { |
||||||
|
ast.BaseBlock |
||||||
|
} |
||||||
|
|
||||||
|
// Dump implements Node.Dump .
|
||||||
|
func (n *Details) Dump(source []byte, level int) { |
||||||
|
ast.DumpHelper(n, source, level, nil, nil) |
||||||
|
} |
||||||
|
|
||||||
|
// KindDetails is the NodeKind for Details
|
||||||
|
var KindDetails = ast.NewNodeKind("Details") |
||||||
|
|
||||||
|
// Kind implements Node.Kind.
|
||||||
|
func (n *Details) Kind() ast.NodeKind { |
||||||
|
return KindDetails |
||||||
|
} |
||||||
|
|
||||||
|
// NewDetails returns a new Paragraph node.
|
||||||
|
func NewDetails() *Details { |
||||||
|
return &Details{ |
||||||
|
BaseBlock: ast.BaseBlock{}, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// IsDetails returns true if the given node implements the Details interface,
|
||||||
|
// otherwise false.
|
||||||
|
func IsDetails(node ast.Node) bool { |
||||||
|
_, ok := node.(*Details) |
||||||
|
return ok |
||||||
|
} |
||||||
|
|
||||||
|
// Summary is a block that contains the summary of details block
|
||||||
|
type Summary struct { |
||||||
|
ast.BaseBlock |
||||||
|
} |
||||||
|
|
||||||
|
// Dump implements Node.Dump .
|
||||||
|
func (n *Summary) Dump(source []byte, level int) { |
||||||
|
ast.DumpHelper(n, source, level, nil, nil) |
||||||
|
} |
||||||
|
|
||||||
|
// KindSummary is the NodeKind for Summary
|
||||||
|
var KindSummary = ast.NewNodeKind("Summary") |
||||||
|
|
||||||
|
// Kind implements Node.Kind.
|
||||||
|
func (n *Summary) Kind() ast.NodeKind { |
||||||
|
return KindSummary |
||||||
|
} |
||||||
|
|
||||||
|
// NewSummary returns a new Summary node.
|
||||||
|
func NewSummary() *Summary { |
||||||
|
return &Summary{ |
||||||
|
BaseBlock: ast.BaseBlock{}, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// IsSummary returns true if the given node implements the Summary interface,
|
||||||
|
// otherwise false.
|
||||||
|
func IsSummary(node ast.Node) bool { |
||||||
|
_, ok := node.(*Summary) |
||||||
|
return ok |
||||||
|
} |
||||||
|
|
||||||
|
// Icon is an inline for a fomantic icon
|
||||||
|
type Icon struct { |
||||||
|
ast.BaseInline |
||||||
|
Name []byte |
||||||
|
} |
||||||
|
|
||||||
|
// Dump implements Node.Dump .
|
||||||
|
func (n *Icon) Dump(source []byte, level int) { |
||||||
|
m := map[string]string{} |
||||||
|
m["Name"] = string(n.Name) |
||||||
|
ast.DumpHelper(n, source, level, m, nil) |
||||||
|
} |
||||||
|
|
||||||
|
// KindIcon is the NodeKind for Icon
|
||||||
|
var KindIcon = ast.NewNodeKind("Icon") |
||||||
|
|
||||||
|
// Kind implements Node.Kind.
|
||||||
|
func (n *Icon) Kind() ast.NodeKind { |
||||||
|
return KindIcon |
||||||
|
} |
||||||
|
|
||||||
|
// NewIcon returns a new Paragraph node.
|
||||||
|
func NewIcon(name string) *Icon { |
||||||
|
return &Icon{ |
||||||
|
BaseInline: ast.BaseInline{}, |
||||||
|
Name: []byte(name), |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// IsIcon returns true if the given node implements the Icon interface,
|
||||||
|
// otherwise false.
|
||||||
|
func IsIcon(node ast.Node) bool { |
||||||
|
_, ok := node.(*Icon) |
||||||
|
return ok |
||||||
|
} |
@ -0,0 +1,163 @@ |
|||||||
|
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package markdown |
||||||
|
|
||||||
|
import ( |
||||||
|
"fmt" |
||||||
|
"strings" |
||||||
|
|
||||||
|
"github.com/yuin/goldmark/ast" |
||||||
|
east "github.com/yuin/goldmark/extension/ast" |
||||||
|
"gopkg.in/yaml.v2" |
||||||
|
) |
||||||
|
|
||||||
|
// RenderConfig represents rendering configuration for this file
|
||||||
|
type RenderConfig struct { |
||||||
|
Meta string |
||||||
|
Icon string |
||||||
|
TOC bool |
||||||
|
Lang string |
||||||
|
} |
||||||
|
|
||||||
|
// ToRenderConfig converts a yaml.MapSlice to a RenderConfig
|
||||||
|
func (rc *RenderConfig) ToRenderConfig(meta yaml.MapSlice) { |
||||||
|
if meta == nil { |
||||||
|
return |
||||||
|
} |
||||||
|
found := false |
||||||
|
var giteaMetaControl yaml.MapItem |
||||||
|
for _, item := range meta { |
||||||
|
strKey, ok := item.Key.(string) |
||||||
|
if !ok { |
||||||
|
continue |
||||||
|
} |
||||||
|
strKey = strings.TrimSpace(strings.ToLower(strKey)) |
||||||
|
switch strKey { |
||||||
|
case "gitea": |
||||||
|
giteaMetaControl = item |
||||||
|
found = true |
||||||
|
case "include_toc": |
||||||
|
val, ok := item.Value.(bool) |
||||||
|
if !ok { |
||||||
|
continue |
||||||
|
} |
||||||
|
rc.TOC = val |
||||||
|
case "lang": |
||||||
|
val, ok := item.Value.(string) |
||||||
|
if !ok { |
||||||
|
continue |
||||||
|
} |
||||||
|
val = strings.TrimSpace(val) |
||||||
|
if len(val) == 0 { |
||||||
|
continue |
||||||
|
} |
||||||
|
rc.Lang = val |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if found { |
||||||
|
switch v := giteaMetaControl.Value.(type) { |
||||||
|
case string: |
||||||
|
switch v { |
||||||
|
case "none": |
||||||
|
rc.Meta = "none" |
||||||
|
case "table": |
||||||
|
rc.Meta = "table" |
||||||
|
default: // "details"
|
||||||
|
rc.Meta = "details" |
||||||
|
} |
||||||
|
case yaml.MapSlice: |
||||||
|
for _, item := range v { |
||||||
|
strKey, ok := item.Key.(string) |
||||||
|
if !ok { |
||||||
|
continue |
||||||
|
} |
||||||
|
strKey = strings.TrimSpace(strings.ToLower(strKey)) |
||||||
|
switch strKey { |
||||||
|
case "meta": |
||||||
|
val, ok := item.Value.(string) |
||||||
|
if !ok { |
||||||
|
continue |
||||||
|
} |
||||||
|
switch strings.TrimSpace(strings.ToLower(val)) { |
||||||
|
case "none": |
||||||
|
rc.Meta = "none" |
||||||
|
case "table": |
||||||
|
rc.Meta = "table" |
||||||
|
default: // "details"
|
||||||
|
rc.Meta = "details" |
||||||
|
} |
||||||
|
case "details_icon": |
||||||
|
val, ok := item.Value.(string) |
||||||
|
if !ok { |
||||||
|
continue |
||||||
|
} |
||||||
|
rc.Icon = strings.TrimSpace(strings.ToLower(val)) |
||||||
|
case "include_toc": |
||||||
|
val, ok := item.Value.(bool) |
||||||
|
if !ok { |
||||||
|
continue |
||||||
|
} |
||||||
|
rc.TOC = val |
||||||
|
case "lang": |
||||||
|
val, ok := item.Value.(string) |
||||||
|
if !ok { |
||||||
|
continue |
||||||
|
} |
||||||
|
val = strings.TrimSpace(val) |
||||||
|
if len(val) == 0 { |
||||||
|
continue |
||||||
|
} |
||||||
|
rc.Lang = val |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (rc *RenderConfig) toMetaNode(meta yaml.MapSlice) ast.Node { |
||||||
|
switch rc.Meta { |
||||||
|
case "table": |
||||||
|
return metaToTable(meta) |
||||||
|
case "details": |
||||||
|
return metaToDetails(meta, rc.Icon) |
||||||
|
default: |
||||||
|
return nil |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func metaToTable(meta yaml.MapSlice) ast.Node { |
||||||
|
table := east.NewTable() |
||||||
|
alignments := []east.Alignment{} |
||||||
|
for range meta { |
||||||
|
alignments = append(alignments, east.AlignNone) |
||||||
|
} |
||||||
|
row := east.NewTableRow(alignments) |
||||||
|
for _, item := range meta { |
||||||
|
cell := east.NewTableCell() |
||||||
|
cell.AppendChild(cell, ast.NewString([]byte(fmt.Sprintf("%v", item.Key)))) |
||||||
|
row.AppendChild(row, cell) |
||||||
|
} |
||||||
|
table.AppendChild(table, east.NewTableHeader(row)) |
||||||
|
|
||||||
|
row = east.NewTableRow(alignments) |
||||||
|
for _, item := range meta { |
||||||
|
cell := east.NewTableCell() |
||||||
|
cell.AppendChild(cell, ast.NewString([]byte(fmt.Sprintf("%v", item.Value)))) |
||||||
|
row.AppendChild(row, cell) |
||||||
|
} |
||||||
|
table.AppendChild(table, row) |
||||||
|
return table |
||||||
|
} |
||||||
|
|
||||||
|
func metaToDetails(meta yaml.MapSlice, icon string) ast.Node { |
||||||
|
details := NewDetails() |
||||||
|
summary := NewSummary() |
||||||
|
summary.AppendChild(summary, NewIcon(icon)) |
||||||
|
details.AppendChild(details, summary) |
||||||
|
details.AppendChild(details, metaToTable(meta)) |
||||||
|
|
||||||
|
return details |
||||||
|
} |
@ -0,0 +1,49 @@ |
|||||||
|
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package markdown |
||||||
|
|
||||||
|
import ( |
||||||
|
"fmt" |
||||||
|
"net/url" |
||||||
|
|
||||||
|
"github.com/unknwon/i18n" |
||||||
|
"github.com/yuin/goldmark/ast" |
||||||
|
) |
||||||
|
|
||||||
|
func createTOCNode(toc []Header, lang string) ast.Node { |
||||||
|
details := NewDetails() |
||||||
|
summary := NewSummary() |
||||||
|
|
||||||
|
summary.AppendChild(summary, ast.NewString([]byte(i18n.Tr(lang, "toc")))) |
||||||
|
details.AppendChild(details, summary) |
||||||
|
ul := ast.NewList('-') |
||||||
|
details.AppendChild(details, ul) |
||||||
|
currentLevel := 6 |
||||||
|
for _, header := range toc { |
||||||
|
if header.Level < currentLevel { |
||||||
|
currentLevel = header.Level |
||||||
|
} |
||||||
|
} |
||||||
|
for _, header := range toc { |
||||||
|
for currentLevel > header.Level { |
||||||
|
ul = ul.Parent().(*ast.List) |
||||||
|
currentLevel-- |
||||||
|
} |
||||||
|
for currentLevel < header.Level { |
||||||
|
newL := ast.NewList('-') |
||||||
|
ul.AppendChild(ul, newL) |
||||||
|
currentLevel++ |
||||||
|
ul = newL |
||||||
|
} |
||||||
|
li := ast.NewListItem(currentLevel * 2) |
||||||
|
a := ast.NewLink() |
||||||
|
a.Destination = []byte(fmt.Sprintf("#%s", url.PathEscape(header.ID))) |
||||||
|
a.AppendChild(a, ast.NewString([]byte(header.Text))) |
||||||
|
li.AppendChild(li, a) |
||||||
|
ul.AppendChild(ul, li) |
||||||
|
} |
||||||
|
|
||||||
|
return details |
||||||
|
} |
Loading…
Reference in new issue