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  }