hugo

Fork of github.com/gohugoio/hugo with reverse pagination support

git clone git://git.shimmy1996.com/hugo.git

convert.go (8868B)

    1 // Copyright 2020 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 asciidocext converts AsciiDoc to HTML using Asciidoctor
   15 // external binary. The `asciidoc` module is reserved for a future golang
   16 // implementation.
   17 package asciidocext
   18 
   19 import (
   20 	"bytes"
   21 	"path/filepath"
   22 	"strings"
   23 
   24 	"github.com/gohugoio/hugo/common/hexec"
   25 	"github.com/gohugoio/hugo/htesting"
   26 
   27 	"github.com/gohugoio/hugo/identity"
   28 	"github.com/gohugoio/hugo/markup/asciidocext/asciidocext_config"
   29 	"github.com/gohugoio/hugo/markup/converter"
   30 	"github.com/gohugoio/hugo/markup/internal"
   31 	"github.com/gohugoio/hugo/markup/tableofcontents"
   32 	"golang.org/x/net/html"
   33 )
   34 
   35 /* ToDo: RelPermalink patch for svg posts not working*/
   36 type pageSubset interface {
   37 	RelPermalink() string
   38 }
   39 
   40 // Provider is the package entry point.
   41 var Provider converter.ProviderProvider = provider{}
   42 
   43 type provider struct{}
   44 
   45 func (p provider) New(cfg converter.ProviderConfig) (converter.Provider, error) {
   46 	return converter.NewProvider("asciidocext", func(ctx converter.DocumentContext) (converter.Converter, error) {
   47 		return &asciidocConverter{
   48 			ctx: ctx,
   49 			cfg: cfg,
   50 		}, nil
   51 	}), nil
   52 }
   53 
   54 type asciidocResult struct {
   55 	converter.Result
   56 	toc tableofcontents.Root
   57 }
   58 
   59 func (r asciidocResult) TableOfContents() tableofcontents.Root {
   60 	return r.toc
   61 }
   62 
   63 type asciidocConverter struct {
   64 	ctx converter.DocumentContext
   65 	cfg converter.ProviderConfig
   66 }
   67 
   68 func (a *asciidocConverter) Convert(ctx converter.RenderContext) (converter.Result, error) {
   69 	b, err := a.getAsciidocContent(ctx.Src, a.ctx)
   70 	if err != nil {
   71 		return nil, err
   72 	}
   73 	content, toc, err := a.extractTOC(b)
   74 	if err != nil {
   75 		return nil, err
   76 	}
   77 	return asciidocResult{
   78 		Result: converter.Bytes(content),
   79 		toc:    toc,
   80 	}, nil
   81 }
   82 
   83 func (a *asciidocConverter) Supports(_ identity.Identity) bool {
   84 	return false
   85 }
   86 
   87 // getAsciidocContent calls asciidoctor as an external helper
   88 // to convert AsciiDoc content to HTML.
   89 func (a *asciidocConverter) getAsciidocContent(src []byte, ctx converter.DocumentContext) ([]byte, error) {
   90 	if !hasAsciiDoc() {
   91 		a.cfg.Logger.Errorln("asciidoctor not found in $PATH: Please install.\n",
   92 			"                 Leaving AsciiDoc content unrendered.")
   93 		return src, nil
   94 	}
   95 
   96 	args := a.parseArgs(ctx)
   97 	args = append(args, "-")
   98 
   99 	a.cfg.Logger.Infoln("Rendering", ctx.DocumentName, " using asciidoctor args", args, "...")
  100 
  101 	return internal.ExternallyRenderContent(a.cfg, ctx, src, asciiDocBinaryName, args)
  102 }
  103 
  104 func (a *asciidocConverter) parseArgs(ctx converter.DocumentContext) []string {
  105 	cfg := a.cfg.MarkupConfig.AsciidocExt
  106 	args := []string{}
  107 
  108 	args = a.appendArg(args, "-b", cfg.Backend, asciidocext_config.CliDefault.Backend, asciidocext_config.AllowedBackend)
  109 
  110 	for _, extension := range cfg.Extensions {
  111 		if strings.LastIndexAny(extension, `\/.`) > -1 {
  112 			a.cfg.Logger.Errorln("Unsupported asciidoctor extension was passed in. Extension `" + extension + "` ignored. Only installed asciidoctor extensions are allowed.")
  113 			continue
  114 		}
  115 		args = append(args, "-r", extension)
  116 	}
  117 
  118 	for attributeKey, attributeValue := range cfg.Attributes {
  119 		if asciidocext_config.DisallowedAttributes[attributeKey] {
  120 			a.cfg.Logger.Errorln("Unsupported asciidoctor attribute was passed in. Attribute `" + attributeKey + "` ignored.")
  121 			continue
  122 		}
  123 
  124 		args = append(args, "-a", attributeKey+"="+attributeValue)
  125 	}
  126 
  127 	if cfg.WorkingFolderCurrent {
  128 		contentDir := filepath.Dir(ctx.Filename)
  129 		sourceDir := a.cfg.Cfg.GetString("source")
  130 		destinationDir := a.cfg.Cfg.GetString("destination")
  131 
  132 		if destinationDir == "" {
  133 			a.cfg.Logger.Errorln("markup.asciidocext.workingFolderCurrent requires hugo command option --destination to be set")
  134 		}
  135 		if !filepath.IsAbs(destinationDir) && sourceDir != "" {
  136 			destinationDir = filepath.Join(sourceDir, destinationDir)
  137 		}
  138 
  139 		var outDir string
  140 		var err error
  141 
  142 		file := filepath.Base(ctx.Filename)
  143 		if a.cfg.Cfg.GetBool("uglyUrls") || file == "_index.adoc" || file == "index.adoc" {
  144 			outDir, err = filepath.Abs(filepath.Dir(filepath.Join(destinationDir, ctx.DocumentName)))
  145 		} else {
  146 			postDir := ""
  147 			page, ok := ctx.Document.(pageSubset)
  148 			if ok {
  149 				postDir = filepath.Base(page.RelPermalink())
  150 			} else {
  151 				a.cfg.Logger.Errorln("unable to cast interface to pageSubset")
  152 			}
  153 
  154 			outDir, err = filepath.Abs(filepath.Join(destinationDir, filepath.Dir(ctx.DocumentName), postDir))
  155 		}
  156 
  157 		if err != nil {
  158 			a.cfg.Logger.Errorln("asciidoctor outDir: ", err)
  159 		}
  160 
  161 		args = append(args, "--base-dir", contentDir, "-a", "outdir="+outDir)
  162 	}
  163 
  164 	if cfg.NoHeaderOrFooter {
  165 		args = append(args, "--no-header-footer")
  166 	} else {
  167 		a.cfg.Logger.Warnln("asciidoctor parameter NoHeaderOrFooter is expected for correct html rendering")
  168 	}
  169 
  170 	if cfg.SectionNumbers {
  171 		args = append(args, "--section-numbers")
  172 	}
  173 
  174 	if cfg.Verbose {
  175 		args = append(args, "--verbose")
  176 	}
  177 
  178 	if cfg.Trace {
  179 		args = append(args, "--trace")
  180 	}
  181 
  182 	args = a.appendArg(args, "--failure-level", cfg.FailureLevel, asciidocext_config.CliDefault.FailureLevel, asciidocext_config.AllowedFailureLevel)
  183 
  184 	args = a.appendArg(args, "--safe-mode", cfg.SafeMode, asciidocext_config.CliDefault.SafeMode, asciidocext_config.AllowedSafeMode)
  185 
  186 	return args
  187 }
  188 
  189 func (a *asciidocConverter) appendArg(args []string, option, value, defaultValue string, allowedValues map[string]bool) []string {
  190 	if value != defaultValue {
  191 		if allowedValues[value] {
  192 			args = append(args, option, value)
  193 		} else {
  194 			a.cfg.Logger.Errorln("Unsupported asciidoctor value `" + value + "` for option " + option + " was passed in and will be ignored.")
  195 		}
  196 	}
  197 	return args
  198 }
  199 
  200 const asciiDocBinaryName = "asciidoctor"
  201 
  202 func hasAsciiDoc() bool {
  203 	return hexec.InPath(asciiDocBinaryName)
  204 }
  205 
  206 // extractTOC extracts the toc from the given src html.
  207 // It returns the html without the TOC, and the TOC data
  208 func (a *asciidocConverter) extractTOC(src []byte) ([]byte, tableofcontents.Root, error) {
  209 	var buf bytes.Buffer
  210 	buf.Write(src)
  211 	node, err := html.Parse(&buf)
  212 	if err != nil {
  213 		return nil, tableofcontents.Root{}, err
  214 	}
  215 	var (
  216 		f       func(*html.Node) bool
  217 		toc     tableofcontents.Root
  218 		toVisit []*html.Node
  219 	)
  220 	f = func(n *html.Node) bool {
  221 		if n.Type == html.ElementNode && n.Data == "div" && attr(n, "id") == "toc" {
  222 			toc = parseTOC(n)
  223 			if !a.cfg.MarkupConfig.AsciidocExt.PreserveTOC {
  224 				n.Parent.RemoveChild(n)
  225 			}
  226 			return true
  227 		}
  228 		if n.FirstChild != nil {
  229 			toVisit = append(toVisit, n.FirstChild)
  230 		}
  231 		if n.NextSibling != nil && f(n.NextSibling) {
  232 			return true
  233 		}
  234 		for len(toVisit) > 0 {
  235 			nv := toVisit[0]
  236 			toVisit = toVisit[1:]
  237 			if f(nv) {
  238 				return true
  239 			}
  240 		}
  241 		return false
  242 	}
  243 	f(node)
  244 	if err != nil {
  245 		return nil, tableofcontents.Root{}, err
  246 	}
  247 	buf.Reset()
  248 	err = html.Render(&buf, node)
  249 	if err != nil {
  250 		return nil, tableofcontents.Root{}, err
  251 	}
  252 	// ltrim <html><head></head><body> and rtrim </body></html> which are added by html.Render
  253 	res := buf.Bytes()[25:]
  254 	res = res[:len(res)-14]
  255 	return res, toc, nil
  256 }
  257 
  258 // parseTOC returns a TOC root from the given toc Node
  259 func parseTOC(doc *html.Node) tableofcontents.Root {
  260 	var (
  261 		toc tableofcontents.Root
  262 		f   func(*html.Node, int, int)
  263 	)
  264 	f = func(n *html.Node, row, level int) {
  265 		if n.Type == html.ElementNode {
  266 			switch n.Data {
  267 			case "ul":
  268 				if level == 0 {
  269 					row++
  270 				}
  271 				level++
  272 				f(n.FirstChild, row, level)
  273 			case "li":
  274 				for c := n.FirstChild; c != nil; c = c.NextSibling {
  275 					if c.Type != html.ElementNode || c.Data != "a" {
  276 						continue
  277 					}
  278 					href := attr(c, "href")[1:]
  279 					toc.AddAt(tableofcontents.Heading{
  280 						Text: nodeContent(c),
  281 						ID:   href,
  282 					}, row, level)
  283 				}
  284 				f(n.FirstChild, row, level)
  285 			}
  286 		}
  287 		if n.NextSibling != nil {
  288 			f(n.NextSibling, row, level)
  289 		}
  290 	}
  291 	f(doc.FirstChild, -1, 0)
  292 	return toc
  293 }
  294 
  295 func attr(node *html.Node, key string) string {
  296 	for _, a := range node.Attr {
  297 		if a.Key == key {
  298 			return a.Val
  299 		}
  300 	}
  301 	return ""
  302 }
  303 
  304 func nodeContent(node *html.Node) string {
  305 	var buf bytes.Buffer
  306 	for c := node.FirstChild; c != nil; c = c.NextSibling {
  307 		html.Render(&buf, c)
  308 	}
  309 	return buf.String()
  310 }
  311 
  312 // Supports returns whether Asciidoctor is installed on this computer.
  313 func Supports() bool {
  314 	hasBin := hasAsciiDoc()
  315 	if htesting.SupportsAll() {
  316 		if !hasBin {
  317 			panic("asciidoctor not installed")
  318 		}
  319 		return true
  320 	}
  321 	return hasBin
  322 }