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