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