1
0
Fork 0
forked from forgejo/forgejo

Add KaTeX rendering to Markdown. (#20571)

This PR adds mathematical rendering with KaTeX.

The first step is to add a Goldmark extension that detects the latex
(and tex) mathematics delimiters.

The second step to make this extension only run if math support is
enabled.

The second step is to then add KaTeX CSS and JS to the head which will
load after the dom is rendered.

Fix #3445

Signed-off-by: Andrew Thornton <art27@cantab.net>

Signed-off-by: Andrew Thornton <art27@cantab.net>
Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
This commit is contained in:
zeripath 2022-09-13 17:33:37 +01:00 committed by GitHub
parent eaa561145a
commit 88c2e24360
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 1079 additions and 174 deletions

View file

@ -5,159 +5,114 @@
package markdown
import (
"fmt"
"strings"
"code.gitea.io/gitea/modules/log"
"github.com/yuin/goldmark/ast"
east "github.com/yuin/goldmark/extension/ast"
"gopkg.in/yaml.v2"
"gopkg.in/yaml.v3"
)
// RenderConfig represents rendering configuration for this file
type RenderConfig struct {
Meta string
Icon string
TOC bool
Lang string
Meta string
Icon string
TOC bool
Lang string
yamlNode *yaml.Node
}
// ToRenderConfig converts a yaml.MapSlice to a RenderConfig
func (rc *RenderConfig) ToRenderConfig(meta yaml.MapSlice) {
if meta == nil {
return
// UnmarshalYAML implement yaml.v3 UnmarshalYAML
func (rc *RenderConfig) UnmarshalYAML(value *yaml.Node) error {
if rc == nil {
rc = &RenderConfig{
Meta: "table",
Icon: "table",
Lang: "",
}
}
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
}
rc.yamlNode = value
type basicRenderConfig struct {
Gitea *yaml.Node `yaml:"gitea"`
TOC bool `yaml:"include_toc"`
Lang string `yaml:"lang"`
}
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
}
}
}
var basic basicRenderConfig
err := value.Decode(&basic)
if err != nil {
return err
}
if basic.Lang != "" {
rc.Lang = basic.Lang
}
rc.TOC = basic.TOC
if basic.Gitea == nil {
return nil
}
var control *string
if err := basic.Gitea.Decode(&control); err == nil && control != nil {
log.Info("control %v", control)
switch strings.TrimSpace(strings.ToLower(*control)) {
case "none":
rc.Meta = "none"
case "table":
rc.Meta = "table"
default: // "details"
rc.Meta = "details"
}
return nil
}
type giteaControl struct {
Meta string `yaml:"meta"`
Icon string `yaml:"details_icon"`
TOC *yaml.Node `yaml:"include_toc"`
Lang string `yaml:"lang"`
}
var controlStruct *giteaControl
if err := basic.Gitea.Decode(controlStruct); err != nil || controlStruct == nil {
return err
}
switch strings.TrimSpace(strings.ToLower(controlStruct.Meta)) {
case "none":
rc.Meta = "none"
case "table":
rc.Meta = "table"
default: // "details"
rc.Meta = "details"
}
rc.Icon = strings.TrimSpace(strings.ToLower(controlStruct.Icon))
if controlStruct.Lang != "" {
rc.Lang = controlStruct.Lang
}
var toc bool
if err := controlStruct.TOC.Decode(&toc); err == nil {
rc.TOC = toc
}
return nil
}
func (rc *RenderConfig) toMetaNode(meta yaml.MapSlice) ast.Node {
func (rc *RenderConfig) toMetaNode() ast.Node {
if rc.yamlNode == nil {
return nil
}
switch rc.Meta {
case "table":
return metaToTable(meta)
return nodeToTable(rc.yamlNode)
case "details":
return metaToDetails(meta, rc.Icon)
return nodeToDetails(rc.yamlNode, 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
}