hugo/markup/internal/attributes/attributes.go
2024-03-15 17:25:52 +01:00

224 lines
5.6 KiB
Go

// Copyright 2024 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 chromaHighlightProcessingAttributes = map[string]bool{
"anchorLineNos": true,
"guessSyntax": true,
"hl_Lines": true,
"hl_inline": true,
"lineAnchors": true,
"lineNos": true,
"lineNoStart": true,
"lineNumbersInTable": true,
"noClasses": true,
"nohl": true,
"style": true,
"tabWidth": true,
}
func init() {
for k, v := range chromaHighlightProcessingAttributes {
chromaHighlightProcessingAttributes[strings.ToLower(k)] = v
}
}
type AttributesOwnerType int
const (
AttributesOwnerGeneral AttributesOwnerType = iota
AttributesOwnerCodeBlockChroma
AttributesOwnerCodeBlockCustom
)
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 any
switch vvv := v.Value.(type) {
case bool, float64:
vv = vvv
case []any:
// 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.
// Now it's up to the templates to decide.
vv = string(vvv)
default:
panic(fmt.Sprintf("not implemented: %T", vvv))
}
if ownerType == AttributesOwnerCodeBlockChroma && chromaHighlightProcessingAttributes[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 any
}
func (a Attribute) ValueString() string {
return cast.ToString(a.Value)
}
// Empty holds no attributes.
var Empty = &AttributesHolder{}
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]any
optionsMapInit sync.Once
optionsMap map[string]any
}
type Attributes map[string]any
func (a *AttributesHolder) Attributes() map[string]any {
a.attributesMapInit.Do(func() {
a.attributesMap = make(map[string]any)
for _, v := range a.attributes {
a.attributesMap[v.Name] = v.Value
}
})
return a.attributesMap
}
func (a *AttributesHolder) Options() map[string]any {
a.optionsMapInit.Do(func() {
a.optionsMap = make(map[string]any)
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 escaping 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('"')
}
}
// RenderAttributes Render writes the attributes to the given as attributes to an HTML element.
// This is used for the default codeblock rendering.
// This performs HTML escaping 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('"')
}
}