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  }