github.com/kovansky/hugo@v0.92.3-0.20220224232819-63076e4ff19f/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 "lineAnchors": true, 34 "lineNos": true, 35 "lineNoStart": true, 36 "lineNumbersInTable": true, 37 "noClasses": true, 38 "nohl": true, 39 "style": true, 40 "tabWidth": true, 41 } 42 43 func init() { 44 for k, v := range chromaHightlightProcessingAttributes { 45 chromaHightlightProcessingAttributes[strings.ToLower(k)] = v 46 } 47 } 48 49 type AttributesOwnerType int 50 51 const ( 52 AttributesOwnerGeneral AttributesOwnerType = iota 53 AttributesOwnerCodeBlock 54 ) 55 56 func New(astAttributes []ast.Attribute, ownerType AttributesOwnerType) *AttributesHolder { 57 var ( 58 attrs []Attribute 59 opts []Attribute 60 ) 61 for _, v := range astAttributes { 62 nameLower := strings.ToLower(string(v.Name)) 63 if strings.HasPrefix(string(nameLower), "on") { 64 continue 65 } 66 var vv interface{} 67 switch vvv := v.Value.(type) { 68 case bool, float64: 69 vv = vvv 70 case []interface{}: 71 // Highlight line number hlRanges. 72 var hlRanges [][2]int 73 for _, l := range vvv { 74 if ln, ok := l.(float64); ok { 75 hlRanges = append(hlRanges, [2]int{int(ln) - 1, int(ln) - 1}) 76 } else if rng, ok := l.([]uint8); ok { 77 slices := strings.Split(string([]byte(rng)), "-") 78 lhs, err := strconv.Atoi(slices[0]) 79 if err != nil { 80 continue 81 } 82 rhs := lhs 83 if len(slices) > 1 { 84 rhs, err = strconv.Atoi(slices[1]) 85 if err != nil { 86 continue 87 } 88 } 89 hlRanges = append(hlRanges, [2]int{lhs - 1, rhs - 1}) 90 } 91 } 92 vv = hlRanges 93 case []byte: 94 // Note that we don't do any HTML escaping here. 95 // We used to do that, but that changed in #9558. 96 // Noww it's up to the templates to decide. 97 vv = string(vvv) 98 default: 99 panic(fmt.Sprintf("not implemented: %T", vvv)) 100 } 101 102 if ownerType == AttributesOwnerCodeBlock && chromaHightlightProcessingAttributes[nameLower] { 103 attr := Attribute{Name: string(v.Name), Value: vv} 104 opts = append(opts, attr) 105 } else { 106 attr := Attribute{Name: nameLower, Value: vv} 107 attrs = append(attrs, attr) 108 } 109 110 } 111 112 return &AttributesHolder{ 113 attributes: attrs, 114 options: opts, 115 } 116 } 117 118 type Attribute struct { 119 Name string 120 Value interface{} 121 } 122 123 func (a Attribute) ValueString() string { 124 return cast.ToString(a.Value) 125 } 126 127 type AttributesHolder struct { 128 // What we get from Goldmark. 129 attributes []Attribute 130 131 // Attributes considered to be an option (code blocks) 132 options []Attribute 133 134 // What we send to the the render hooks. 135 attributesMapInit sync.Once 136 attributesMap map[string]interface{} 137 optionsMapInit sync.Once 138 optionsMap map[string]interface{} 139 } 140 141 type Attributes map[string]interface{} 142 143 func (a *AttributesHolder) Attributes() map[string]interface{} { 144 a.attributesMapInit.Do(func() { 145 a.attributesMap = make(map[string]interface{}) 146 for _, v := range a.attributes { 147 a.attributesMap[v.Name] = v.Value 148 } 149 }) 150 return a.attributesMap 151 } 152 153 func (a *AttributesHolder) Options() map[string]interface{} { 154 a.optionsMapInit.Do(func() { 155 a.optionsMap = make(map[string]interface{}) 156 for _, v := range a.options { 157 a.optionsMap[v.Name] = v.Value 158 } 159 }) 160 return a.optionsMap 161 } 162 163 func (a *AttributesHolder) AttributesSlice() []Attribute { 164 return a.attributes 165 } 166 167 func (a *AttributesHolder) OptionsSlice() []Attribute { 168 return a.options 169 } 170 171 // RenderASTAttributes writes the AST attributes to the given as attributes to an HTML element. 172 // This is used by the default HTML renderers, e.g. for headings etc. where no hook template could be found. 173 // This performs HTML esacaping of string attributes. 174 func RenderASTAttributes(w hugio.FlexiWriter, attributes ...ast.Attribute) { 175 for _, attr := range attributes { 176 177 a := strings.ToLower(string(attr.Name)) 178 if strings.HasPrefix(a, "on") { 179 continue 180 } 181 182 _, _ = w.WriteString(" ") 183 _, _ = w.Write(attr.Name) 184 _, _ = w.WriteString(`="`) 185 186 switch v := attr.Value.(type) { 187 case []byte: 188 _, _ = w.Write(util.EscapeHTML(v)) 189 default: 190 w.WriteString(cast.ToString(v)) 191 } 192 193 _ = w.WriteByte('"') 194 } 195 } 196 197 // Render writes the attributes to the given as attributes to an HTML element. 198 // This is used for the default codeblock renderering. 199 // This performs HTML esacaping of string attributes. 200 func RenderAttributes(w hugio.FlexiWriter, skipClass bool, attributes ...Attribute) { 201 for _, attr := range attributes { 202 a := strings.ToLower(string(attr.Name)) 203 if skipClass && a == "class" { 204 continue 205 } 206 _, _ = w.WriteString(" ") 207 _, _ = w.WriteString(attr.Name) 208 _, _ = w.WriteString(`="`) 209 210 switch v := attr.Value.(type) { 211 case []byte: 212 _, _ = w.Write(util.EscapeHTML(v)) 213 default: 214 w.WriteString(cast.ToString(v)) 215 } 216 217 _ = w.WriteByte('"') 218 } 219 }