diff options
author | Bjørn Erik Pedersen <[email protected]> | 2022-02-17 13:04:00 +0100 |
---|---|---|
committer | Bjørn Erik Pedersen <[email protected]> | 2022-02-24 18:59:50 +0100 |
commit | 08fdca9d9365eaf1e496a12e2af5e18617bd0e66 (patch) | |
tree | 6c6942d1b74a4160d93a997860bafd52b92025f5 /markup/internal | |
parent | 2c20f5bc00b604e72b3b7e401fbdbf9447fe3470 (diff) | |
download | hugo-08fdca9d9365eaf1e496a12e2af5e18617bd0e66.tar.gz hugo-08fdca9d9365eaf1e496a12e2af5e18617bd0e66.zip |
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
Diffstat (limited to 'markup/internal')
-rw-r--r-- | markup/internal/attributes/attributes.go | 219 |
1 files changed, 219 insertions, 0 deletions
diff --git a/markup/internal/attributes/attributes.go b/markup/internal/attributes/attributes.go new file mode 100644 index 000000000..1cce7edd1 --- /dev/null +++ b/markup/internal/attributes/attributes.go @@ -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('"') + } +} |