github.com/graemephi/kahugo@v0.62.3-0.20211121071557-d78c0423784d/markup/asciidocext/convert.go (about)

     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/htesting"
    25  
    26  	"github.com/cli/safeexec"
    27  
    28  	"github.com/gohugoio/hugo/identity"
    29  	"github.com/gohugoio/hugo/markup/asciidocext/asciidocext_config"
    30  	"github.com/gohugoio/hugo/markup/converter"
    31  	"github.com/gohugoio/hugo/markup/internal"
    32  	"github.com/gohugoio/hugo/markup/tableofcontents"
    33  	"golang.org/x/net/html"
    34  )
    35  
    36  /* ToDo: RelPermalink patch for svg posts not working*/
    37  type pageSubset interface {
    38  	RelPermalink() string
    39  }
    40  
    41  // Provider is the package entry point.
    42  var Provider converter.ProviderProvider = provider{}
    43  
    44  type provider struct{}
    45  
    46  func (p provider) New(cfg converter.ProviderConfig) (converter.Provider, error) {
    47  	return converter.NewProvider("asciidocext", func(ctx converter.DocumentContext) (converter.Converter, error) {
    48  		return &asciidocConverter{
    49  			ctx: ctx,
    50  			cfg: cfg,
    51  		}, nil
    52  	}), nil
    53  }
    54  
    55  type asciidocResult struct {
    56  	converter.Result
    57  	toc tableofcontents.Root
    58  }
    59  
    60  func (r asciidocResult) TableOfContents() tableofcontents.Root {
    61  	return r.toc
    62  }
    63  
    64  type asciidocConverter struct {
    65  	ctx converter.DocumentContext
    66  	cfg converter.ProviderConfig
    67  }
    68  
    69  func (a *asciidocConverter) Convert(ctx converter.RenderContext) (converter.Result, error) {
    70  	content, toc, err := a.extractTOC(a.getAsciidocContent(ctx.Src, a.ctx))
    71  	if err != nil {
    72  		return nil, err
    73  	}
    74  	return asciidocResult{
    75  		Result: converter.Bytes(content),
    76  		toc:    toc,
    77  	}, nil
    78  }
    79  
    80  func (a *asciidocConverter) Supports(_ identity.Identity) bool {
    81  	return false
    82  }
    83  
    84  // getAsciidocContent calls asciidoctor as an external helper
    85  // to convert AsciiDoc content to HTML.
    86  func (a *asciidocConverter) getAsciidocContent(src []byte, ctx converter.DocumentContext) []byte {
    87  	path := getAsciidoctorExecPath()
    88  	if path == "" {
    89  		a.cfg.Logger.Errorln("asciidoctor not found in $PATH: Please install.\n",
    90  			"                 Leaving AsciiDoc content unrendered.")
    91  		return src
    92  	}
    93  
    94  	args := a.parseArgs(ctx)
    95  	args = append(args, "-")
    96  
    97  	a.cfg.Logger.Infoln("Rendering", ctx.DocumentName, "with", path, "using asciidoctor args", args, "...")
    98  
    99  	return internal.ExternallyRenderContent(a.cfg, ctx, src, path, args)
   100  }
   101  
   102  func (a *asciidocConverter) parseArgs(ctx converter.DocumentContext) []string {
   103  	cfg := a.cfg.MarkupConfig.AsciidocExt
   104  	args := []string{}
   105  
   106  	args = a.appendArg(args, "-b", cfg.Backend, asciidocext_config.CliDefault.Backend, asciidocext_config.AllowedBackend)
   107  
   108  	for _, extension := range cfg.Extensions {
   109  		if strings.LastIndexAny(extension, `\/.`) > -1 {
   110  			a.cfg.Logger.Errorln("Unsupported asciidoctor extension was passed in. Extension `" + extension + "` ignored. Only installed asciidoctor extensions are allowed.")
   111  			continue
   112  		}
   113  		args = append(args, "-r", extension)
   114  	}
   115  
   116  	for attributeKey, attributeValue := range cfg.Attributes {
   117  		if asciidocext_config.DisallowedAttributes[attributeKey] {
   118  			a.cfg.Logger.Errorln("Unsupported asciidoctor attribute was passed in. Attribute `" + attributeKey + "` ignored.")
   119  			continue
   120  		}
   121  
   122  		args = append(args, "-a", attributeKey+"="+attributeValue)
   123  	}
   124  
   125  	if cfg.WorkingFolderCurrent {
   126  		contentDir := filepath.Dir(ctx.Filename)
   127  		sourceDir := a.cfg.Cfg.GetString("source")
   128  		destinationDir := a.cfg.Cfg.GetString("destination")
   129  
   130  		if destinationDir == "" {
   131  			a.cfg.Logger.Errorln("markup.asciidocext.workingFolderCurrent requires hugo command option --destination to be set")
   132  		}
   133  		if !filepath.IsAbs(destinationDir) && sourceDir != "" {
   134  			destinationDir = filepath.Join(sourceDir, destinationDir)
   135  		}
   136  
   137  		var outDir string
   138  		var err error
   139  
   140  		file := filepath.Base(ctx.Filename)
   141  		if a.cfg.Cfg.GetBool("uglyUrls") || file == "_index.adoc" || file == "index.adoc" {
   142  			outDir, err = filepath.Abs(filepath.Dir(filepath.Join(destinationDir, ctx.DocumentName)))
   143  		} else {
   144  			postDir := ""
   145  			page, ok := ctx.Document.(pageSubset)
   146  			if ok {
   147  				postDir = filepath.Base(page.RelPermalink())
   148  			} else {
   149  				a.cfg.Logger.Errorln("unable to cast interface to pageSubset")
   150  			}
   151  
   152  			outDir, err = filepath.Abs(filepath.Join(destinationDir, filepath.Dir(ctx.DocumentName), postDir))
   153  		}
   154  
   155  		if err != nil {
   156  			a.cfg.Logger.Errorln("asciidoctor outDir: ", err)
   157  		}
   158  
   159  		args = append(args, "--base-dir", contentDir, "-a", "outdir="+outDir)
   160  	}
   161  
   162  	if cfg.NoHeaderOrFooter {
   163  		args = append(args, "--no-header-footer")
   164  	} else {
   165  		a.cfg.Logger.Warnln("asciidoctor parameter NoHeaderOrFooter is expected for correct html rendering")
   166  	}
   167  
   168  	if cfg.SectionNumbers {
   169  		args = append(args, "--section-numbers")
   170  	}
   171  
   172  	if cfg.Verbose {
   173  		args = append(args, "--verbose")
   174  	}
   175  
   176  	if cfg.Trace {
   177  		args = append(args, "--trace")
   178  	}
   179  
   180  	args = a.appendArg(args, "--failure-level", cfg.FailureLevel, asciidocext_config.CliDefault.FailureLevel, asciidocext_config.AllowedFailureLevel)
   181  
   182  	args = a.appendArg(args, "--safe-mode", cfg.SafeMode, asciidocext_config.CliDefault.SafeMode, asciidocext_config.AllowedSafeMode)
   183  
   184  	return args
   185  }
   186  
   187  func (a *asciidocConverter) appendArg(args []string, option, value, defaultValue string, allowedValues map[string]bool) []string {
   188  	if value != defaultValue {
   189  		if allowedValues[value] {
   190  			args = append(args, option, value)
   191  		} else {
   192  			a.cfg.Logger.Errorln("Unsupported asciidoctor value `" + value + "` for option " + option + " was passed in and will be ignored.")
   193  		}
   194  	}
   195  	return args
   196  }
   197  
   198  func getAsciidoctorExecPath() string {
   199  	path, err := safeexec.LookPath("asciidoctor")
   200  	if err != nil {
   201  		return ""
   202  	}
   203  	return path
   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  	if htesting.SupportsAll() {
   315  		return true
   316  	}
   317  	return getAsciidoctorExecPath() != ""
   318  }