github.com/linchen2chris/hugo@v0.0.0-20230307053224-cec209389705/markup/internal/attributes/attributes.go (about) 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 // Empty holds no attributes. 130 var Empty = &AttributesHolder{} 131 132 type AttributesHolder struct { 133 // What we get from Goldmark. 134 attributes []Attribute 135 136 // Attributes considered to be an option (code blocks) 137 options []Attribute 138 139 // What we send to the the render hooks. 140 attributesMapInit sync.Once 141 attributesMap map[string]any 142 optionsMapInit sync.Once 143 optionsMap map[string]any 144 } 145 146 type Attributes map[string]any 147 148 func (a *AttributesHolder) Attributes() map[string]any { 149 a.attributesMapInit.Do(func() { 150 a.attributesMap = make(map[string]any) 151 for _, v := range a.attributes { 152 a.attributesMap[v.Name] = v.Value 153 } 154 }) 155 return a.attributesMap 156 } 157 158 func (a *AttributesHolder) Options() map[string]any { 159 a.optionsMapInit.Do(func() { 160 a.optionsMap = make(map[string]any) 161 for _, v := range a.options { 162 a.optionsMap[v.Name] = v.Value 163 } 164 }) 165 return a.optionsMap 166 } 167 168 func (a *AttributesHolder) AttributesSlice() []Attribute { 169 return a.attributes 170 } 171 172 func (a *AttributesHolder) OptionsSlice() []Attribute { 173 return a.options 174 } 175 176 // RenderASTAttributes writes the AST attributes to the given as attributes to an HTML element. 177 // This is used by the default HTML renderers, e.g. for headings etc. where no hook template could be found. 178 // This performs HTML esacaping of string attributes. 179 func RenderASTAttributes(w hugio.FlexiWriter, attributes ...ast.Attribute) { 180 for _, attr := range attributes { 181 182 a := strings.ToLower(string(attr.Name)) 183 if strings.HasPrefix(a, "on") { 184 continue 185 } 186 187 _, _ = w.WriteString(" ") 188 _, _ = w.Write(attr.Name) 189 _, _ = w.WriteString(`="`) 190 191 switch v := attr.Value.(type) { 192 case []byte: 193 _, _ = w.Write(util.EscapeHTML(v)) 194 default: 195 w.WriteString(cast.ToString(v)) 196 } 197 198 _ = w.WriteByte('"') 199 } 200 } 201 202 // Render writes the attributes to the given as attributes to an HTML element. 203 // This is used for the default codeblock rendering. 204 // This performs HTML escaping of string attributes. 205 func RenderAttributes(w hugio.FlexiWriter, skipClass bool, attributes ...Attribute) { 206 for _, attr := range attributes { 207 a := strings.ToLower(string(attr.Name)) 208 if skipClass && a == "class" { 209 continue 210 } 211 _, _ = w.WriteString(" ") 212 _, _ = w.WriteString(attr.Name) 213 _, _ = w.WriteString(`="`) 214 215 switch v := attr.Value.(type) { 216 case []byte: 217 _, _ = w.Write(util.EscapeHTML(v)) 218 default: 219 w.WriteString(cast.ToString(v)) 220 } 221 222 _ = w.WriteByte('"') 223 } 224 }