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 }