github.com/gohugoio/hugo@v0.88.1/markup/goldmark/convert.go (about)

     1  // Copyright 2019 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 goldmark converts Markdown to HTML using Goldmark.
    15  package goldmark
    16  
    17  import (
    18  	"bytes"
    19  	"fmt"
    20  	"math/bits"
    21  	"path/filepath"
    22  	"runtime/debug"
    23  
    24  	"github.com/gohugoio/hugo/markup/goldmark/internal/extensions/attributes"
    25  	"github.com/yuin/goldmark/ast"
    26  
    27  	"github.com/gohugoio/hugo/identity"
    28  
    29  	"github.com/pkg/errors"
    30  
    31  	"github.com/spf13/afero"
    32  
    33  	"github.com/gohugoio/hugo/hugofs"
    34  	"github.com/gohugoio/hugo/markup/converter"
    35  	"github.com/gohugoio/hugo/markup/highlight"
    36  	"github.com/gohugoio/hugo/markup/tableofcontents"
    37  	"github.com/yuin/goldmark"
    38  	hl "github.com/yuin/goldmark-highlighting"
    39  	"github.com/yuin/goldmark/extension"
    40  	"github.com/yuin/goldmark/parser"
    41  	"github.com/yuin/goldmark/renderer"
    42  	"github.com/yuin/goldmark/renderer/html"
    43  	"github.com/yuin/goldmark/text"
    44  	"github.com/yuin/goldmark/util"
    45  )
    46  
    47  // Provider is the package entry point.
    48  var Provider converter.ProviderProvider = provide{}
    49  
    50  type provide struct {
    51  }
    52  
    53  func (p provide) New(cfg converter.ProviderConfig) (converter.Provider, error) {
    54  	md := newMarkdown(cfg)
    55  
    56  	return converter.NewProvider("goldmark", func(ctx converter.DocumentContext) (converter.Converter, error) {
    57  		return &goldmarkConverter{
    58  			ctx: ctx,
    59  			cfg: cfg,
    60  			md:  md,
    61  			sanitizeAnchorName: func(s string) string {
    62  				return sanitizeAnchorNameString(s, cfg.MarkupConfig.Goldmark.Parser.AutoHeadingIDType)
    63  			},
    64  		}, nil
    65  	}), nil
    66  }
    67  
    68  var _ converter.AnchorNameSanitizer = (*goldmarkConverter)(nil)
    69  
    70  type goldmarkConverter struct {
    71  	md  goldmark.Markdown
    72  	ctx converter.DocumentContext
    73  	cfg converter.ProviderConfig
    74  
    75  	sanitizeAnchorName func(s string) string
    76  }
    77  
    78  func (c *goldmarkConverter) SanitizeAnchorName(s string) string {
    79  	return c.sanitizeAnchorName(s)
    80  }
    81  
    82  func newMarkdown(pcfg converter.ProviderConfig) goldmark.Markdown {
    83  	mcfg := pcfg.MarkupConfig
    84  	cfg := pcfg.MarkupConfig.Goldmark
    85  	var rendererOptions []renderer.Option
    86  
    87  	if cfg.Renderer.HardWraps {
    88  		rendererOptions = append(rendererOptions, html.WithHardWraps())
    89  	}
    90  
    91  	if cfg.Renderer.XHTML {
    92  		rendererOptions = append(rendererOptions, html.WithXHTML())
    93  	}
    94  
    95  	if cfg.Renderer.Unsafe {
    96  		rendererOptions = append(rendererOptions, html.WithUnsafe())
    97  	}
    98  
    99  	var (
   100  		extensions = []goldmark.Extender{
   101  			newLinks(),
   102  			newTocExtension(rendererOptions),
   103  		}
   104  		parserOptions []parser.Option
   105  	)
   106  
   107  	if mcfg.Highlight.CodeFences {
   108  		extensions = append(extensions, newHighlighting(mcfg.Highlight))
   109  	}
   110  
   111  	if cfg.Extensions.Table {
   112  		extensions = append(extensions, extension.Table)
   113  	}
   114  
   115  	if cfg.Extensions.Strikethrough {
   116  		extensions = append(extensions, extension.Strikethrough)
   117  	}
   118  
   119  	if cfg.Extensions.Linkify {
   120  		extensions = append(extensions, extension.Linkify)
   121  	}
   122  
   123  	if cfg.Extensions.TaskList {
   124  		extensions = append(extensions, extension.TaskList)
   125  	}
   126  
   127  	if cfg.Extensions.Typographer {
   128  		extensions = append(extensions, extension.Typographer)
   129  	}
   130  
   131  	if cfg.Extensions.DefinitionList {
   132  		extensions = append(extensions, extension.DefinitionList)
   133  	}
   134  
   135  	if cfg.Extensions.Footnote {
   136  		extensions = append(extensions, extension.Footnote)
   137  	}
   138  
   139  	if cfg.Parser.AutoHeadingID {
   140  		parserOptions = append(parserOptions, parser.WithAutoHeadingID())
   141  	}
   142  
   143  	if cfg.Parser.Attribute.Title {
   144  		parserOptions = append(parserOptions, parser.WithAttribute())
   145  	}
   146  
   147  	if cfg.Parser.Attribute.Block {
   148  		extensions = append(extensions, attributes.New())
   149  	}
   150  
   151  	md := goldmark.New(
   152  		goldmark.WithExtensions(
   153  			extensions...,
   154  		),
   155  		goldmark.WithParserOptions(
   156  			parserOptions...,
   157  		),
   158  		goldmark.WithRendererOptions(
   159  			rendererOptions...,
   160  		),
   161  	)
   162  
   163  	return md
   164  }
   165  
   166  var _ identity.IdentitiesProvider = (*converterResult)(nil)
   167  
   168  type converterResult struct {
   169  	converter.Result
   170  	toc tableofcontents.Root
   171  	ids identity.Identities
   172  }
   173  
   174  func (c converterResult) TableOfContents() tableofcontents.Root {
   175  	return c.toc
   176  }
   177  
   178  func (c converterResult) GetIdentities() identity.Identities {
   179  	return c.ids
   180  }
   181  
   182  type bufWriter struct {
   183  	*bytes.Buffer
   184  }
   185  
   186  const maxInt = 1<<(bits.UintSize-1) - 1
   187  
   188  func (b *bufWriter) Available() int {
   189  	return maxInt
   190  }
   191  
   192  func (b *bufWriter) Buffered() int {
   193  	return b.Len()
   194  }
   195  
   196  func (b *bufWriter) Flush() error {
   197  	return nil
   198  }
   199  
   200  type renderContext struct {
   201  	*bufWriter
   202  	pos int
   203  	renderContextData
   204  }
   205  
   206  type renderContextData interface {
   207  	RenderContext() converter.RenderContext
   208  	DocumentContext() converter.DocumentContext
   209  	AddIdentity(id identity.Provider)
   210  }
   211  
   212  type renderContextDataHolder struct {
   213  	rctx converter.RenderContext
   214  	dctx converter.DocumentContext
   215  	ids  identity.Manager
   216  }
   217  
   218  func (ctx *renderContextDataHolder) RenderContext() converter.RenderContext {
   219  	return ctx.rctx
   220  }
   221  
   222  func (ctx *renderContextDataHolder) DocumentContext() converter.DocumentContext {
   223  	return ctx.dctx
   224  }
   225  
   226  func (ctx *renderContextDataHolder) AddIdentity(id identity.Provider) {
   227  	ctx.ids.Add(id)
   228  }
   229  
   230  var converterIdentity = identity.KeyValueIdentity{Key: "goldmark", Value: "converter"}
   231  
   232  func (c *goldmarkConverter) Convert(ctx converter.RenderContext) (result converter.Result, err error) {
   233  	defer func() {
   234  		if r := recover(); r != nil {
   235  			dir := afero.GetTempDir(hugofs.Os, "hugo_bugs")
   236  			name := fmt.Sprintf("goldmark_%s.txt", c.ctx.DocumentID)
   237  			filename := filepath.Join(dir, name)
   238  			afero.WriteFile(hugofs.Os, filename, ctx.Src, 07555)
   239  			fmt.Print(string(debug.Stack()))
   240  			err = errors.Errorf("[BUG] goldmark: %s: create an issue on GitHub attaching the file in: %s", r, filename)
   241  		}
   242  	}()
   243  
   244  	buf := &bufWriter{Buffer: &bytes.Buffer{}}
   245  	result = buf
   246  	pctx := c.newParserContext(ctx)
   247  	reader := text.NewReader(ctx.Src)
   248  
   249  	doc := c.md.Parser().Parse(
   250  		reader,
   251  		parser.WithContext(pctx),
   252  	)
   253  
   254  	rcx := &renderContextDataHolder{
   255  		rctx: ctx,
   256  		dctx: c.ctx,
   257  		ids:  identity.NewManager(converterIdentity),
   258  	}
   259  
   260  	w := &renderContext{
   261  		bufWriter:         buf,
   262  		renderContextData: rcx,
   263  	}
   264  
   265  	if err := c.md.Renderer().Render(w, ctx.Src, doc); err != nil {
   266  		return nil, err
   267  	}
   268  
   269  	return converterResult{
   270  		Result: buf,
   271  		ids:    rcx.ids.GetIdentities(),
   272  		toc:    pctx.TableOfContents(),
   273  	}, nil
   274  }
   275  
   276  var featureSet = map[identity.Identity]bool{
   277  	converter.FeatureRenderHooks: true,
   278  }
   279  
   280  func (c *goldmarkConverter) Supports(feature identity.Identity) bool {
   281  	return featureSet[feature.GetIdentity()]
   282  }
   283  
   284  func (c *goldmarkConverter) newParserContext(rctx converter.RenderContext) *parserContext {
   285  	ctx := parser.NewContext(parser.WithIDs(newIDFactory(c.cfg.MarkupConfig.Goldmark.Parser.AutoHeadingIDType)))
   286  	ctx.Set(tocEnableKey, rctx.RenderTOC)
   287  	return &parserContext{
   288  		Context: ctx,
   289  	}
   290  }
   291  
   292  type parserContext struct {
   293  	parser.Context
   294  }
   295  
   296  func (p *parserContext) TableOfContents() tableofcontents.Root {
   297  	if v := p.Get(tocResultKey); v != nil {
   298  		return v.(tableofcontents.Root)
   299  	}
   300  	return tableofcontents.Root{}
   301  }
   302  
   303  func newHighlighting(cfg highlight.Config) goldmark.Extender {
   304  	return hl.NewHighlighting(
   305  		hl.WithStyle(cfg.Style),
   306  		hl.WithGuessLanguage(cfg.GuessSyntax),
   307  		hl.WithCodeBlockOptions(highlight.GetCodeBlockOptions()),
   308  		hl.WithFormatOptions(
   309  			cfg.ToHTMLOptions()...,
   310  		),
   311  
   312  		hl.WithWrapperRenderer(func(w util.BufWriter, ctx hl.CodeBlockContext, entering bool) {
   313  			var language string
   314  			if l, hasLang := ctx.Language(); hasLang {
   315  				language = string(l)
   316  			}
   317  
   318  			if ctx.Highlighted() {
   319  				if entering {
   320  					writeDivStart(w, ctx)
   321  				} else {
   322  					writeDivEnd(w)
   323  				}
   324  			} else {
   325  				if entering {
   326  					highlight.WritePreStart(w, language, "")
   327  				} else {
   328  					highlight.WritePreEnd(w)
   329  				}
   330  			}
   331  		}),
   332  	)
   333  }
   334  
   335  func writeDivStart(w util.BufWriter, ctx hl.CodeBlockContext) {
   336  	w.WriteString(`<div class="highlight`)
   337  
   338  	var attributes []ast.Attribute
   339  	if ctx.Attributes() != nil {
   340  		attributes = ctx.Attributes().All()
   341  	}
   342  
   343  	if attributes != nil {
   344  		class, found := ctx.Attributes().GetString("class")
   345  		if found {
   346  			w.WriteString(" ")
   347  			w.Write(util.EscapeHTML(class.([]byte)))
   348  
   349  		}
   350  		_, _ = w.WriteString("\"")
   351  		renderAttributes(w, true, attributes...)
   352  	} else {
   353  		_, _ = w.WriteString("\"")
   354  	}
   355  
   356  	w.WriteString(">")
   357  }
   358  
   359  func writeDivEnd(w util.BufWriter) {
   360  	w.WriteString("</div>")
   361  }