mirror of
https://github.com/gohugoio/hugo.git
synced 2024-12-18 07:56:50 -05:00
339 lines
8.9 KiB
Go
339 lines
8.9 KiB
Go
// Copyright 2019 The Hugo Authors. All rights reserved.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
// Package goldmark converts Markdown to HTML using Goldmark.
|
|
package goldmark
|
|
|
|
import (
|
|
"bytes"
|
|
|
|
"github.com/gohugoio/hugo-goldmark-extensions/passthrough"
|
|
"github.com/yuin/goldmark/util"
|
|
|
|
"github.com/gohugoio/hugo/markup/goldmark/codeblocks"
|
|
"github.com/gohugoio/hugo/markup/goldmark/goldmark_config"
|
|
"github.com/gohugoio/hugo/markup/goldmark/images"
|
|
"github.com/gohugoio/hugo/markup/goldmark/internal/extensions/attributes"
|
|
"github.com/gohugoio/hugo/markup/goldmark/internal/render"
|
|
|
|
"github.com/yuin/goldmark"
|
|
emoji "github.com/yuin/goldmark-emoji"
|
|
"github.com/yuin/goldmark/ast"
|
|
"github.com/yuin/goldmark/extension"
|
|
"github.com/yuin/goldmark/parser"
|
|
"github.com/yuin/goldmark/renderer"
|
|
"github.com/yuin/goldmark/renderer/html"
|
|
"github.com/yuin/goldmark/text"
|
|
|
|
"github.com/gohugoio/hugo/markup/converter"
|
|
"github.com/gohugoio/hugo/markup/tableofcontents"
|
|
)
|
|
|
|
const (
|
|
internalAttrPrefix = "_h__"
|
|
)
|
|
|
|
// Provider is the package entry point.
|
|
var Provider converter.ProviderProvider = provide{}
|
|
|
|
type provide struct{}
|
|
|
|
func (p provide) New(cfg converter.ProviderConfig) (converter.Provider, error) {
|
|
md := newMarkdown(cfg)
|
|
|
|
return converter.NewProvider("goldmark", func(ctx converter.DocumentContext) (converter.Converter, error) {
|
|
return &goldmarkConverter{
|
|
ctx: ctx,
|
|
cfg: cfg,
|
|
md: md,
|
|
sanitizeAnchorName: func(s string) string {
|
|
return sanitizeAnchorNameString(s, cfg.MarkupConfig().Goldmark.Parser.AutoHeadingIDType)
|
|
},
|
|
}, nil
|
|
}), nil
|
|
}
|
|
|
|
var _ converter.AnchorNameSanitizer = (*goldmarkConverter)(nil)
|
|
|
|
type goldmarkConverter struct {
|
|
md goldmark.Markdown
|
|
ctx converter.DocumentContext
|
|
cfg converter.ProviderConfig
|
|
|
|
sanitizeAnchorName func(s string) string
|
|
}
|
|
|
|
func (c *goldmarkConverter) SanitizeAnchorName(s string) string {
|
|
return c.sanitizeAnchorName(s)
|
|
}
|
|
|
|
func newMarkdown(pcfg converter.ProviderConfig) goldmark.Markdown {
|
|
mcfg := pcfg.MarkupConfig()
|
|
cfg := mcfg.Goldmark
|
|
var rendererOptions []renderer.Option
|
|
|
|
if cfg.Renderer.HardWraps {
|
|
rendererOptions = append(rendererOptions, html.WithHardWraps())
|
|
}
|
|
|
|
if cfg.Renderer.XHTML {
|
|
rendererOptions = append(rendererOptions, html.WithXHTML())
|
|
}
|
|
|
|
if cfg.Renderer.Unsafe {
|
|
rendererOptions = append(rendererOptions, html.WithUnsafe())
|
|
}
|
|
|
|
tocRendererOptions := make([]renderer.Option, len(rendererOptions))
|
|
if rendererOptions != nil {
|
|
copy(tocRendererOptions, rendererOptions)
|
|
}
|
|
tocRendererOptions = append(tocRendererOptions,
|
|
renderer.WithNodeRenderers(util.Prioritized(extension.NewStrikethroughHTMLRenderer(), 500)),
|
|
renderer.WithNodeRenderers(util.Prioritized(emoji.NewHTMLRenderer(), 200)))
|
|
var (
|
|
extensions = []goldmark.Extender{
|
|
newLinks(cfg),
|
|
newTocExtension(tocRendererOptions),
|
|
}
|
|
parserOptions []parser.Option
|
|
)
|
|
|
|
extensions = append(extensions, images.New(cfg.Parser.WrapStandAloneImageWithinParagraph))
|
|
|
|
if mcfg.Highlight.CodeFences {
|
|
extensions = append(extensions, codeblocks.New())
|
|
}
|
|
|
|
if cfg.Extensions.Table {
|
|
extensions = append(extensions, extension.Table)
|
|
}
|
|
|
|
if cfg.Extensions.Strikethrough {
|
|
extensions = append(extensions, extension.Strikethrough)
|
|
}
|
|
|
|
if cfg.Extensions.Linkify {
|
|
extensions = append(extensions, extension.Linkify)
|
|
}
|
|
|
|
if cfg.Extensions.TaskList {
|
|
extensions = append(extensions, extension.TaskList)
|
|
}
|
|
|
|
if !cfg.Extensions.Typographer.Disable {
|
|
t := extension.NewTypographer(
|
|
extension.WithTypographicSubstitutions(toTypographicPunctuationMap(cfg.Extensions.Typographer)),
|
|
)
|
|
extensions = append(extensions, t)
|
|
}
|
|
|
|
if cfg.Extensions.DefinitionList {
|
|
extensions = append(extensions, extension.DefinitionList)
|
|
}
|
|
|
|
if cfg.Extensions.Footnote {
|
|
extensions = append(extensions, extension.Footnote)
|
|
}
|
|
|
|
if cfg.Extensions.CJK.Enable {
|
|
opts := []extension.CJKOption{}
|
|
if cfg.Extensions.CJK.EastAsianLineBreaks {
|
|
if cfg.Extensions.CJK.EastAsianLineBreaksStyle == "css3draft" {
|
|
opts = append(opts, extension.WithEastAsianLineBreaks(extension.EastAsianLineBreaksCSS3Draft))
|
|
} else {
|
|
opts = append(opts, extension.WithEastAsianLineBreaks())
|
|
}
|
|
}
|
|
|
|
if cfg.Extensions.CJK.EscapedSpace {
|
|
opts = append(opts, extension.WithEscapedSpace())
|
|
}
|
|
c := extension.NewCJK(opts...)
|
|
extensions = append(extensions, c)
|
|
}
|
|
|
|
if cfg.Extensions.Passthrough.Enable {
|
|
configuredInlines := cfg.Extensions.Passthrough.Delimiters.Inline
|
|
configuredBlocks := cfg.Extensions.Passthrough.Delimiters.Block
|
|
|
|
inlineDelimiters := make([]passthrough.Delimiters, len(configuredInlines))
|
|
blockDelimiters := make([]passthrough.Delimiters, len(configuredBlocks))
|
|
|
|
for i, d := range configuredInlines {
|
|
inlineDelimiters[i] = passthrough.Delimiters{
|
|
Open: d[0],
|
|
Close: d[1],
|
|
}
|
|
}
|
|
|
|
for i, d := range configuredBlocks {
|
|
blockDelimiters[i] = passthrough.Delimiters{
|
|
Open: d[0],
|
|
Close: d[1],
|
|
}
|
|
}
|
|
|
|
extensions = append(extensions, passthrough.New(
|
|
passthrough.Config{
|
|
InlineDelimiters: inlineDelimiters,
|
|
BlockDelimiters: blockDelimiters,
|
|
},
|
|
))
|
|
}
|
|
|
|
if pcfg.Conf.EnableEmoji() {
|
|
extensions = append(extensions, emoji.Emoji)
|
|
}
|
|
|
|
if cfg.Parser.AutoHeadingID {
|
|
parserOptions = append(parserOptions, parser.WithAutoHeadingID())
|
|
}
|
|
|
|
if cfg.Parser.Attribute.Title {
|
|
parserOptions = append(parserOptions, parser.WithAttribute())
|
|
}
|
|
|
|
if cfg.Parser.Attribute.Block {
|
|
extensions = append(extensions, attributes.New())
|
|
}
|
|
|
|
md := goldmark.New(
|
|
goldmark.WithExtensions(
|
|
extensions...,
|
|
),
|
|
goldmark.WithParserOptions(
|
|
parserOptions...,
|
|
),
|
|
goldmark.WithRendererOptions(
|
|
rendererOptions...,
|
|
),
|
|
)
|
|
|
|
return md
|
|
}
|
|
|
|
type parserResult struct {
|
|
doc any
|
|
toc *tableofcontents.Fragments
|
|
}
|
|
|
|
func (p parserResult) Doc() any {
|
|
return p.doc
|
|
}
|
|
|
|
func (p parserResult) TableOfContents() *tableofcontents.Fragments {
|
|
return p.toc
|
|
}
|
|
|
|
type renderResult struct {
|
|
converter.ResultRender
|
|
}
|
|
|
|
type converterResult struct {
|
|
converter.ResultRender
|
|
tableOfContentsProvider
|
|
}
|
|
|
|
type tableOfContentsProvider interface {
|
|
TableOfContents() *tableofcontents.Fragments
|
|
}
|
|
|
|
func (c *goldmarkConverter) Parse(ctx converter.RenderContext) (converter.ResultParse, error) {
|
|
pctx := c.newParserContext(ctx)
|
|
reader := text.NewReader(ctx.Src)
|
|
|
|
doc := c.md.Parser().Parse(
|
|
reader,
|
|
parser.WithContext(pctx),
|
|
)
|
|
|
|
return parserResult{
|
|
doc: doc,
|
|
toc: pctx.TableOfContents(),
|
|
}, nil
|
|
}
|
|
|
|
func (c *goldmarkConverter) Render(ctx converter.RenderContext, doc any) (converter.ResultRender, error) {
|
|
n := doc.(ast.Node)
|
|
buf := &render.BufWriter{Buffer: &bytes.Buffer{}}
|
|
|
|
rcx := &render.RenderContextDataHolder{
|
|
Rctx: ctx,
|
|
Dctx: c.ctx,
|
|
}
|
|
|
|
w := &render.Context{
|
|
BufWriter: buf,
|
|
ContextData: rcx,
|
|
}
|
|
|
|
if err := c.md.Renderer().Render(w, ctx.Src, n); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return renderResult{
|
|
ResultRender: buf,
|
|
}, nil
|
|
}
|
|
|
|
func (c *goldmarkConverter) Convert(ctx converter.RenderContext) (converter.ResultRender, error) {
|
|
parseResult, err := c.Parse(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
renderResult, err := c.Render(ctx, parseResult.Doc())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return converterResult{
|
|
ResultRender: renderResult,
|
|
tableOfContentsProvider: parseResult,
|
|
}, nil
|
|
}
|
|
|
|
func (c *goldmarkConverter) newParserContext(rctx converter.RenderContext) *parserContext {
|
|
ctx := parser.NewContext(parser.WithIDs(newIDFactory(c.cfg.MarkupConfig().Goldmark.Parser.AutoHeadingIDType)))
|
|
ctx.Set(tocEnableKey, rctx.RenderTOC)
|
|
return &parserContext{
|
|
Context: ctx,
|
|
}
|
|
}
|
|
|
|
type parserContext struct {
|
|
parser.Context
|
|
}
|
|
|
|
func (p *parserContext) TableOfContents() *tableofcontents.Fragments {
|
|
if v := p.Get(tocResultKey); v != nil {
|
|
return v.(*tableofcontents.Fragments)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Note: It's tempting to put this in the config package, but that doesn't work.
|
|
// TODO(bep) create upstream issue.
|
|
func toTypographicPunctuationMap(t goldmark_config.Typographer) map[extension.TypographicPunctuation][]byte {
|
|
return map[extension.TypographicPunctuation][]byte{
|
|
extension.LeftSingleQuote: []byte(t.LeftSingleQuote),
|
|
extension.RightSingleQuote: []byte(t.RightSingleQuote),
|
|
extension.LeftDoubleQuote: []byte(t.LeftDoubleQuote),
|
|
extension.RightDoubleQuote: []byte(t.RightDoubleQuote),
|
|
extension.EnDash: []byte(t.EnDash),
|
|
extension.EmDash: []byte(t.EmDash),
|
|
extension.Ellipsis: []byte(t.Ellipsis),
|
|
extension.LeftAngleQuote: []byte(t.LeftAngleQuote),
|
|
extension.RightAngleQuote: []byte(t.RightAngleQuote),
|
|
extension.Apostrophe: []byte(t.Apostrophe),
|
|
}
|
|
}
|