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 }