mirror of
https://github.com/gohugoio/hugo.git
synced 2025-04-27 14:10:31 +03:00
Add Markdown diagrams and render hooks for code blocks
You can now create custom hook templates for code blocks, either one for all (`render-codeblock.html`) or for a given code language (e.g. `render-codeblock-go.html`). We also used this new hook to add support for diagrams in Hugo: * Goat (Go ASCII Tool) is built-in and enabled by default; just create a fenced code block with the language `goat` and start draw your Ascii diagrams. * Another popular alternative for diagrams in Markdown, Mermaid (supported by GitHub), can also be implemented with a simple template. See the Hugo documentation for more information. Updates #7765 Closes #9538 Fixes #9553 Fixes #8520 Fixes #6702 Fixes #9558
This commit is contained in:
parent
2c20f5bc00
commit
08fdca9d93
73 changed files with 1887 additions and 1986 deletions
219
markup/internal/attributes/attributes.go
Normal file
219
markup/internal/attributes/attributes.go
Normal file
|
@ -0,0 +1,219 @@
|
|||
// Copyright 2022 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package attributes
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/gohugoio/hugo/common/hugio"
|
||||
"github.com/spf13/cast"
|
||||
"github.com/yuin/goldmark/ast"
|
||||
"github.com/yuin/goldmark/util"
|
||||
)
|
||||
|
||||
// Markdown attributes used as options by the Chroma highlighter.
|
||||
var chromaHightlightProcessingAttributes = map[string]bool{
|
||||
"anchorLineNos": true,
|
||||
"guessSyntax": true,
|
||||
"hl_Lines": true,
|
||||
"lineAnchors": true,
|
||||
"lineNos": true,
|
||||
"lineNoStart": true,
|
||||
"lineNumbersInTable": true,
|
||||
"noClasses": true,
|
||||
"nohl": true,
|
||||
"style": true,
|
||||
"tabWidth": true,
|
||||
}
|
||||
|
||||
func init() {
|
||||
for k, v := range chromaHightlightProcessingAttributes {
|
||||
chromaHightlightProcessingAttributes[strings.ToLower(k)] = v
|
||||
}
|
||||
}
|
||||
|
||||
type AttributesOwnerType int
|
||||
|
||||
const (
|
||||
AttributesOwnerGeneral AttributesOwnerType = iota
|
||||
AttributesOwnerCodeBlock
|
||||
)
|
||||
|
||||
func New(astAttributes []ast.Attribute, ownerType AttributesOwnerType) *AttributesHolder {
|
||||
var (
|
||||
attrs []Attribute
|
||||
opts []Attribute
|
||||
)
|
||||
for _, v := range astAttributes {
|
||||
nameLower := strings.ToLower(string(v.Name))
|
||||
if strings.HasPrefix(string(nameLower), "on") {
|
||||
continue
|
||||
}
|
||||
var vv interface{}
|
||||
switch vvv := v.Value.(type) {
|
||||
case bool, float64:
|
||||
vv = vvv
|
||||
case []interface{}:
|
||||
// Highlight line number hlRanges.
|
||||
var hlRanges [][2]int
|
||||
for _, l := range vvv {
|
||||
if ln, ok := l.(float64); ok {
|
||||
hlRanges = append(hlRanges, [2]int{int(ln) - 1, int(ln) - 1})
|
||||
} else if rng, ok := l.([]uint8); ok {
|
||||
slices := strings.Split(string([]byte(rng)), "-")
|
||||
lhs, err := strconv.Atoi(slices[0])
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
rhs := lhs
|
||||
if len(slices) > 1 {
|
||||
rhs, err = strconv.Atoi(slices[1])
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
}
|
||||
hlRanges = append(hlRanges, [2]int{lhs - 1, rhs - 1})
|
||||
}
|
||||
}
|
||||
vv = hlRanges
|
||||
case []byte:
|
||||
// Note that we don't do any HTML escaping here.
|
||||
// We used to do that, but that changed in #9558.
|
||||
// Noww it's up to the templates to decide.
|
||||
vv = string(vvv)
|
||||
default:
|
||||
panic(fmt.Sprintf("not implemented: %T", vvv))
|
||||
}
|
||||
|
||||
if ownerType == AttributesOwnerCodeBlock && chromaHightlightProcessingAttributes[nameLower] {
|
||||
attr := Attribute{Name: string(v.Name), Value: vv}
|
||||
opts = append(opts, attr)
|
||||
} else {
|
||||
attr := Attribute{Name: nameLower, Value: vv}
|
||||
attrs = append(attrs, attr)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return &AttributesHolder{
|
||||
attributes: attrs,
|
||||
options: opts,
|
||||
}
|
||||
}
|
||||
|
||||
type Attribute struct {
|
||||
Name string
|
||||
Value interface{}
|
||||
}
|
||||
|
||||
func (a Attribute) ValueString() string {
|
||||
return cast.ToString(a.Value)
|
||||
}
|
||||
|
||||
type AttributesHolder struct {
|
||||
// What we get from Goldmark.
|
||||
attributes []Attribute
|
||||
|
||||
// Attributes considered to be an option (code blocks)
|
||||
options []Attribute
|
||||
|
||||
// What we send to the the render hooks.
|
||||
attributesMapInit sync.Once
|
||||
attributesMap map[string]interface{}
|
||||
optionsMapInit sync.Once
|
||||
optionsMap map[string]interface{}
|
||||
}
|
||||
|
||||
type Attributes map[string]interface{}
|
||||
|
||||
func (a *AttributesHolder) Attributes() map[string]interface{} {
|
||||
a.attributesMapInit.Do(func() {
|
||||
a.attributesMap = make(map[string]interface{})
|
||||
for _, v := range a.attributes {
|
||||
a.attributesMap[v.Name] = v.Value
|
||||
}
|
||||
})
|
||||
return a.attributesMap
|
||||
}
|
||||
|
||||
func (a *AttributesHolder) Options() map[string]interface{} {
|
||||
a.optionsMapInit.Do(func() {
|
||||
a.optionsMap = make(map[string]interface{})
|
||||
for _, v := range a.options {
|
||||
a.optionsMap[v.Name] = v.Value
|
||||
}
|
||||
})
|
||||
return a.optionsMap
|
||||
}
|
||||
|
||||
func (a *AttributesHolder) AttributesSlice() []Attribute {
|
||||
return a.attributes
|
||||
}
|
||||
|
||||
func (a *AttributesHolder) OptionsSlice() []Attribute {
|
||||
return a.options
|
||||
}
|
||||
|
||||
// RenderASTAttributes writes the AST attributes to the given as attributes to an HTML element.
|
||||
// This is used by the default HTML renderers, e.g. for headings etc. where no hook template could be found.
|
||||
// This performs HTML esacaping of string attributes.
|
||||
func RenderASTAttributes(w hugio.FlexiWriter, attributes ...ast.Attribute) {
|
||||
for _, attr := range attributes {
|
||||
|
||||
a := strings.ToLower(string(attr.Name))
|
||||
if strings.HasPrefix(a, "on") {
|
||||
continue
|
||||
}
|
||||
|
||||
_, _ = w.WriteString(" ")
|
||||
_, _ = w.Write(attr.Name)
|
||||
_, _ = w.WriteString(`="`)
|
||||
|
||||
switch v := attr.Value.(type) {
|
||||
case []byte:
|
||||
_, _ = w.Write(util.EscapeHTML(v))
|
||||
default:
|
||||
w.WriteString(cast.ToString(v))
|
||||
}
|
||||
|
||||
_ = w.WriteByte('"')
|
||||
}
|
||||
}
|
||||
|
||||
// Render writes the attributes to the given as attributes to an HTML element.
|
||||
// This is used for the default codeblock renderering.
|
||||
// This performs HTML esacaping of string attributes.
|
||||
func RenderAttributes(w hugio.FlexiWriter, skipClass bool, attributes ...Attribute) {
|
||||
for _, attr := range attributes {
|
||||
a := strings.ToLower(string(attr.Name))
|
||||
if skipClass && a == "class" {
|
||||
continue
|
||||
}
|
||||
_, _ = w.WriteString(" ")
|
||||
_, _ = w.WriteString(attr.Name)
|
||||
_, _ = w.WriteString(`="`)
|
||||
|
||||
switch v := attr.Value.(type) {
|
||||
case []byte:
|
||||
_, _ = w.Write(util.EscapeHTML(v))
|
||||
default:
|
||||
w.WriteString(cast.ToString(v))
|
||||
}
|
||||
|
||||
_ = w.WriteByte('"')
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue