github.com/anakojm/hugo-katex@v0.0.0-20231023141351-42d6f5de9c0b/markup/asciidocext/internal/converter.go (about)

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