mirror of
https://github.com/gohugoio/hugo.git
synced 2024-11-21 20:46:30 -05:00
9cd54cab20
This avoids double parsing the page content when `enableEmoji=true`. This commit also adds some general improvements to the parser, making it in general much faster: ```bash benchmark old ns/op new ns/op delta BenchmarkShortcodeLexer-4 90258 101730 +12.71% BenchmarkParse-4 148940 15037 -89.90% benchmark old allocs new allocs delta BenchmarkShortcodeLexer-4 456 700 +53.51% BenchmarkParse-4 28 33 +17.86% benchmark old bytes new bytes delta BenchmarkShortcodeLexer-4 69875 81014 +15.94% BenchmarkParse-4 8128 8304 +2.17% ``` Running some site benchmarks with Emoji support turned on: ```bash benchmark old ns/op new ns/op delta BenchmarkSiteBuilding/TOML,num_langs=3,num_pages=5000,tags_per_page=5,shortcodes,render-4 924556797 818115620 -11.51% benchmark old allocs new allocs delta BenchmarkSiteBuilding/TOML,num_langs=3,num_pages=5000,tags_per_page=5,shortcodes,render-4 4112613 4133787 +0.51% benchmark old bytes new bytes delta BenchmarkSiteBuilding/TOML,num_langs=3,num_pages=5000,tags_per_page=5,shortcodes,render-4 426982864 424363832 -0.61% ``` Fixes #5534
341 lines
7.4 KiB
Go
341 lines
7.4 KiB
Go
// Copyright 2017-present 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 hugolib
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"path/filepath"
|
|
"sort"
|
|
|
|
"strings"
|
|
|
|
"github.com/gohugoio/hugo/helpers"
|
|
"github.com/gohugoio/hugo/resource"
|
|
)
|
|
|
|
var (
|
|
// This should be the only list of valid extensions for content files.
|
|
contentFileExtensions = []string{
|
|
"html", "htm",
|
|
"mdown", "markdown", "md",
|
|
"asciidoc", "adoc", "ad",
|
|
"rest", "rst",
|
|
"mmark",
|
|
"org",
|
|
"pandoc", "pdc"}
|
|
|
|
contentFileExtensionsSet map[string]bool
|
|
)
|
|
|
|
func init() {
|
|
contentFileExtensionsSet = make(map[string]bool)
|
|
for _, ext := range contentFileExtensions {
|
|
contentFileExtensionsSet[ext] = true
|
|
}
|
|
}
|
|
|
|
func newHandlerChain(s *Site) contentHandler {
|
|
c := &contentHandlers{s: s}
|
|
|
|
contentFlow := c.parsePage(c.processFirstMatch(
|
|
// Handles all files with a content file extension. See above.
|
|
c.handlePageContent(),
|
|
|
|
// Every HTML file without front matter will be passed on to this handler.
|
|
c.handleHTMLContent(),
|
|
))
|
|
|
|
c.rootHandler = c.processFirstMatch(
|
|
contentFlow,
|
|
|
|
// Creates a file resource (image, CSS etc.) if there is a parent
|
|
// page set on the current context.
|
|
c.createResource(),
|
|
|
|
// Everything that isn't handled above, will just be copied
|
|
// to destination.
|
|
c.copyFile(),
|
|
)
|
|
|
|
return c.rootHandler
|
|
|
|
}
|
|
|
|
type contentHandlers struct {
|
|
s *Site
|
|
rootHandler contentHandler
|
|
}
|
|
|
|
func (c *contentHandlers) processFirstMatch(handlers ...contentHandler) func(ctx *handlerContext) handlerResult {
|
|
return func(ctx *handlerContext) handlerResult {
|
|
for _, h := range handlers {
|
|
res := h(ctx)
|
|
if res.handled || res.err != nil {
|
|
return res
|
|
}
|
|
}
|
|
return handlerResult{err: errors.New("no matching handler found")}
|
|
}
|
|
}
|
|
|
|
type handlerContext struct {
|
|
// These are the pages stored in Site.
|
|
pages chan<- *Page
|
|
|
|
doNotAddToSiteCollections bool
|
|
|
|
currentPage *Page
|
|
parentPage *Page
|
|
|
|
bundle *bundleDir
|
|
|
|
source *fileInfo
|
|
|
|
// Relative path to the target.
|
|
target string
|
|
}
|
|
|
|
func (c *handlerContext) ext() string {
|
|
if c.currentPage != nil {
|
|
if c.currentPage.Markup != "" {
|
|
return c.currentPage.Markup
|
|
}
|
|
return c.currentPage.Ext()
|
|
}
|
|
|
|
if c.bundle != nil {
|
|
return c.bundle.fi.Ext()
|
|
} else {
|
|
return c.source.Ext()
|
|
}
|
|
}
|
|
|
|
func (c *handlerContext) targetPath() string {
|
|
if c.target != "" {
|
|
return c.target
|
|
}
|
|
|
|
return c.source.Filename()
|
|
}
|
|
|
|
func (c *handlerContext) file() *fileInfo {
|
|
if c.bundle != nil {
|
|
return c.bundle.fi
|
|
}
|
|
|
|
return c.source
|
|
}
|
|
|
|
// Create a copy with the current context as its parent.
|
|
func (c handlerContext) childCtx(fi *fileInfo) *handlerContext {
|
|
if c.currentPage == nil {
|
|
panic("Need a Page to create a child context")
|
|
}
|
|
|
|
c.target = strings.TrimPrefix(fi.Path(), c.bundle.fi.Dir())
|
|
c.source = fi
|
|
|
|
c.doNotAddToSiteCollections = c.bundle != nil && c.bundle.tp != bundleBranch
|
|
|
|
c.bundle = nil
|
|
|
|
c.parentPage = c.currentPage
|
|
c.currentPage = nil
|
|
|
|
return &c
|
|
}
|
|
|
|
func (c *handlerContext) supports(exts ...string) bool {
|
|
ext := c.ext()
|
|
for _, s := range exts {
|
|
if s == ext {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func (c *handlerContext) isContentFile() bool {
|
|
return contentFileExtensionsSet[c.ext()]
|
|
}
|
|
|
|
type (
|
|
handlerResult struct {
|
|
err error
|
|
handled bool
|
|
resource resource.Resource
|
|
}
|
|
|
|
contentHandler func(ctx *handlerContext) handlerResult
|
|
)
|
|
|
|
var (
|
|
notHandled handlerResult
|
|
)
|
|
|
|
func (c *contentHandlers) parsePage(h contentHandler) contentHandler {
|
|
return func(ctx *handlerContext) handlerResult {
|
|
if !ctx.isContentFile() {
|
|
return notHandled
|
|
}
|
|
|
|
result := handlerResult{handled: true}
|
|
fi := ctx.file()
|
|
|
|
f, err := fi.Open()
|
|
if err != nil {
|
|
return handlerResult{err: fmt.Errorf("(%s) failed to open content file: %s", fi.Filename(), err)}
|
|
}
|
|
defer f.Close()
|
|
|
|
p := c.s.newPageFromFile(fi)
|
|
|
|
_, err = p.ReadFrom(f)
|
|
if err != nil {
|
|
return handlerResult{err: err}
|
|
}
|
|
|
|
if !p.shouldBuild() {
|
|
if !ctx.doNotAddToSiteCollections {
|
|
ctx.pages <- p
|
|
}
|
|
return result
|
|
}
|
|
|
|
ctx.currentPage = p
|
|
|
|
if ctx.bundle != nil {
|
|
// Add the bundled files
|
|
for _, fi := range ctx.bundle.resources {
|
|
childCtx := ctx.childCtx(fi)
|
|
res := c.rootHandler(childCtx)
|
|
if res.err != nil {
|
|
return res
|
|
}
|
|
if res.resource != nil {
|
|
if pageResource, ok := res.resource.(*Page); ok {
|
|
pageResource.resourcePath = filepath.ToSlash(childCtx.target)
|
|
pageResource.parent = p
|
|
}
|
|
p.Resources = append(p.Resources, res.resource)
|
|
}
|
|
}
|
|
|
|
sort.SliceStable(p.Resources, func(i, j int) bool {
|
|
if p.Resources[i].ResourceType() < p.Resources[j].ResourceType() {
|
|
return true
|
|
}
|
|
|
|
p1, ok1 := p.Resources[i].(*Page)
|
|
p2, ok2 := p.Resources[j].(*Page)
|
|
|
|
if ok1 != ok2 {
|
|
return ok2
|
|
}
|
|
|
|
if ok1 {
|
|
return defaultPageSort(p1, p2)
|
|
}
|
|
|
|
return p.Resources[i].RelPermalink() < p.Resources[j].RelPermalink()
|
|
})
|
|
|
|
// Assign metadata from front matter if set
|
|
if len(p.resourcesMetadata) > 0 {
|
|
resource.AssignMetadata(p.resourcesMetadata, p.Resources...)
|
|
}
|
|
|
|
}
|
|
|
|
return h(ctx)
|
|
}
|
|
}
|
|
|
|
func (c *contentHandlers) handlePageContent() contentHandler {
|
|
return func(ctx *handlerContext) handlerResult {
|
|
if ctx.supports("html", "htm") {
|
|
return notHandled
|
|
}
|
|
|
|
p := ctx.currentPage
|
|
|
|
p.workContent = p.renderContent(p.workContent)
|
|
|
|
tmpContent, tmpTableOfContents := helpers.ExtractTOC(p.workContent)
|
|
p.TableOfContents = helpers.BytesToHTML(tmpTableOfContents)
|
|
p.workContent = tmpContent
|
|
|
|
if !ctx.doNotAddToSiteCollections {
|
|
ctx.pages <- p
|
|
}
|
|
|
|
return handlerResult{handled: true, resource: p}
|
|
}
|
|
}
|
|
|
|
func (c *contentHandlers) handleHTMLContent() contentHandler {
|
|
return func(ctx *handlerContext) handlerResult {
|
|
if !ctx.supports("html", "htm") {
|
|
return notHandled
|
|
}
|
|
|
|
p := ctx.currentPage
|
|
|
|
if !ctx.doNotAddToSiteCollections {
|
|
ctx.pages <- p
|
|
}
|
|
|
|
return handlerResult{handled: true, resource: p}
|
|
}
|
|
}
|
|
|
|
func (c *contentHandlers) createResource() contentHandler {
|
|
return func(ctx *handlerContext) handlerResult {
|
|
if ctx.parentPage == nil {
|
|
return notHandled
|
|
}
|
|
|
|
resource, err := c.s.ResourceSpec.New(
|
|
resource.ResourceSourceDescriptor{
|
|
TargetPathBuilder: ctx.parentPage.subResourceTargetPathFactory,
|
|
SourceFile: ctx.source,
|
|
RelTargetFilename: ctx.target,
|
|
URLBase: c.s.GetURLLanguageBasePath(),
|
|
TargetBasePaths: []string{c.s.GetTargetLanguageBasePath()},
|
|
})
|
|
|
|
return handlerResult{err: err, handled: true, resource: resource}
|
|
}
|
|
}
|
|
|
|
func (c *contentHandlers) copyFile() contentHandler {
|
|
return func(ctx *handlerContext) handlerResult {
|
|
f, err := c.s.BaseFs.Content.Fs.Open(ctx.source.Filename())
|
|
if err != nil {
|
|
err := fmt.Errorf("failed to open file in copyFile: %s", err)
|
|
return handlerResult{err: err}
|
|
}
|
|
|
|
target := ctx.targetPath()
|
|
|
|
defer f.Close()
|
|
if err := c.s.publish(&c.s.PathSpec.ProcessingStats.Files, target, f); err != nil {
|
|
return handlerResult{err: err}
|
|
}
|
|
|
|
return handlerResult{handled: true}
|
|
}
|
|
}
|