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 }