github.com/anakojm/hugo-katex@v0.0.0-20231023141351-42d6f5de9c0b/markup/goldmark/codeblocks/render.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 codeblocks
    15  
    16  import (
    17  	"bytes"
    18  	"errors"
    19  	"fmt"
    20  	"strings"
    21  	"sync"
    22  
    23  	"github.com/gohugoio/hugo/common/herrors"
    24  	htext "github.com/gohugoio/hugo/common/text"
    25  	"github.com/gohugoio/hugo/markup/converter/hooks"
    26  	"github.com/gohugoio/hugo/markup/goldmark/internal/render"
    27  	"github.com/gohugoio/hugo/markup/highlight/chromalexers"
    28  	"github.com/gohugoio/hugo/markup/internal/attributes"
    29  	"github.com/yuin/goldmark"
    30  	"github.com/yuin/goldmark/ast"
    31  	"github.com/yuin/goldmark/parser"
    32  	"github.com/yuin/goldmark/renderer"
    33  	"github.com/yuin/goldmark/text"
    34  	"github.com/yuin/goldmark/util"
    35  )
    36  
    37  type (
    38  	codeBlocksExtension struct{}
    39  	htmlRenderer        struct{}
    40  )
    41  
    42  func New() goldmark.Extender {
    43  	return &codeBlocksExtension{}
    44  }
    45  
    46  func (e *codeBlocksExtension) Extend(m goldmark.Markdown) {
    47  	m.Parser().AddOptions(
    48  		parser.WithASTTransformers(
    49  			util.Prioritized(&Transformer{}, 100),
    50  		),
    51  	)
    52  	m.Renderer().AddOptions(renderer.WithNodeRenderers(
    53  		util.Prioritized(newHTMLRenderer(), 100),
    54  	))
    55  }
    56  
    57  func newHTMLRenderer() renderer.NodeRenderer {
    58  	r := &htmlRenderer{}
    59  	return r
    60  }
    61  
    62  func (r *htmlRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
    63  	reg.Register(KindCodeBlock, r.renderCodeBlock)
    64  }
    65  
    66  func (r *htmlRenderer) renderCodeBlock(w util.BufWriter, src []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
    67  	ctx := w.(*render.Context)
    68  
    69  	if entering {
    70  		return ast.WalkContinue, nil
    71  	}
    72  
    73  	n := node.(*codeBlock)
    74  	lang := getLang(n.b, src)
    75  	renderer := ctx.RenderContext().GetRenderer(hooks.CodeBlockRendererType, lang)
    76  	if renderer == nil {
    77  		return ast.WalkStop, fmt.Errorf("no code renderer found for %q", lang)
    78  	}
    79  
    80  	ordinal := n.ordinal
    81  
    82  	var buff bytes.Buffer
    83  
    84  	l := n.b.Lines().Len()
    85  	for i := 0; i < l; i++ {
    86  		line := n.b.Lines().At(i)
    87  		buff.Write(line.Value(src))
    88  	}
    89  
    90  	s := htext.Chomp(buff.String())
    91  
    92  	var info []byte
    93  	if n.b.Info != nil {
    94  		info = n.b.Info.Segment.Value(src)
    95  	}
    96  
    97  	attrtp := attributes.AttributesOwnerCodeBlockCustom
    98  	if isd, ok := renderer.(hooks.IsDefaultCodeBlockRendererProvider); (ok && isd.IsDefaultCodeBlockRenderer()) || chromalexers.Get(lang) != nil {
    99  		// We say that this is a Chroma code block if it's the default code block renderer
   100  		// or if the language is supported by Chroma.
   101  		attrtp = attributes.AttributesOwnerCodeBlockChroma
   102  	}
   103  
   104  	// IsDefaultCodeBlockRendererProvider
   105  	attrs, attrStr, err := getAttributes(n.b, info)
   106  	if err != nil {
   107  		return ast.WalkStop, &herrors.TextSegmentError{Err: err, Segment: attrStr}
   108  	}
   109  	cbctx := &codeBlockContext{
   110  		page:             ctx.DocumentContext().Document,
   111  		lang:             lang,
   112  		code:             s,
   113  		ordinal:          ordinal,
   114  		AttributesHolder: attributes.New(attrs, attrtp),
   115  	}
   116  
   117  	cbctx.createPos = func() htext.Position {
   118  		if resolver, ok := renderer.(hooks.ElementPositionResolver); ok {
   119  			return resolver.ResolvePosition(cbctx)
   120  		}
   121  		return htext.Position{
   122  			Filename:     ctx.DocumentContext().Filename,
   123  			LineNumber:   1,
   124  			ColumnNumber: 1,
   125  		}
   126  	}
   127  
   128  	cr := renderer.(hooks.CodeBlockRenderer)
   129  
   130  	err = cr.RenderCodeblock(
   131  		ctx.RenderContext().Ctx,
   132  		w,
   133  		cbctx,
   134  	)
   135  
   136  	ctx.AddIdentity(cr)
   137  
   138  	if err != nil {
   139  		return ast.WalkContinue, herrors.NewFileErrorFromPos(err, cbctx.createPos())
   140  	}
   141  
   142  	return ast.WalkContinue, nil
   143  }
   144  
   145  type codeBlockContext struct {
   146  	page    any
   147  	lang    string
   148  	code    string
   149  	ordinal int
   150  
   151  	// This is only used in error situations and is expensive to create,
   152  	// to deleay creation until needed.
   153  	pos       htext.Position
   154  	posInit   sync.Once
   155  	createPos func() htext.Position
   156  
   157  	*attributes.AttributesHolder
   158  }
   159  
   160  func (c *codeBlockContext) Page() any {
   161  	return c.page
   162  }
   163  
   164  func (c *codeBlockContext) Type() string {
   165  	return c.lang
   166  }
   167  
   168  func (c *codeBlockContext) Inner() string {
   169  	return c.code
   170  }
   171  
   172  func (c *codeBlockContext) Ordinal() int {
   173  	return c.ordinal
   174  }
   175  
   176  func (c *codeBlockContext) Position() htext.Position {
   177  	c.posInit.Do(func() {
   178  		c.pos = c.createPos()
   179  	})
   180  	return c.pos
   181  }
   182  
   183  func getLang(node *ast.FencedCodeBlock, src []byte) string {
   184  	langWithAttributes := string(node.Language(src))
   185  	lang, _, _ := strings.Cut(langWithAttributes, "{")
   186  	return lang
   187  }
   188  
   189  func getAttributes(node *ast.FencedCodeBlock, infostr []byte) ([]ast.Attribute, string, error) {
   190  	if node.Attributes() != nil {
   191  		return node.Attributes(), "", nil
   192  	}
   193  	if infostr != nil {
   194  		attrStartIdx := -1
   195  		attrEndIdx := -1
   196  
   197  		for idx, char := range infostr {
   198  			if attrEndIdx == -1 && char == '{' {
   199  				attrStartIdx = idx
   200  			}
   201  			if attrStartIdx != -1 && char == '}' {
   202  				attrEndIdx = idx
   203  				break
   204  			}
   205  		}
   206  
   207  		if attrStartIdx != -1 && attrEndIdx != -1 {
   208  			n := ast.NewTextBlock() // dummy node for storing attributes
   209  			attrStr := infostr[attrStartIdx : attrEndIdx+1]
   210  			if attrs, hasAttr := parser.ParseAttributes(text.NewReader(attrStr)); hasAttr {
   211  				for _, attr := range attrs {
   212  					n.SetAttribute(attr.Name, attr.Value)
   213  				}
   214  				return n.Attributes(), "", nil
   215  			} else {
   216  				return nil, string(attrStr), errors.New("failed to parse Markdown attributes; you may need to quote the values")
   217  			}
   218  		}
   219  	}
   220  	return nil, "", nil
   221  }