attributes.go (5666B)
1 // Copyright 2022 The Hugo Authors. All rights reserved.
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 // http://www.apache.org/licenses/LICENSE-2.0
7 //
8 // Unless required by applicable law or agreed to in writing, software
9 // distributed under the License is distributed on an "AS IS" BASIS,
10 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 // See the License for the specific language governing permissions and
12 // limitations under the License.
13
14 package attributes
15
16 import (
17 "fmt"
18 "strconv"
19 "strings"
20 "sync"
21
22 "github.com/gohugoio/hugo/common/hugio"
23 "github.com/spf13/cast"
24 "github.com/yuin/goldmark/ast"
25 "github.com/yuin/goldmark/util"
26 )
27
28 // Markdown attributes used as options by the Chroma highlighter.
29 var chromaHightlightProcessingAttributes = map[string]bool{
30 "anchorLineNos": true,
31 "guessSyntax": true,
32 "hl_Lines": true,
33 "hl_inline": true,
34 "lineAnchors": true,
35 "lineNos": true,
36 "lineNoStart": true,
37 "lineNumbersInTable": true,
38 "noClasses": true,
39 "nohl": true,
40 "style": true,
41 "tabWidth": true,
42 }
43
44 func init() {
45 for k, v := range chromaHightlightProcessingAttributes {
46 chromaHightlightProcessingAttributes[strings.ToLower(k)] = v
47 }
48 }
49
50 type AttributesOwnerType int
51
52 const (
53 AttributesOwnerGeneral AttributesOwnerType = iota
54 AttributesOwnerCodeBlockChroma
55 AttributesOwnerCodeBlockCustom
56 )
57
58 func New(astAttributes []ast.Attribute, ownerType AttributesOwnerType) *AttributesHolder {
59 var (
60 attrs []Attribute
61 opts []Attribute
62 )
63 for _, v := range astAttributes {
64 nameLower := strings.ToLower(string(v.Name))
65 if strings.HasPrefix(string(nameLower), "on") {
66 continue
67 }
68 var vv any
69 switch vvv := v.Value.(type) {
70 case bool, float64:
71 vv = vvv
72 case []any:
73 // Highlight line number hlRanges.
74 var hlRanges [][2]int
75 for _, l := range vvv {
76 if ln, ok := l.(float64); ok {
77 hlRanges = append(hlRanges, [2]int{int(ln) - 1, int(ln) - 1})
78 } else if rng, ok := l.([]uint8); ok {
79 slices := strings.Split(string([]byte(rng)), "-")
80 lhs, err := strconv.Atoi(slices[0])
81 if err != nil {
82 continue
83 }
84 rhs := lhs
85 if len(slices) > 1 {
86 rhs, err = strconv.Atoi(slices[1])
87 if err != nil {
88 continue
89 }
90 }
91 hlRanges = append(hlRanges, [2]int{lhs - 1, rhs - 1})
92 }
93 }
94 vv = hlRanges
95 case []byte:
96 // Note that we don't do any HTML escaping here.
97 // We used to do that, but that changed in #9558.
98 // Noww it's up to the templates to decide.
99 vv = string(vvv)
100 default:
101 panic(fmt.Sprintf("not implemented: %T", vvv))
102 }
103
104 if ownerType == AttributesOwnerCodeBlockChroma && chromaHightlightProcessingAttributes[nameLower] {
105 attr := Attribute{Name: string(v.Name), Value: vv}
106 opts = append(opts, attr)
107 } else {
108 attr := Attribute{Name: nameLower, Value: vv}
109 attrs = append(attrs, attr)
110 }
111
112 }
113
114 return &AttributesHolder{
115 attributes: attrs,
116 options: opts,
117 }
118 }
119
120 type Attribute struct {
121 Name string
122 Value any
123 }
124
125 func (a Attribute) ValueString() string {
126 return cast.ToString(a.Value)
127 }
128
129 type AttributesHolder struct {
130 // What we get from Goldmark.
131 attributes []Attribute
132
133 // Attributes considered to be an option (code blocks)
134 options []Attribute
135
136 // What we send to the the render hooks.
137 attributesMapInit sync.Once
138 attributesMap map[string]any
139 optionsMapInit sync.Once
140 optionsMap map[string]any
141 }
142
143 type Attributes map[string]any
144
145 func (a *AttributesHolder) Attributes() map[string]any {
146 a.attributesMapInit.Do(func() {
147 a.attributesMap = make(map[string]any)
148 for _, v := range a.attributes {
149 a.attributesMap[v.Name] = v.Value
150 }
151 })
152 return a.attributesMap
153 }
154
155 func (a *AttributesHolder) Options() map[string]any {
156 a.optionsMapInit.Do(func() {
157 a.optionsMap = make(map[string]any)
158 for _, v := range a.options {
159 a.optionsMap[v.Name] = v.Value
160 }
161 })
162 return a.optionsMap
163 }
164
165 func (a *AttributesHolder) AttributesSlice() []Attribute {
166 return a.attributes
167 }
168
169 func (a *AttributesHolder) OptionsSlice() []Attribute {
170 return a.options
171 }
172
173 // RenderASTAttributes writes the AST attributes to the given as attributes to an HTML element.
174 // This is used by the default HTML renderers, e.g. for headings etc. where no hook template could be found.
175 // This performs HTML esacaping of string attributes.
176 func RenderASTAttributes(w hugio.FlexiWriter, attributes ...ast.Attribute) {
177 for _, attr := range attributes {
178
179 a := strings.ToLower(string(attr.Name))
180 if strings.HasPrefix(a, "on") {
181 continue
182 }
183
184 _, _ = w.WriteString(" ")
185 _, _ = w.Write(attr.Name)
186 _, _ = w.WriteString(`="`)
187
188 switch v := attr.Value.(type) {
189 case []byte:
190 _, _ = w.Write(util.EscapeHTML(v))
191 default:
192 w.WriteString(cast.ToString(v))
193 }
194
195 _ = w.WriteByte('"')
196 }
197 }
198
199 // Render writes the attributes to the given as attributes to an HTML element.
200 // This is used for the default codeblock renderering.
201 // This performs HTML esacaping of string attributes.
202 func RenderAttributes(w hugio.FlexiWriter, skipClass bool, attributes ...Attribute) {
203 for _, attr := range attributes {
204 a := strings.ToLower(string(attr.Name))
205 if skipClass && a == "class" {
206 continue
207 }
208 _, _ = w.WriteString(" ")
209 _, _ = w.WriteString(attr.Name)
210 _, _ = w.WriteString(`="`)
211
212 switch v := attr.Value.(type) {
213 case []byte:
214 _, _ = w.Write(util.EscapeHTML(v))
215 default:
216 w.WriteString(cast.ToString(v))
217 }
218
219 _ = w.WriteByte('"')
220 }
221 }