markup/asciidocext: Improve Asciidoctor integration

Fixes an issue where improper attribute derivation from the page's
relative permalink caused failures with `outdir`, `imagesoutdir`, and
`imagesdir` when `markup.asciidocext.workingFolderCurrent` is enabled.
The updated logic now correctly handles:

- Multi-byte characters
- Multilingual multi-host sites
- Site builds from a subdirectory
- Pages using ugly URLs

Supports diagram caching as implemented in v3.1.0 of the asciidoctor-diagram
extension:

- Enables caching by default
- Sets default cache location to the compiled value of caches.misc.dir

Reduces duration of integration tests by:

- Generating GoAT diagrams instead of Ditaa diagrams
- Taking advantage of asciidoctor-diagram caching

Closes #9202
Closes #10183
Closes #10473
Closes #14160
This commit is contained in:
Joe Mooring
2025-10-24 12:11:54 -07:00
committed by Bjørn Erik Pedersen
parent 34b0c15a54
commit 3d21b0687b
17 changed files with 1949 additions and 673 deletions
File diff suppressed because it is too large Load Diff
+25 -8
View File
@@ -29,21 +29,38 @@ type provider struct{}
func (p provider) New(cfg converter.ProviderConfig) (converter.Provider, error) {
return converter.NewProvider("asciidocext", func(ctx converter.DocumentContext) (converter.Converter, error) {
return &internal.AsciidocConverter{
return &internal.AsciiDocConverter{
Ctx: ctx,
Cfg: cfg,
}, nil
}), nil
}
// Supports returns whether Asciidoctor is installed on this computer.
func Supports() bool {
hasBin := internal.HasAsciiDoc()
// Supports reports whether the AsciiDoc converter is installed. Only used in
// tests.
func Supports() (bool, error) {
hasAsciiDoc, err := internal.HasAsciiDoc()
if htesting.SupportsAll() {
if !hasBin {
panic("asciidoctor not installed")
if !hasAsciiDoc {
panic(err)
}
return true
return true, nil
}
return hasBin
return hasAsciiDoc, err
}
// SupportsGoATDiagrams reports whether the AsciiDoc converter can render GoAT
// diagrams. Only used in tests.
func SupportsGoATDiagrams() (bool, error) {
supportsGoATDiagrams, err := internal.CanRenderGoATDiagrams()
if htesting.SupportsAll() {
if !supportsGoATDiagrams {
panic(err)
}
return true, nil
}
return supportsGoATDiagrams, err
}
+85 -113
View File
@@ -11,14 +11,14 @@
// See the License for the specific language governing permissions and
// limitations under the License.
// Package asciidocext converts AsciiDoc to HTML using Asciidoctor
// Package asciidocext converts AsciiDoc to HTML using the Asciidoctor
// external binary. The `asciidoc` module is reserved for a future golang
// implementation.
package asciidocext_test
import (
"path/filepath"
"encoding/json"
"testing"
"github.com/gohugoio/hugo/common/collections"
@@ -28,6 +28,7 @@ import (
"github.com/gohugoio/hugo/config/security"
"github.com/gohugoio/hugo/config/testconfig"
"github.com/gohugoio/hugo/markup/asciidocext"
"github.com/gohugoio/hugo/markup/asciidocext/asciidocext_config"
"github.com/gohugoio/hugo/markup/asciidocext/internal"
"github.com/gohugoio/hugo/markup/converter"
"github.com/gohugoio/hugo/markup/markup_config"
@@ -36,6 +37,24 @@ import (
qt "github.com/frankban/quicktest"
)
var defaultAsciiDocExtConfigAsJSON []byte
func init() {
var err error
defaultAsciiDocExtConfigAsJSON, err = json.Marshal(markup_config.Default.AsciiDocExt)
if err != nil {
panic(err)
}
}
func resetDefaultAsciiDocExtConfig() {
markup_config.Default.AsciiDocExt = asciidocext_config.Config{}
err := json.Unmarshal(defaultAsciiDocExtConfigAsJSON, &markup_config.Default.AsciiDocExt)
if err != nil {
panic(err)
}
}
func TestAsciidoctorDefaultArgs(t *testing.T) {
c := qt.New(t)
cfg := config.New()
@@ -52,24 +71,30 @@ func TestAsciidoctorDefaultArgs(t *testing.T) {
conv, err := p.New(converter.DocumentContext{})
c.Assert(err, qt.IsNil)
ac := conv.(*internal.AsciidocConverter)
ac := conv.(*internal.AsciiDocConverter)
c.Assert(ac, qt.Not(qt.IsNil))
args := ac.ParseArgs(converter.DocumentContext{})
args, err := ac.ParseArgs(converter.DocumentContext{})
c.Assert(err, qt.IsNil)
expected := []string{"--no-header-footer"}
c.Assert(args, qt.DeepEquals, expected)
}
func TestAsciidoctorNonDefaultArgs(t *testing.T) {
c := qt.New(t)
t.Cleanup(func() {
resetDefaultAsciiDocExtConfig()
})
mconf := markup_config.Default
mconf.AsciidocExt.Backend = "manpage"
mconf.AsciidocExt.NoHeaderOrFooter = false
mconf.AsciidocExt.SafeMode = "safe"
mconf.AsciidocExt.SectionNumbers = true
mconf.AsciidocExt.Verbose = true
mconf.AsciidocExt.Trace = false
mconf.AsciidocExt.FailureLevel = "warn"
mconf.AsciiDocExt.Backend = "manpage"
mconf.AsciiDocExt.NoHeaderOrFooter = false
mconf.AsciiDocExt.SafeMode = "safe"
mconf.AsciiDocExt.SectionNumbers = true
mconf.AsciiDocExt.Verbose = true
mconf.AsciiDocExt.Trace = false
mconf.AsciiDocExt.FailureLevel = "warn"
conf := testconfig.GetTestConfigSectionFromStruct("markup", mconf)
@@ -84,22 +109,28 @@ func TestAsciidoctorNonDefaultArgs(t *testing.T) {
conv, err := p.New(converter.DocumentContext{})
c.Assert(err, qt.IsNil)
ac := conv.(*internal.AsciidocConverter)
ac := conv.(*internal.AsciiDocConverter)
c.Assert(ac, qt.Not(qt.IsNil))
args := ac.ParseArgs(converter.DocumentContext{})
args, err := ac.ParseArgs(converter.DocumentContext{})
c.Assert(err, qt.IsNil)
expected := []string{"-b", "manpage", "--section-numbers", "--verbose", "--failure-level", "warn", "--safe-mode", "safe"}
c.Assert(args, qt.DeepEquals, expected)
}
func TestAsciidoctorDisallowedArgs(t *testing.T) {
c := qt.New(t)
t.Cleanup(func() {
resetDefaultAsciiDocExtConfig()
})
mconf := markup_config.Default
mconf.AsciidocExt.Backend = "disallowed-backend"
mconf.AsciidocExt.Extensions = []string{"./disallowed-extension"}
mconf.AsciidocExt.Attributes = map[string]any{"outdir": "disallowed-attribute"}
mconf.AsciidocExt.SafeMode = "disallowed-safemode"
mconf.AsciidocExt.FailureLevel = "disallowed-failurelevel"
mconf.AsciiDocExt.Backend = "disallowed-backend"
mconf.AsciiDocExt.Extensions = []string{"./disallowed-extension"}
mconf.AsciiDocExt.Attributes = map[string]any{"outdir": "disallowed-attribute"}
mconf.AsciiDocExt.SafeMode = "disallowed-safemode"
mconf.AsciiDocExt.FailureLevel = "disallowed-failurelevel"
conf := testconfig.GetTestConfigSectionFromStruct("markup", mconf)
@@ -114,18 +145,24 @@ func TestAsciidoctorDisallowedArgs(t *testing.T) {
conv, err := p.New(converter.DocumentContext{})
c.Assert(err, qt.IsNil)
ac := conv.(*internal.AsciidocConverter)
ac := conv.(*internal.AsciiDocConverter)
c.Assert(ac, qt.Not(qt.IsNil))
args := ac.ParseArgs(converter.DocumentContext{})
args, err := ac.ParseArgs(converter.DocumentContext{})
c.Assert(err, qt.IsNil)
expected := []string{"--no-header-footer"}
c.Assert(args, qt.DeepEquals, expected)
}
func TestAsciidoctorArbitraryExtension(t *testing.T) {
c := qt.New(t)
t.Cleanup(func() {
resetDefaultAsciiDocExtConfig()
})
mconf := markup_config.Default
mconf.AsciidocExt.Extensions = []string{"arbitrary-extension"}
mconf.AsciiDocExt.Extensions = []string{"arbitrary-extension"}
conf := testconfig.GetTestConfigSectionFromStruct("markup", mconf)
p, err := asciidocext.Provider.New(
converter.ProviderConfig{
@@ -138,10 +175,11 @@ func TestAsciidoctorArbitraryExtension(t *testing.T) {
conv, err := p.New(converter.DocumentContext{})
c.Assert(err, qt.IsNil)
ac := conv.(*internal.AsciidocConverter)
ac := conv.(*internal.AsciiDocConverter)
c.Assert(ac, qt.Not(qt.IsNil))
args := ac.ParseArgs(converter.DocumentContext{})
args, err := ac.ParseArgs(converter.DocumentContext{})
c.Assert(err, qt.IsNil)
expected := []string{"-r", "arbitrary-extension", "--no-header-footer"}
c.Assert(args, qt.DeepEquals, expected)
}
@@ -149,6 +187,10 @@ func TestAsciidoctorArbitraryExtension(t *testing.T) {
func TestAsciidoctorDisallowedExtension(t *testing.T) {
c := qt.New(t)
t.Cleanup(func() {
resetDefaultAsciiDocExtConfig()
})
for _, disallowedExtension := range []string{
`foo-bar//`,
`foo-bar\\ `,
@@ -159,7 +201,7 @@ func TestAsciidoctorDisallowedExtension(t *testing.T) {
`foo.bar`,
} {
mconf := markup_config.Default
mconf.AsciidocExt.Extensions = []string{disallowedExtension}
mconf.AsciiDocExt.Extensions = []string{disallowedExtension}
conf := testconfig.GetTestConfigSectionFromStruct("markup", mconf)
p, err := asciidocext.Provider.New(
converter.ProviderConfig{
@@ -172,94 +214,23 @@ func TestAsciidoctorDisallowedExtension(t *testing.T) {
conv, err := p.New(converter.DocumentContext{})
c.Assert(err, qt.IsNil)
ac := conv.(*internal.AsciidocConverter)
ac := conv.(*internal.AsciiDocConverter)
c.Assert(ac, qt.Not(qt.IsNil))
args := ac.ParseArgs(converter.DocumentContext{})
args, err := ac.ParseArgs(converter.DocumentContext{})
c.Assert(err, qt.IsNil)
expected := []string{"--no-header-footer"}
c.Assert(args, qt.DeepEquals, expected)
}
}
func TestAsciidoctorWorkingFolderCurrent(t *testing.T) {
c := qt.New(t)
cfg := config.FromTOMLConfigString(`
[markup]
[markup.asciidocext]
workingFolderCurrent = true
trace = false
`)
conf := testconfig.GetTestConfig(afero.NewMemMapFs(), cfg)
p, err := asciidocext.Provider.New(
converter.ProviderConfig{
Conf: conf,
Logger: loggers.NewDefault(),
},
)
c.Assert(err, qt.IsNil)
ctx := converter.DocumentContext{Filename: "/tmp/hugo_asciidoc_ddd/docs/chapter2/index.adoc", DocumentName: "chapter2/index.adoc"}
conv, err := p.New(ctx)
c.Assert(err, qt.IsNil)
ac := conv.(*internal.AsciidocConverter)
c.Assert(ac, qt.Not(qt.IsNil))
args := ac.ParseArgs(ctx)
c.Assert(len(args), qt.Equals, 5)
c.Assert(args[0], qt.Equals, "--base-dir")
c.Assert(filepath.ToSlash(args[1]), qt.Matches, "/tmp/hugo_asciidoc_ddd/docs/chapter2")
c.Assert(args[2], qt.Equals, "-a")
c.Assert(args[3], qt.Matches, `outdir=.*chapter2`)
c.Assert(args[4], qt.Equals, "--no-header-footer")
}
func TestAsciidoctorWorkingFolderCurrentAndExtensions(t *testing.T) {
c := qt.New(t)
cfg := config.FromTOMLConfigString(`
[markup]
[markup.asciidocext]
backend = "html5s"
workingFolderCurrent = true
trace = false
noHeaderOrFooter = true
extensions = ["asciidoctor-html5s", "asciidoctor-diagram"]
`)
conf := testconfig.GetTestConfig(afero.NewMemMapFs(), cfg)
p, err := asciidocext.Provider.New(
converter.ProviderConfig{
Conf: conf,
Logger: loggers.NewDefault(),
},
)
c.Assert(err, qt.IsNil)
conv, err := p.New(converter.DocumentContext{})
c.Assert(err, qt.IsNil)
ac := conv.(*internal.AsciidocConverter)
c.Assert(ac, qt.Not(qt.IsNil))
args := ac.ParseArgs(converter.DocumentContext{})
c.Assert(len(args), qt.Equals, 11)
c.Assert(args[0], qt.Equals, "-b")
c.Assert(args[1], qt.Equals, "html5s")
c.Assert(args[2], qt.Equals, "-r")
c.Assert(args[3], qt.Equals, "asciidoctor-html5s")
c.Assert(args[4], qt.Equals, "-r")
c.Assert(args[5], qt.Equals, "asciidoctor-diagram")
c.Assert(args[6], qt.Equals, "--base-dir")
c.Assert(args[7], qt.Equals, ".")
c.Assert(args[8], qt.Equals, "-a")
c.Assert(args[9], qt.Contains, "outdir=")
c.Assert(args[10], qt.Equals, "--no-header-footer")
}
func TestAsciidoctorAttributes(t *testing.T) {
c := qt.New(t)
t.Cleanup(func() {
resetDefaultAsciiDocExtConfig()
})
cfg := config.FromTOMLConfigString(`
[markup]
[markup.asciidocext]
@@ -282,7 +253,7 @@ my-attribute-false = false
conv, err := p.New(converter.DocumentContext{})
c.Assert(err, qt.IsNil)
ac := conv.(*internal.AsciidocConverter)
ac := conv.(*internal.AsciiDocConverter)
c.Assert(ac, qt.Not(qt.IsNil))
expectedValues := map[string]bool{
@@ -292,7 +263,8 @@ my-attribute-false = false
"'!my-attribute-false'": true,
}
args := ac.ParseArgs(converter.DocumentContext{})
args, err := ac.ParseArgs(converter.DocumentContext{})
c.Assert(err, qt.IsNil)
c.Assert(len(args), qt.Equals, 9)
c.Assert(args[0], qt.Equals, "-a")
c.Assert(expectedValues[args[1]], qt.Equals, true)
@@ -329,8 +301,8 @@ allow = ['asciidoctor']
}
func TestConvert(t *testing.T) {
if !asciidocext.Supports() {
t.Skip("asciidoctor not installed")
if ok, err := asciidocext.Supports(); !ok {
t.Skip(err)
}
c := qt.New(t)
@@ -345,8 +317,8 @@ func TestConvert(t *testing.T) {
}
func TestTableOfContents(t *testing.T) {
if !asciidocext.Supports() {
t.Skip("asciidoctor not installed")
if ok, err := asciidocext.Supports(); !ok {
t.Skip(err)
}
c := qt.New(t)
p := getProvider(c, "")
@@ -387,8 +359,8 @@ testContent
}
func TestTableOfContentsWithCode(t *testing.T) {
if !asciidocext.Supports() {
t.Skip("asciidoctor not installed")
if ok, err := asciidocext.Supports(); !ok {
t.Skip(err)
}
c := qt.New(t)
p := getProvider(c, "")
@@ -406,8 +378,8 @@ func TestTableOfContentsWithCode(t *testing.T) {
}
func TestTableOfContentsPreserveTOC(t *testing.T) {
if !asciidocext.Supports() {
t.Skip("asciidoctor not installed")
if ok, err := asciidocext.Supports(); !ok {
t.Skip(err)
}
c := qt.New(t)
confStr := `
+175 -50
View File
@@ -2,11 +2,17 @@ package internal
import (
"bytes"
"fmt"
"net/url"
"path"
"path/filepath"
"strings"
"github.com/gohugoio/hugo/common/hexec"
"github.com/gohugoio/hugo/common/loggers"
"github.com/gohugoio/hugo/config/security"
"github.com/gohugoio/hugo/identity"
"github.com/gohugoio/hugo/langs"
"github.com/gohugoio/hugo/markup/asciidocext/asciidocext_config"
"github.com/gohugoio/hugo/markup/converter"
"github.com/gohugoio/hugo/markup/internal"
@@ -15,27 +21,52 @@ import (
"golang.org/x/net/html"
)
type AsciidocConverter struct {
type AsciiDocConverter struct {
Ctx converter.DocumentContext
Cfg converter.ProviderConfig
}
type AsciidocResult struct {
type asciiDocResult struct {
converter.ResultRender
toc *tableofcontents.Fragments
}
/* ToDo: RelPermalink patch for svg posts not working*/
type pageSubset interface {
IsPage() bool
RelPermalink() string
Section() string
}
func (r AsciidocResult) TableOfContents() *tableofcontents.Fragments {
const (
// asciiDocBinaryName is name of the AsciiDoc converter CLI.
asciiDocBinaryName = "asciidoctor"
// asciiDocDiagramExtension is the name of the AsciiDoc converter diagram
// extension.
asciiDocDiagramExtension = "asciidoctor-diagram"
// asciiDocDiagramCacheDirKey is the AsciiDoc converter attribute key for
// setting the path to the diagram cache directory.
asciiDocDiagramCacheDirKey = "diagram-cachedir"
// asciiDocDiagramCacheImagesOptionKey is the AsciiDoc converter attribute
// key for determining whether to cache image files in addition to
// metadata files.
asciiDocDiagramCacheImagesOptionKey = "diagram-cache-images-option"
// gemBinaryName is the name of the RubyGems CLI.
gemBinaryName = "gem"
// goatBinaryName is the name of the GoAT CLI.
goatBinaryName = "goat"
)
func (r asciiDocResult) TableOfContents() *tableofcontents.Fragments {
return r.toc
}
func (a *AsciidocConverter) Convert(ctx converter.RenderContext) (converter.ResultRender, error) {
b, err := a.GetAsciidocContent(ctx.Src, a.Ctx)
func (a *AsciiDocConverter) Convert(ctx converter.RenderContext) (converter.ResultRender, error) {
b, err := a.GetAsciiDocContent(ctx.Src, a.Ctx)
if err != nil {
return nil, err
}
@@ -43,50 +74,81 @@ func (a *AsciidocConverter) Convert(ctx converter.RenderContext) (converter.Resu
if err != nil {
return nil, err
}
return AsciidocResult{
return asciiDocResult{
ResultRender: converter.Bytes(content),
toc: toc,
}, nil
}
func (a *AsciidocConverter) Supports(_ identity.Identity) bool {
func (a *AsciiDocConverter) Supports(_ identity.Identity) bool {
return false
}
// GetAsciidocContent calls asciidoctor as an external helper
// to convert AsciiDoc content to HTML.
func (a *AsciidocConverter) GetAsciidocContent(src []byte, ctx converter.DocumentContext) ([]byte, error) {
if !HasAsciiDoc() {
a.Cfg.Logger.Errorln("asciidoctor not found in $PATH: Please install.\n",
" Leaving AsciiDoc content unrendered.")
// GetAsciiDocContent calls asciidoctor as an external helper to convert
// AsciiDoc content to HTML.
func (a *AsciiDocConverter) GetAsciiDocContent(src []byte, ctx converter.DocumentContext) ([]byte, error) {
if ok, err := HasAsciiDoc(); !ok {
a.Cfg.Logger.Errorf("leaving AsciiDoc content unrendered: %s", err.Error())
return src, nil
}
args := a.ParseArgs(ctx)
args = append(args, "-")
args, err := a.ParseArgs(ctx)
if err != nil {
return nil, err
}
args = append(args, "-") // read from stdin
a.Cfg.Logger.Infoln("Rendering", ctx.DocumentName, " using asciidoctor args", args, "...")
a.Cfg.Logger.Infof("Rendering %s using Asciidoctor args %s ...", ctx.DocumentName, args)
return internal.ExternallyRenderContent(a.Cfg, ctx, src, asciiDocBinaryName, args)
}
func (a *AsciidocConverter) ParseArgs(ctx converter.DocumentContext) []string {
cfg := a.Cfg.MarkupConfig().AsciidocExt
func (a *AsciiDocConverter) ParseArgs(ctx converter.DocumentContext) ([]string, error) {
cfg := a.Cfg.MarkupConfig().AsciiDocExt
args := []string{}
args = a.AppendArg(args, "-b", cfg.Backend, asciidocext_config.CliDefault.Backend, asciidocext_config.AllowedBackend)
for _, extension := range cfg.Extensions {
if strings.LastIndexAny(extension, `\/.`) > -1 {
a.Cfg.Logger.Errorln("Unsupported asciidoctor extension was passed in. Extension `" + extension + "` ignored. Only installed asciidoctor extensions are allowed.")
a.Cfg.Logger.Errorf(
"The %q Asciidoctor extension is unsupported and ignored. Only installed Asciidoctor extensions are allowed.",
extension,
)
continue
}
args = append(args, "-r", extension)
if extension == asciiDocDiagramExtension {
cacheDir := filepath.Clean(filepath.Join(a.Cfg.Conf.CacheDirMisc(), asciiDocDiagramExtension))
args = append(args, "-a", asciiDocDiagramCacheDirKey+"="+cacheDir)
args = append(args, "-a", asciiDocDiagramCacheImagesOptionKey)
}
}
for attributeKey, attributeValue := range cfg.Attributes {
if asciidocext_config.DisallowedAttributes[attributeKey] {
a.Cfg.Logger.Errorln("Unsupported asciidoctor attribute was passed in. Attribute `" + attributeKey + "` ignored.")
a.Cfg.Logger.Errorf(
"The %q Asciidoctor attribute is unsupported and ignored.",
attributeKey,
)
continue
}
if attributeKey == asciiDocDiagramCacheImagesOptionKey {
a.Cfg.Logger.Warnf(
"The %q Asciidoctor attribute is fixed and cannot be modified. To disable caching of both image and metadata files, set markup.asciidocext.attributes.diagram-nocache-option to true in your site configuration.",
attributeKey,
)
continue
}
if attributeKey == asciiDocDiagramCacheDirKey {
a.Cfg.Logger.Warnf(
"The %q Asciidoctor attribute is fixed and cannot be modified. To change the cache location, modify caches.misc.dir in your site configuration.",
attributeKey,
)
continue
}
@@ -105,42 +167,63 @@ func (a *AsciidocConverter) ParseArgs(ctx converter.DocumentContext) []string {
}
if cfg.WorkingFolderCurrent {
contentDir := filepath.Dir(ctx.Filename)
destinationDir := a.Cfg.Conf.BaseConfig().PublishDir
if destinationDir == "" {
a.Cfg.Logger.Errorln("markup.asciidocext.workingFolderCurrent requires hugo command option --destination to be set")
page, ok := ctx.Document.(pageSubset)
if !ok {
return nil, fmt.Errorf("expected pageSubset, got %T", ctx.Document)
}
var outDir string
var err error
// Derive the outdir document attribute from the relative permalink.
relPath := strings.TrimPrefix(page.RelPermalink(), a.Cfg.Conf.BaseURL().BasePathNoTrailingSlash)
relPath, err := url.PathUnescape(relPath)
if err != nil {
return nil, err
}
file := filepath.Base(ctx.Filename)
if a.Cfg.Conf.IsUglyURLs("") || file == "_index.adoc" || file == "index.adoc" {
outDir, err = filepath.Abs(filepath.Dir(filepath.Join(destinationDir, ctx.DocumentName)))
} else {
postDir := ""
page, ok := ctx.Document.(pageSubset)
if ok {
postDir = filepath.Base(page.RelPermalink())
if a.Cfg.Conf.IsMultihost() {
// In a multi-host configuration, neither absolute nor relative
// permalinks include the language key; prepend it.
language, ok := a.Cfg.Conf.Language().(*langs.Language)
if !ok {
return nil, fmt.Errorf("expected *langs.Language, got %T", a.Cfg.Conf.Language())
}
relPath = path.Join(language.Lang, relPath)
}
if a.Cfg.Conf.IsUglyURLs(page.Section()) {
if page.IsPage() {
// Remove the extension.
relPath = strings.TrimSuffix(relPath, path.Ext(relPath))
} else {
a.Cfg.Logger.Errorln("unable to cast interface to pageSubset")
// Remove the file name.
relPath = path.Dir(relPath)
}
outDir, err = filepath.Abs(filepath.Join(destinationDir, filepath.Dir(ctx.DocumentName), postDir))
}
// Set imagesoutdir and imagesdir attributes.
imagesoutdir, err := filepath.Abs(filepath.Join(a.Cfg.Conf.BaseConfig().PublishDir, relPath))
if err != nil {
return nil, err
}
imagesdir := filepath.Base(imagesoutdir)
if page.IsPage() {
args = append(args, "-a", "imagesoutdir="+imagesoutdir, "-a", "imagesdir="+imagesdir)
} else {
args = append(args, "-a", "imagesoutdir="+imagesoutdir)
}
}
// Prepend the publishDir.
outDir, err := filepath.Abs(filepath.Join(a.Cfg.Conf.BaseConfig().PublishDir, relPath))
if err != nil {
a.Cfg.Logger.Errorln("asciidoctor outDir: ", err)
return nil, err
}
args = append(args, "--base-dir", contentDir, "-a", "outdir="+outDir)
args = append(args, "--base-dir", filepath.Dir(ctx.Filename), "-a", "outdir="+outDir)
}
if cfg.NoHeaderOrFooter {
args = append(args, "--no-header-footer")
} else {
a.Cfg.Logger.Warnln("asciidoctor parameter NoHeaderOrFooter is expected for correct html rendering")
a.Cfg.Logger.Warnln("Asciidoctor parameter NoHeaderOrFooter is required for correct HTML rendering")
}
if cfg.SectionNumbers {
@@ -159,29 +242,71 @@ func (a *AsciidocConverter) ParseArgs(ctx converter.DocumentContext) []string {
args = a.AppendArg(args, "--safe-mode", cfg.SafeMode, asciidocext_config.CliDefault.SafeMode, asciidocext_config.AllowedSafeMode)
return args
return args, nil
}
func (a *AsciidocConverter) AppendArg(args []string, option, value, defaultValue string, allowedValues map[string]bool) []string {
func (a *AsciiDocConverter) AppendArg(args []string, option, value, defaultValue string, allowedValues map[string]bool) []string {
if value != defaultValue {
if allowedValues[value] {
args = append(args, option, value)
} else {
a.Cfg.Logger.Errorln("Unsupported asciidoctor value `" + value + "` for option " + option + " was passed in and will be ignored.")
a.Cfg.Logger.Errorf(
"Unsupported Asciidoctor value %q for option %q was passed in and will be ignored.",
value,
option,
)
}
}
return args
}
const asciiDocBinaryName = "asciidoctor"
// HasAsciiDoc reports whether the AsciiDoc converter is installed.
func HasAsciiDoc() (bool, error) {
if !hexec.InPath(asciiDocBinaryName) {
return false, fmt.Errorf("the AsciiDoc converter (%s) is not installed", asciiDocBinaryName)
}
return true, nil
}
func HasAsciiDoc() bool {
return hexec.InPath(asciiDocBinaryName)
// CanRenderGoATDiagrams reports whether the AsciiDoc converter can render
// GoAT diagrams. Only used in tests.
func CanRenderGoATDiagrams() (bool, error) {
// Verify that the AsciiDoc converter is installed.
if ok, err := HasAsciiDoc(); !ok {
return false, err
}
// Verify that the RubyGems CLI is installed.
if !hexec.InPath(gemBinaryName) {
return false, fmt.Errorf("the RubyGems CLI (%s) is not installed", gemBinaryName)
}
// Verify that the required AsciiDoc converter extension is installed.
sc := security.DefaultConfig
sc.Exec.Allow = security.MustNewWhitelist(gemBinaryName)
ex := hexec.New(sc, "", loggers.NewDefault())
args := []any{"list", asciiDocDiagramExtension, "--installed"}
cmd, err := ex.New(gemBinaryName, args...)
if err != nil {
return false, err
}
err = cmd.Run()
if err != nil {
return false, fmt.Errorf("the %s gem is not installed", asciiDocDiagramExtension)
}
// Verify that the GoAT CLI is installed.
if !hexec.InPath(goatBinaryName) {
return false, fmt.Errorf("the GoAT CLI (%s) is not installed", goatBinaryName)
}
return true, nil
}
// extractTOC extracts the toc from the given src html.
// It returns the html without the TOC, and the TOC data
func (a *AsciidocConverter) extractTOC(src []byte) ([]byte, *tableofcontents.Fragments, error) {
func (a *AsciiDocConverter) extractTOC(src []byte) ([]byte, *tableofcontents.Fragments, error) {
var buf bytes.Buffer
buf.Write(src)
node, err := html.Parse(&buf)
@@ -196,7 +321,7 @@ func (a *AsciidocConverter) extractTOC(src []byte) ([]byte, *tableofcontents.Fra
f = func(n *html.Node) bool {
if n.Type == html.ElementNode && n.Data == "div" && attr(n, "id") == "toc" {
toc = parseTOC(n)
if !a.Cfg.MarkupConfig().AsciidocExt.PreserveTOC {
if !a.Cfg.MarkupConfig().AsciiDocExt.PreserveTOC {
n.Parent.RemoveChild(n)
}
return true
+3 -3
View File
@@ -37,8 +37,8 @@ type Config struct {
// Configuration for the Goldmark markdown engine.
Goldmark goldmark_config.Config
// Configuration for the Asciidoc external markdown engine.
AsciidocExt asciidocext_config.Config
// Configuration for the AsciiDoc external markdown engine.
AsciiDocExt asciidocext_config.Config
}
func (c *Config) Init() error {
@@ -118,5 +118,5 @@ var Default = Config{
Highlight: highlight.DefaultConfig,
Goldmark: goldmark_config.Default,
AsciidocExt: asciidocext_config.Default,
AsciiDocExt: asciidocext_config.Default,
}
+2 -2
View File
@@ -48,8 +48,8 @@ func TestConfig(t *testing.T) {
c.Assert(conf.Goldmark.Parser.Attribute.Title, qt.Equals, true)
c.Assert(conf.Goldmark.Parser.Attribute.Block, qt.Equals, false)
c.Assert(conf.AsciidocExt.WorkingFolderCurrent, qt.Equals, true)
c.Assert(conf.AsciidocExt.Extensions[0], qt.Equals, "asciidoctor-html5s")
c.Assert(conf.AsciiDocExt.WorkingFolderCurrent, qt.Equals, true)
c.Assert(conf.AsciiDocExt.Extensions[0], qt.Equals, "asciidoctor-html5s")
})
// We changed the typographer extension config from a bool to a struct in 0.112.0.