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 }