diff options
author | Bjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com> | 2023-01-04 18:24:36 +0100 |
---|---|---|
committer | Bjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com> | 2023-05-16 18:01:29 +0200 |
commit | 241b21b0fd34d91fccb2ce69874110dceae6f926 (patch) | |
tree | d4e0118eac7e9c42f065815447a70805f8d6ad3e /markup/asciidocext/internal | |
parent | 6aededf6b42011c3039f5f66487a89a8dd65e0e7 (diff) |
Create a struct with all of Hugo's config options
Primary motivation is documentation, but it will also hopefully simplify the code.
Also,
* Lower case the default output format names; this is in line with the custom ones (map keys) and how
it's treated all the places. This avoids doing `stringds.EqualFold` everywhere.
Closes #10896
Closes #10620
Diffstat (limited to 'markup/asciidocext/internal')
-rw-r--r-- | markup/asciidocext/internal/converter.go | 274 |
1 files changed, 274 insertions, 0 deletions
diff --git a/markup/asciidocext/internal/converter.go b/markup/asciidocext/internal/converter.go new file mode 100644 index 000000000..5108bdd0a --- /dev/null +++ b/markup/asciidocext/internal/converter.go @@ -0,0 +1,274 @@ +package internal + +import ( + "bytes" + "path/filepath" + "strings" + + "github.com/gohugoio/hugo/common/hexec" + "github.com/gohugoio/hugo/identity" + "github.com/gohugoio/hugo/markup/asciidocext/asciidocext_config" + "github.com/gohugoio/hugo/markup/converter" + "github.com/gohugoio/hugo/markup/internal" + "github.com/gohugoio/hugo/markup/tableofcontents" + "golang.org/x/net/html" +) + +type AsciidocConverter struct { + Ctx converter.DocumentContext + Cfg converter.ProviderConfig +} + +type AsciidocResult struct { + converter.ResultRender + toc *tableofcontents.Fragments +} + +/* ToDo: RelPermalink patch for svg posts not working*/ +type pageSubset interface { + RelPermalink() string +} + +func (r AsciidocResult) TableOfContents() *tableofcontents.Fragments { + return r.toc +} + +func (a *AsciidocConverter) Convert(ctx converter.RenderContext) (converter.ResultRender, error) { + b, err := a.GetAsciidocContent(ctx.Src, a.Ctx) + if err != nil { + return nil, err + } + content, toc, err := a.extractTOC(b) + if err != nil { + return nil, err + } + return AsciidocResult{ + ResultRender: converter.Bytes(content), + toc: toc, + }, nil +} + +func (a *AsciidocConverter) Supports(_ identity.Identity) bool { + return false +} + +// GetAsciidocContent calls asciidoctor as an external helper +// to convert AsciiDoc content to HTML. +func (a *AsciidocConverter) GetAsciidocContent(src []byte, ctx converter.DocumentContext) ([]byte, error) { + if !HasAsciiDoc() { + a.Cfg.Logger.Errorln("asciidoctor not found in $PATH: Please install.\n", + " Leaving AsciiDoc content unrendered.") + return src, nil + } + + args := a.ParseArgs(ctx) + args = append(args, "-") + + a.Cfg.Logger.Infoln("Rendering", ctx.DocumentName, " using asciidoctor args", args, "...") + + return internal.ExternallyRenderContent(a.Cfg, ctx, src, asciiDocBinaryName, args) +} + +func (a *AsciidocConverter) ParseArgs(ctx converter.DocumentContext) []string { + cfg := a.Cfg.MarkupConfig().AsciidocExt + args := []string{} + + args = a.AppendArg(args, "-b", cfg.Backend, asciidocext_config.CliDefault.Backend, asciidocext_config.AllowedBackend) + + for _, extension := range cfg.Extensions { + if strings.LastIndexAny(extension, `\/.`) > -1 { + a.Cfg.Logger.Errorln("Unsupported asciidoctor extension was passed in. Extension `" + extension + "` ignored. Only installed asciidoctor extensions are allowed.") + continue + } + args = append(args, "-r", extension) + } + + for attributeKey, attributeValue := range cfg.Attributes { + if asciidocext_config.DisallowedAttributes[attributeKey] { + a.Cfg.Logger.Errorln("Unsupported asciidoctor attribute was passed in. Attribute `" + attributeKey + "` ignored.") + continue + } + + args = append(args, "-a", attributeKey+"="+attributeValue) + } + + if cfg.WorkingFolderCurrent { + contentDir := filepath.Dir(ctx.Filename) + destinationDir := a.Cfg.Conf.BaseConfig().PublishDir + + if destinationDir == "" { + a.Cfg.Logger.Errorln("markup.asciidocext.workingFolderCurrent requires hugo command option --destination to be set") + } + + var outDir string + var err error + + file := filepath.Base(ctx.Filename) + if a.Cfg.Conf.IsUglyURLs("") || file == "_index.adoc" || file == "index.adoc" { + outDir, err = filepath.Abs(filepath.Dir(filepath.Join(destinationDir, ctx.DocumentName))) + } else { + postDir := "" + page, ok := ctx.Document.(pageSubset) + if ok { + postDir = filepath.Base(page.RelPermalink()) + } else { + a.Cfg.Logger.Errorln("unable to cast interface to pageSubset") + } + + outDir, err = filepath.Abs(filepath.Join(destinationDir, filepath.Dir(ctx.DocumentName), postDir)) + } + + if err != nil { + a.Cfg.Logger.Errorln("asciidoctor outDir: ", err) + } + + args = append(args, "--base-dir", contentDir, "-a", "outdir="+outDir) + } + + if cfg.NoHeaderOrFooter { + args = append(args, "--no-header-footer") + } else { + a.Cfg.Logger.Warnln("asciidoctor parameter NoHeaderOrFooter is expected for correct html rendering") + } + + if cfg.SectionNumbers { + args = append(args, "--section-numbers") + } + + if cfg.Verbose { + args = append(args, "--verbose") + } + + if cfg.Trace { + args = append(args, "--trace") + } + + args = a.AppendArg(args, "--failure-level", cfg.FailureLevel, asciidocext_config.CliDefault.FailureLevel, asciidocext_config.AllowedFailureLevel) + + args = a.AppendArg(args, "--safe-mode", cfg.SafeMode, asciidocext_config.CliDefault.SafeMode, asciidocext_config.AllowedSafeMode) + + return args +} + +func (a *AsciidocConverter) AppendArg(args []string, option, value, defaultValue string, allowedValues map[string]bool) []string { + if value != defaultValue { + if allowedValues[value] { + args = append(args, option, value) + } else { + a.Cfg.Logger.Errorln("Unsupported asciidoctor value `" + value + "` for option " + option + " was passed in and will be ignored.") + } + } + return args +} + +const asciiDocBinaryName = "asciidoctor" + +func HasAsciiDoc() bool { + return hexec.InPath(asciiDocBinaryName) +} + +// extractTOC extracts the toc from the given src html. +// It returns the html without the TOC, and the TOC data +func (a *AsciidocConverter) extractTOC(src []byte) ([]byte, *tableofcontents.Fragments, error) { + var buf bytes.Buffer + buf.Write(src) + node, err := html.Parse(&buf) + if err != nil { + return nil, nil, err + } + var ( + f func(*html.Node) bool + toc *tableofcontents.Fragments + toVisit []*html.Node + ) + f = func(n *html.Node) bool { + if n.Type == html.ElementNode && n.Data == "div" && attr(n, "id") == "toc" { + toc = parseTOC(n) + if !a.Cfg.MarkupConfig().AsciidocExt.PreserveTOC { + n.Parent.RemoveChild(n) + } + return true + } + if n.FirstChild != nil { + toVisit = append(toVisit, n.FirstChild) + } + if n.NextSibling != nil && f(n.NextSibling) { + return true + } + for len(toVisit) > 0 { + nv := toVisit[0] + toVisit = toVisit[1:] + if f(nv) { + return true + } + } + return false + } + f(node) + if err != nil { + return nil, nil, err + } + buf.Reset() + err = html.Render(&buf, node) + if err != nil { + return nil, nil, err + } + // ltrim <html><head></head><body> and rtrim </body></html> which are added by html.Render + res := buf.Bytes()[25:] + res = res[:len(res)-14] + return res, toc, nil +} + +// parseTOC returns a TOC root from the given toc Node +func parseTOC(doc *html.Node) *tableofcontents.Fragments { + var ( + toc tableofcontents.Builder + f func(*html.Node, int, int) + ) + f = func(n *html.Node, row, level int) { + if n.Type == html.ElementNode { + switch n.Data { + case "ul": + if level == 0 { + row++ + } + level++ + f(n.FirstChild, row, level) + case "li": + for c := n.FirstChild; c != nil; c = c.NextSibling { + if c.Type != html.ElementNode || c.Data != "a" { + continue + } + href := attr(c, "href")[1:] + toc.AddAt(&tableofcontents.Heading{ + Title: nodeContent(c), + ID: href, + }, row, level) + } + f(n.FirstChild, row, level) + } + } + if n.NextSibling != nil { + f(n.NextSibling, row, level) + } + } + f(doc.FirstChild, -1, 0) + return toc.Build() +} + +func attr(node *html.Node, key string) string { + for _, a := range node.Attr { + if a.Key == key { + return a.Val + } + } + return "" +} + +func nodeContent(node *html.Node) string { + var buf bytes.Buffer + for c := node.FirstChild; c != nil; c = c.NextSibling { + html.Render(&buf, c) + } + return buf.String() +} |