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 }