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 }