github.com/graemephi/kahugo@v0.62.3-0.20211121071557-d78c0423784d/hugolib/page__per_output.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 hugolib 15 16 import ( 17 "bytes" 18 "context" 19 "fmt" 20 "html/template" 21 "runtime/debug" 22 "strings" 23 "sync" 24 "unicode/utf8" 25 26 "github.com/gohugoio/hugo/identity" 27 28 "github.com/gohugoio/hugo/markup/converter/hooks" 29 30 "github.com/gohugoio/hugo/markup/converter" 31 32 "github.com/gohugoio/hugo/lazy" 33 34 bp "github.com/gohugoio/hugo/bufferpool" 35 "github.com/gohugoio/hugo/tpl" 36 37 "github.com/gohugoio/hugo/helpers" 38 "github.com/gohugoio/hugo/output" 39 "github.com/gohugoio/hugo/resources/page" 40 "github.com/gohugoio/hugo/resources/resource" 41 ) 42 43 var ( 44 nopTargetPath = targetPathsHolder{} 45 nopPagePerOutput = struct { 46 resource.ResourceLinksProvider 47 page.ContentProvider 48 page.PageRenderProvider 49 page.PaginatorProvider 50 page.TableOfContentsProvider 51 page.AlternativeOutputFormatsProvider 52 53 targetPather 54 }{ 55 page.NopPage, 56 page.NopPage, 57 page.NopPage, 58 page.NopPage, 59 page.NopPage, 60 page.NopPage, 61 nopTargetPath, 62 } 63 ) 64 65 var pageContentOutputDependenciesID = identity.KeyValueIdentity{Key: "pageOutput", Value: "dependencies"} 66 67 func newPageContentOutput(p *pageState, po *pageOutput) (*pageContentOutput, error) { 68 parent := p.init 69 70 var dependencyTracker identity.Manager 71 if p.s.running() { 72 dependencyTracker = identity.NewManager(pageContentOutputDependenciesID) 73 } 74 75 cp := &pageContentOutput{ 76 dependencyTracker: dependencyTracker, 77 p: p, 78 f: po.f, 79 renderHooks: &renderHooks{}, 80 } 81 82 initContent := func() (err error) { 83 p.s.h.IncrContentRender() 84 85 if p.cmap == nil { 86 // Nothing to do. 87 return nil 88 } 89 defer func() { 90 // See https://github.com/gohugoio/hugo/issues/6210 91 if r := recover(); r != nil { 92 err = fmt.Errorf("%s", r) 93 p.s.Log.Errorf("[BUG] Got panic:\n%s\n%s", r, string(debug.Stack())) 94 } 95 }() 96 97 if err := po.initRenderHooks(); err != nil { 98 return err 99 } 100 101 var hasShortcodeVariants bool 102 103 f := po.f 104 cp.contentPlaceholders, hasShortcodeVariants, err = p.shortcodeState.renderShortcodesForPage(p, f) 105 if err != nil { 106 return err 107 } 108 109 enableReuse := !(hasShortcodeVariants || cp.renderHooksHaveVariants) 110 111 if enableReuse { 112 // Reuse this for the other output formats. 113 // We may improve on this, but we really want to avoid re-rendering the content 114 // to all output formats. 115 // The current rule is that if you need output format-aware shortcodes or 116 // content rendering hooks, create a output format-specific template, e.g. 117 // myshortcode.amp.html. 118 cp.enableReuse() 119 } 120 121 cp.workContent = p.contentToRender(cp.contentPlaceholders) 122 123 isHTML := cp.p.m.markup == "html" 124 125 if !isHTML { 126 r, err := cp.renderContent(cp.workContent, true) 127 if err != nil { 128 return err 129 } 130 131 cp.workContent = r.Bytes() 132 133 if tocProvider, ok := r.(converter.TableOfContentsProvider); ok { 134 cfg := p.s.ContentSpec.Converters.GetMarkupConfig() 135 cp.tableOfContents = template.HTML( 136 tocProvider.TableOfContents().ToHTML( 137 cfg.TableOfContents.StartLevel, 138 cfg.TableOfContents.EndLevel, 139 cfg.TableOfContents.Ordered, 140 ), 141 ) 142 } else { 143 tmpContent, tmpTableOfContents := helpers.ExtractTOC(cp.workContent) 144 cp.tableOfContents = helpers.BytesToHTML(tmpTableOfContents) 145 cp.workContent = tmpContent 146 } 147 } 148 149 if cp.placeholdersEnabled { 150 // ToC was accessed via .Page.TableOfContents in the shortcode, 151 // at a time when the ToC wasn't ready. 152 cp.contentPlaceholders[tocShortcodePlaceholder] = string(cp.tableOfContents) 153 } 154 155 if p.cmap.hasNonMarkdownShortcode || cp.placeholdersEnabled { 156 // There are one or more replacement tokens to be replaced. 157 cp.workContent, err = replaceShortcodeTokens(cp.workContent, cp.contentPlaceholders) 158 if err != nil { 159 return err 160 } 161 } 162 163 if cp.p.source.hasSummaryDivider { 164 if isHTML { 165 src := p.source.parsed.Input() 166 167 // Use the summary sections as they are provided by the user. 168 if p.source.posSummaryEnd != -1 { 169 cp.summary = helpers.BytesToHTML(src[p.source.posMainContent:p.source.posSummaryEnd]) 170 } 171 172 if cp.p.source.posBodyStart != -1 { 173 cp.workContent = src[cp.p.source.posBodyStart:] 174 } 175 176 } else { 177 summary, content, err := splitUserDefinedSummaryAndContent(cp.p.m.markup, cp.workContent) 178 if err != nil { 179 cp.p.s.Log.Errorf("Failed to set user defined summary for page %q: %s", cp.p.pathOrTitle(), err) 180 } else { 181 cp.workContent = content 182 cp.summary = helpers.BytesToHTML(summary) 183 } 184 } 185 } else if cp.p.m.summary != "" { 186 b, err := cp.renderContent([]byte(cp.p.m.summary), false) 187 if err != nil { 188 return err 189 } 190 html := cp.p.s.ContentSpec.TrimShortHTML(b.Bytes()) 191 cp.summary = helpers.BytesToHTML(html) 192 } 193 194 cp.content = helpers.BytesToHTML(cp.workContent) 195 196 return nil 197 } 198 199 // Recursive loops can only happen in content files with template code (shortcodes etc.) 200 // Avoid creating new goroutines if we don't have to. 201 needTimeout := p.shortcodeState.hasShortcodes() || cp.renderHooks != nil 202 203 if needTimeout { 204 cp.initMain = parent.BranchWithTimeout(p.s.siteCfg.timeout, func(ctx context.Context) (interface{}, error) { 205 return nil, initContent() 206 }) 207 } else { 208 cp.initMain = parent.Branch(func() (interface{}, error) { 209 return nil, initContent() 210 }) 211 } 212 213 cp.initPlain = cp.initMain.Branch(func() (interface{}, error) { 214 cp.plain = helpers.StripHTML(string(cp.content)) 215 cp.plainWords = strings.Fields(cp.plain) 216 cp.setWordCounts(p.m.isCJKLanguage) 217 218 if err := cp.setAutoSummary(); err != nil { 219 return err, nil 220 } 221 222 return nil, nil 223 }) 224 225 return cp, nil 226 } 227 228 type renderHooks struct { 229 hooks hooks.Renderers 230 init sync.Once 231 } 232 233 // pageContentOutput represents the Page content for a given output format. 234 type pageContentOutput struct { 235 f output.Format 236 237 // If we can reuse this for other output formats. 238 reuse bool 239 reuseInit sync.Once 240 241 p *pageState 242 243 // Lazy load dependencies 244 initMain *lazy.Init 245 initPlain *lazy.Init 246 247 placeholdersEnabled bool 248 placeholdersEnabledInit sync.Once 249 250 renderHooks *renderHooks 251 252 // Set if there are more than one output format variant 253 renderHooksHaveVariants bool // TODO(bep) reimplement this in another way, consolidate with shortcodes 254 255 // Content state 256 257 workContent []byte 258 dependencyTracker identity.Manager // Set in server mode. 259 260 // Temporary storage of placeholders mapped to their content. 261 // These are shortcodes etc. Some of these will need to be replaced 262 // after any markup is rendered, so they share a common prefix. 263 contentPlaceholders map[string]string 264 265 // Content sections 266 content template.HTML 267 summary template.HTML 268 tableOfContents template.HTML 269 270 truncated bool 271 272 plainWords []string 273 plain string 274 fuzzyWordCount int 275 wordCount int 276 readingTime int 277 } 278 279 func (p *pageContentOutput) trackDependency(id identity.Provider) { 280 if p.dependencyTracker != nil { 281 p.dependencyTracker.Add(id) 282 } 283 } 284 285 func (p *pageContentOutput) Reset() { 286 if p.dependencyTracker != nil { 287 p.dependencyTracker.Reset() 288 } 289 p.initMain.Reset() 290 p.initPlain.Reset() 291 p.renderHooks = &renderHooks{} 292 } 293 294 func (p *pageContentOutput) Content() (interface{}, error) { 295 if p.p.s.initInit(p.initMain, p.p) { 296 return p.content, nil 297 } 298 return nil, nil 299 } 300 301 func (p *pageContentOutput) FuzzyWordCount() int { 302 p.p.s.initInit(p.initPlain, p.p) 303 return p.fuzzyWordCount 304 } 305 306 func (p *pageContentOutput) Len() int { 307 p.p.s.initInit(p.initMain, p.p) 308 return len(p.content) 309 } 310 311 func (p *pageContentOutput) Plain() string { 312 p.p.s.initInit(p.initPlain, p.p) 313 return p.plain 314 } 315 316 func (p *pageContentOutput) PlainWords() []string { 317 p.p.s.initInit(p.initPlain, p.p) 318 return p.plainWords 319 } 320 321 func (p *pageContentOutput) ReadingTime() int { 322 p.p.s.initInit(p.initPlain, p.p) 323 return p.readingTime 324 } 325 326 func (p *pageContentOutput) Summary() template.HTML { 327 p.p.s.initInit(p.initMain, p.p) 328 if !p.p.source.hasSummaryDivider { 329 p.p.s.initInit(p.initPlain, p.p) 330 } 331 return p.summary 332 } 333 334 func (p *pageContentOutput) TableOfContents() template.HTML { 335 p.p.s.initInit(p.initMain, p.p) 336 return p.tableOfContents 337 } 338 339 func (p *pageContentOutput) Truncated() bool { 340 if p.p.truncated { 341 return true 342 } 343 p.p.s.initInit(p.initPlain, p.p) 344 return p.truncated 345 } 346 347 func (p *pageContentOutput) WordCount() int { 348 p.p.s.initInit(p.initPlain, p.p) 349 return p.wordCount 350 } 351 352 func (p *pageContentOutput) setAutoSummary() error { 353 if p.p.source.hasSummaryDivider || p.p.m.summary != "" { 354 return nil 355 } 356 357 var summary string 358 var truncated bool 359 360 if p.p.m.isCJKLanguage { 361 summary, truncated = p.p.s.ContentSpec.TruncateWordsByRune(p.plainWords) 362 } else { 363 summary, truncated = p.p.s.ContentSpec.TruncateWordsToWholeSentence(p.plain) 364 } 365 p.summary = template.HTML(summary) 366 367 p.truncated = truncated 368 369 return nil 370 } 371 372 func (cp *pageContentOutput) renderContent(content []byte, renderTOC bool) (converter.Result, error) { 373 c := cp.p.getContentConverter() 374 return cp.renderContentWithConverter(c, content, renderTOC) 375 } 376 377 func (cp *pageContentOutput) renderContentWithConverter(c converter.Converter, content []byte, renderTOC bool) (converter.Result, error) { 378 r, err := c.Convert( 379 converter.RenderContext{ 380 Src: content, 381 RenderTOC: renderTOC, 382 RenderHooks: cp.renderHooks.hooks, 383 }) 384 385 if err == nil { 386 if ids, ok := r.(identity.IdentitiesProvider); ok { 387 for _, v := range ids.GetIdentities() { 388 cp.trackDependency(v) 389 } 390 } 391 } 392 393 return r, err 394 } 395 396 func (p *pageContentOutput) setWordCounts(isCJKLanguage bool) { 397 if isCJKLanguage { 398 p.wordCount = 0 399 for _, word := range p.plainWords { 400 runeCount := utf8.RuneCountInString(word) 401 if len(word) == runeCount { 402 p.wordCount++ 403 } else { 404 p.wordCount += runeCount 405 } 406 } 407 } else { 408 p.wordCount = helpers.TotalWords(p.plain) 409 } 410 411 // TODO(bep) is set in a test. Fix that. 412 if p.fuzzyWordCount == 0 { 413 p.fuzzyWordCount = (p.wordCount + 100) / 100 * 100 414 } 415 416 if isCJKLanguage { 417 p.readingTime = (p.wordCount + 500) / 501 418 } else { 419 p.readingTime = (p.wordCount + 212) / 213 420 } 421 } 422 423 // A callback to signal that we have inserted a placeholder into the rendered 424 // content. This avoids doing extra replacement work. 425 func (p *pageContentOutput) enablePlaceholders() { 426 p.placeholdersEnabledInit.Do(func() { 427 p.placeholdersEnabled = true 428 }) 429 } 430 431 func (p *pageContentOutput) enableReuse() { 432 p.reuseInit.Do(func() { 433 p.reuse = true 434 }) 435 } 436 437 // these will be shifted out when rendering a given output format. 438 type pagePerOutputProviders interface { 439 targetPather 440 page.PaginatorProvider 441 resource.ResourceLinksProvider 442 } 443 444 type targetPather interface { 445 targetPaths() page.TargetPaths 446 } 447 448 type targetPathsHolder struct { 449 paths page.TargetPaths 450 page.OutputFormat 451 } 452 453 func (t targetPathsHolder) targetPaths() page.TargetPaths { 454 return t.paths 455 } 456 457 func executeToString(h tpl.TemplateHandler, templ tpl.Template, data interface{}) (string, error) { 458 b := bp.GetBuffer() 459 defer bp.PutBuffer(b) 460 if err := h.Execute(templ, b, data); err != nil { 461 return "", err 462 } 463 return b.String(), nil 464 } 465 466 func splitUserDefinedSummaryAndContent(markup string, c []byte) (summary []byte, content []byte, err error) { 467 defer func() { 468 if r := recover(); r != nil { 469 err = fmt.Errorf("summary split failed: %s", r) 470 } 471 }() 472 473 startDivider := bytes.Index(c, internalSummaryDividerBaseBytes) 474 475 if startDivider == -1 { 476 return 477 } 478 479 startTag := "p" 480 switch markup { 481 case "asciidocext": 482 startTag = "div" 483 } 484 485 // Walk back and forward to the surrounding tags. 486 start := bytes.LastIndex(c[:startDivider], []byte("<"+startTag)) 487 end := bytes.Index(c[startDivider:], []byte("</"+startTag)) 488 489 if start == -1 { 490 start = startDivider 491 } else { 492 start = startDivider - (startDivider - start) 493 } 494 495 if end == -1 { 496 end = startDivider + len(internalSummaryDividerBase) 497 } else { 498 end = startDivider + end + len(startTag) + 3 499 } 500 501 var addDiv bool 502 503 switch markup { 504 case "rst": 505 addDiv = true 506 } 507 508 withoutDivider := append(c[:start], bytes.Trim(c[end:], "\n")...) 509 510 if len(withoutDivider) > 0 { 511 summary = bytes.TrimSpace(withoutDivider[:start]) 512 } 513 514 if addDiv { 515 // For the rst 516 summary = append(append([]byte(nil), summary...), []byte("</div>")...) 517 } 518 519 if err != nil { 520 return 521 } 522 523 content = bytes.TrimSpace(withoutDivider) 524 525 return 526 }