You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
864 lines
28 KiB
Groovy
864 lines
28 KiB
Groovy
package org.xbib.gradle.plugin.asciidoctor
|
|
|
|
import org.gradle.api.DefaultTask
|
|
import org.gradle.api.GradleException
|
|
import org.gradle.api.InvalidUserDataException
|
|
import org.gradle.api.artifacts.Configuration
|
|
import org.gradle.api.file.CopySpec
|
|
import org.gradle.api.file.FileCollection
|
|
import org.gradle.api.file.FileTree
|
|
import org.gradle.api.internal.file.copy.CopySpecInternal
|
|
import org.gradle.api.tasks.Input
|
|
import org.gradle.api.tasks.InputDirectory
|
|
import org.gradle.api.tasks.InputFile
|
|
import org.gradle.api.tasks.InputFiles
|
|
import org.gradle.api.tasks.Internal
|
|
import org.gradle.api.tasks.Optional
|
|
import org.gradle.api.tasks.OutputDirectories
|
|
import org.gradle.api.tasks.OutputDirectory
|
|
import org.gradle.api.tasks.SkipWhenEmpty
|
|
import org.gradle.api.tasks.TaskAction
|
|
import org.gradle.api.tasks.util.PatternSet
|
|
import org.gradle.internal.FileUtils
|
|
import org.gradle.util.CollectionUtils
|
|
import org.xbib.gradle.plugin.asciidoctor.groovydsl.AsciidoctorExtensions
|
|
|
|
import java.nio.file.Path
|
|
|
|
@SuppressWarnings(['MethodCount', 'Instanceof'])
|
|
class AsciidoctorTask extends DefaultTask {
|
|
|
|
public static final String ASCIIDOCTOR_FACTORY_CLASSNAME = 'org.asciidoctor.Asciidoctor$Factory'
|
|
|
|
private static final boolean IS_WINDOWS = System.getProperty('os.name').contains('Windows')
|
|
|
|
private static final String PATH_SEPARATOR = System.getProperty('path.separator')
|
|
|
|
private static final String DOUBLE_BACKLASH = '\\\\'
|
|
|
|
private static final String BACKLASH = '\\'
|
|
|
|
private static final String SAFE_MODE_CLASSNAME = 'org.asciidoctor.SafeMode'
|
|
|
|
private static final String DEFAULT_BACKEND = AsciidoctorBackend.HTML5.id
|
|
|
|
private boolean baseDirSetToNull
|
|
|
|
private Object outDir
|
|
|
|
private Object srcDir
|
|
|
|
private final List<Object> gemPaths = []
|
|
|
|
private Set<String> backends
|
|
|
|
private Set<String> requires
|
|
|
|
private Map opts = [:]
|
|
|
|
private Map attrs = [:]
|
|
|
|
private PatternSet sourceDocumentPattern
|
|
|
|
private CopySpec resourceCopy
|
|
|
|
private static ClassLoader cl
|
|
|
|
/**
|
|
* If set to true each backend will be output to a separate subfolder below {@code outputDir}
|
|
*/
|
|
@Input
|
|
boolean separateOutputDirs = true
|
|
|
|
@Optional
|
|
@InputDirectory
|
|
File baseDir
|
|
|
|
/**
|
|
* Logs documents as they are converted
|
|
*/
|
|
@Input
|
|
boolean logDocuments = false
|
|
|
|
/**
|
|
* Old way to set only one source document
|
|
*/
|
|
@Optional
|
|
@InputFile
|
|
File sourceDocumentName
|
|
|
|
/**
|
|
* Old way to define the backend to use
|
|
*/
|
|
@Optional
|
|
@Input
|
|
String backend
|
|
|
|
@Internal
|
|
AsciidoctorProxy asciidoctor
|
|
|
|
/**
|
|
* Stores the extensions defined in the configuration phase
|
|
* to register them in the execution phase.
|
|
*/
|
|
@Internal
|
|
List<Object> asciidoctorExtensions = []
|
|
|
|
@Internal
|
|
ResourceCopyProxy resourceCopyProxy
|
|
|
|
@Internal
|
|
Configuration classpath
|
|
|
|
AsciidoctorTask() {
|
|
srcDir = project.file('src/docs/asciidoc')
|
|
}
|
|
|
|
/**
|
|
* Returns all of the Asciidoctor options
|
|
*/
|
|
@Optional
|
|
@Input
|
|
Map getOptions() { this.opts }
|
|
|
|
/** Apply a new set of Asciidoctor options, clearing any options previously set.
|
|
*
|
|
* For backwards compatibility it is still possible to replace attributes via this call. However the
|
|
* use of {@link #setAttributes(java.util.Map)} and {@link #attributes(java.util.Map)} are the now
|
|
* correct way of working with attributes
|
|
*
|
|
* @param m Map with new options
|
|
*/
|
|
@SuppressWarnings('DuplicateStringLiteral')
|
|
void setOptions(Map m) {
|
|
if (!m) return // null check
|
|
if (m.containsKey('attributes')) {
|
|
logger.warn 'Attributes found in options. Existing attributes will be replaced due to assignment. ' +
|
|
'Please use \'attributes\' method instead as current behaviour will be removed in future'
|
|
attrs = coerceLegacyAttributeFormats(m.attributes)
|
|
m.remove('attributes')
|
|
}
|
|
this.opts = m
|
|
}
|
|
|
|
/** Appends a new set of Asciidoctor options.
|
|
*
|
|
* For backwards compatibility it is still possible to append attributes via this call. However the
|
|
* use of {@link #setAttributes(java.util.Map)} and {@link #attributes(java.util.Map)} are the now
|
|
* correct way of working with attributes
|
|
*
|
|
* @param m Map with new options
|
|
*/
|
|
@SuppressWarnings('DuplicateStringLiteral')
|
|
void options(Map m) {
|
|
if (!m) return // null check
|
|
if (m.containsKey('attributes')) {
|
|
logger.warn 'Attributes found in options. These will be added to existing attributes. ' +
|
|
'Please use \'attributes\' method instead as current behaviour will be removed in future'
|
|
attributes coerceLegacyAttributeFormats(m.attributes)
|
|
m.remove('attributes')
|
|
}
|
|
this.opts += m
|
|
}
|
|
|
|
/**
|
|
* Returns the current set of Asciidoctor attributes
|
|
*/
|
|
@Optional
|
|
@Input
|
|
Map getAttributes() { this.attrs }
|
|
|
|
/**
|
|
* Applies a new set of Asciidoctor attributes, clearing any previously set
|
|
*
|
|
* @param m New map of attributes
|
|
*/
|
|
void setAttributes(Map m) {
|
|
if (m) {
|
|
this.attrs = m
|
|
} else {
|
|
this.attrs.clear()
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Appends a set of Asciidoctor attributes.
|
|
*
|
|
* @param o a Map, List or a literal (String) definition
|
|
*/
|
|
void attributes(Object... o) {
|
|
if (!o) {
|
|
this.attrs.clear()
|
|
return
|
|
}
|
|
for (input in o) {
|
|
this.attrs += coerceLegacyAttributeFormats(input)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns the set of Ruby modules to be included.
|
|
*/
|
|
@Optional
|
|
@Input
|
|
Set<String> getRequires() { this.requires }
|
|
|
|
/**
|
|
* Applies a new set of Ruby modules to be included, clearing any previous set.
|
|
*
|
|
* @param b One or more ruby modules to be included
|
|
*/
|
|
void setRequires(Object... b) {
|
|
this.requires?.clear()
|
|
requires(b)
|
|
}
|
|
|
|
/**
|
|
* Appends new set of Ruby modules to be included.
|
|
*
|
|
* @param b One or more ruby modules to be included
|
|
*/
|
|
@SuppressWarnings('ConfusingMethodName')
|
|
void requires(Object... b) {
|
|
if (this.requires == null) {
|
|
this.requires = []
|
|
}
|
|
if (!b) {
|
|
return
|
|
}
|
|
this.requires.addAll(CollectionUtils.stringize(b as List))
|
|
}
|
|
|
|
/**
|
|
* Returns the current set of Asciidoctor backends that will be used for document generation
|
|
*/
|
|
@Optional
|
|
@Input
|
|
Set<String> getBackends() { this.backends }
|
|
|
|
void setBackend(String b) {
|
|
if (!b) {
|
|
return
|
|
}
|
|
deprecated 'setBackend', 'backends', 'Using `backend` and `backends` together will result in `backend` being ignored.'
|
|
backend = b
|
|
}
|
|
|
|
/**
|
|
* Applies a new set of Asciidoctor backends that will be used for document generation clearing any
|
|
* previous backends
|
|
*
|
|
* @param b List of backends. Each item must be convertible to string.
|
|
*/
|
|
void setBackends(Object... b) {
|
|
this.backends?.clear()
|
|
backends(b)
|
|
}
|
|
|
|
/**
|
|
* Appends additional Asciidoctor backends that will be used for document generation.
|
|
*
|
|
* @param b List of backends. Each item must be convertible to string.
|
|
*/
|
|
@SuppressWarnings('ConfusingMethodName')
|
|
void backends(Object... b) {
|
|
if (this.backends == null) {
|
|
this.backends = []
|
|
}
|
|
if (!b) {
|
|
return
|
|
}
|
|
this.backends.addAll(CollectionUtils.stringize(b as List))
|
|
}
|
|
|
|
/** Defines extensions. The given parameters should
|
|
* either contain Asciidoctor Groovy DSL closures or files
|
|
* with content conforming to the Asciidoctor Groovy DSL.
|
|
*/
|
|
void extensions(Object... exts) {
|
|
if (!exts) return // null check
|
|
asciidoctorExtensions.addAll(exts as List)
|
|
}
|
|
|
|
/** Sets a new gemPath to be used
|
|
*
|
|
* @param f A path object can be be converted with {@code project.file}.
|
|
*/
|
|
@SuppressWarnings('ConfusingMethodName')
|
|
void gemPath(Object... f) {
|
|
if (!f) {
|
|
return
|
|
}
|
|
this.gemPaths.addAll(f as List)
|
|
}
|
|
|
|
/**
|
|
* Sets a new list of GEM paths to be used.
|
|
*
|
|
* @param f A {@code File} object pointing to list of installed GEMs
|
|
*/
|
|
void setGemPath(Object... f) {
|
|
this.gemPaths.clear()
|
|
if (!f) {
|
|
return
|
|
}
|
|
this.gemPaths.addAll(f as List)
|
|
}
|
|
|
|
/**
|
|
* Assigns a single string to a GEM path, scanning it for concatenated GEM Paths, separated by the platform
|
|
* separator. This utility is only for backwards compatibility
|
|
*
|
|
* @param s
|
|
*/
|
|
void setGemPath(Object path) {
|
|
this.gemPaths.clear()
|
|
if (path instanceof CharSequence) {
|
|
setGemPath(path.tokenize(PATH_SEPARATOR))
|
|
} else if (path instanceof List) {
|
|
this.gemPaths.addAll(path)
|
|
} else if (path instanceof File || path instanceof Path) {
|
|
this.gemPaths.add(path)
|
|
} else {
|
|
throw new IllegalArgumentException("unknown path class: " + path.getClass().getName())
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns the list of paths to be used for {@code GEM_HOME}
|
|
*/
|
|
@Optional
|
|
@InputFiles
|
|
FileCollection getGemPath() {
|
|
project.files(this.gemPaths)
|
|
}
|
|
|
|
/**
|
|
* Returns the list of paths to be used for GEM installations in a format that is suitable for assignment to {@code GEM_HOME}
|
|
*
|
|
* Calling this will cause gemPath to be resolved immediately.
|
|
*/
|
|
String asGemPath() {
|
|
gemPath.files*.toString().join(PATH_SEPARATOR)
|
|
}
|
|
|
|
/**
|
|
* Sets the new Asciidoctor parent source directory.
|
|
*
|
|
* @param f An object convertible via {@code project.file}
|
|
*/
|
|
void sourceDir(Object f) {
|
|
this.srcDir = f
|
|
}
|
|
|
|
/**
|
|
* Sets the new Asciidoctor parent source directory.
|
|
*
|
|
* @param f A {@code File} object pointing to the parent source directory
|
|
*/
|
|
void setSourceDir(File f) {
|
|
this.srcDir = f
|
|
}
|
|
|
|
/**
|
|
* Returns the parent directory for Asciidoctor source. Default is {@code src/asciidoc}.
|
|
*/
|
|
@Optional
|
|
@InputDirectory
|
|
File getSourceDir() {
|
|
project.file(srcDir)
|
|
}
|
|
|
|
/**
|
|
* Sets the new Asciidoctor parent output directory.
|
|
*
|
|
* @param f An object convertible via {@code project.file}
|
|
*/
|
|
void outputDir(Object f) {
|
|
this.outDir = f
|
|
}
|
|
|
|
/**
|
|
* Sets the new Asciidoctor parent output directory.
|
|
*
|
|
* @param f A {@code File} object pointing to the parent output directory
|
|
*/
|
|
void setOutputDir(File f) {
|
|
this.outDir = f
|
|
}
|
|
|
|
/**
|
|
* Returns the current toplevel output directory
|
|
*/
|
|
@OutputDirectory
|
|
File getOutputDir() {
|
|
if (this.outDir == null) {
|
|
this.outDir = new File(project.buildDir, 'asciidoc')
|
|
}
|
|
project.file(this.outDir)
|
|
}
|
|
|
|
/**
|
|
* Returns the collection of source documents
|
|
*
|
|
* If sourceDocumentNames was not set or is empty, it will return all asciidoc files
|
|
* in {@code sourceDir}. Otherwise only the files provided earlier to sourceDocumentNames
|
|
* are returned if they are found below {@code sourceDir}
|
|
*/
|
|
@OutputDirectories
|
|
FileCollection getSourceDocumentNames() {
|
|
deprecated 'getSourceDocumentNames', 'getSourceFileTree'
|
|
sourceFileTree
|
|
}
|
|
|
|
/**
|
|
* Sets a single file to the main source file
|
|
*
|
|
* @param f A file that is relative to {@code sourceDir}
|
|
*/
|
|
void setSourceDocumentName(File f) {
|
|
deprecated 'setSourceDocumentName',
|
|
'setIncludes', 'File will be converted to a pattern.'
|
|
sources {
|
|
setIncludes([AsciidoctorUtils.getRelativePath(f.absoluteFile, sourceDir.absoluteFile)])
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Replaces the current set of source documents with a new set
|
|
*
|
|
* @parm src List of source documents, which must be convertible using {@code project.files}
|
|
*/
|
|
@SuppressWarnings('DuplicateStringLiteral')
|
|
void setSourceDocumentNames(Object... src) {
|
|
deprecated 'setSourceDocumentNames',
|
|
'setIncludes',
|
|
'Files are converted to patterns. Some might not convert correctly. ' +
|
|
'FileCollections will not convert'
|
|
File base = sourceDir.absoluteFile
|
|
def patterns = CollectionUtils.stringize(src as List).collect { String it ->
|
|
def tmpFile = new File(it)
|
|
String relPath
|
|
if (tmpFile.isAbsolute()) {
|
|
relPath = AsciidoctorUtils.getRelativePath(tmpFile.absoluteFile, base)
|
|
} else {
|
|
relPath = it
|
|
}
|
|
logger.debug "setSourceDocumentNames - Found ${it}, converted to ${relPath}"
|
|
relPath
|
|
}
|
|
sources {
|
|
setIncludes(patterns)
|
|
}
|
|
}
|
|
|
|
void setBaseDir(File baseDir) {
|
|
this.baseDir = baseDir
|
|
baseDirSetToNull = baseDir == null
|
|
}
|
|
|
|
/**
|
|
* Returns a list of all output directories.
|
|
*/
|
|
@OutputDirectories
|
|
Set<File> getOutputDirectories() {
|
|
if (separateOutputDirs) {
|
|
backends.collect { new File(outputDir, it) } as Set
|
|
} else {
|
|
[outputDir] as Set
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns a FileTree containing all of the source documents
|
|
*
|
|
* @return If {@code sources} was never called then all asciidoc source files below {@code sourceDir} will
|
|
* be included
|
|
*/
|
|
@InputFiles
|
|
@SkipWhenEmpty
|
|
FileTree getSourceFileTree() {
|
|
project.fileTree(sourceDir).
|
|
matching(this.sourceDocumentPattern ?: defaultSourceDocumentPattern)
|
|
}
|
|
|
|
/**
|
|
* Add patterns for source files or source files via a closure
|
|
*
|
|
* @param cfg PatternSet configuration closure
|
|
*/
|
|
void sources(Closure cfg) {
|
|
if (sourceDocumentPattern == null) {
|
|
sourceDocumentPattern = new PatternSet()
|
|
}
|
|
def configuration = cfg.clone()
|
|
configuration.delegate = sourceDocumentPattern
|
|
configuration()
|
|
}
|
|
|
|
/**
|
|
* Add to the CopySpec for extra files. The destination of these files will always have a parent directory
|
|
* of {@code outputDir} or {@code outputDir + backend}
|
|
*
|
|
* @param cfg CopySpec configuration closure
|
|
*/
|
|
void resources(Closure cfg) {
|
|
if (this.resourceCopy == null) {
|
|
this.resourceCopy = project.copySpec(cfg)
|
|
} else {
|
|
def configuration = cfg.clone()
|
|
configuration.delegate = this.resourceCopy
|
|
configuration()
|
|
}
|
|
}
|
|
|
|
/**
|
|
* The default PatternSet that will be used if {@code sources} was never called
|
|
*
|
|
* By default all *.adoc,*.ad,*.asc,*.asciidoc is included. Files beginning with underscore are excluded
|
|
*
|
|
*/
|
|
@Internal
|
|
PatternSet getDefaultSourceDocumentPattern() {
|
|
PatternSet ps = new PatternSet()
|
|
ps.include '**/*.adoc'
|
|
ps.include '**/*.ad'
|
|
ps.include '**/*.asc'
|
|
ps.include '**/*.asciidoc'
|
|
ps.exclude '**/_*'
|
|
}
|
|
|
|
/**
|
|
* The default CopySpec that will be used if {@code resources} was never called
|
|
*
|
|
* By default anything below {@code $sourceDir/images} will be included.
|
|
*
|
|
* @return A {@code CopySpec}, never null
|
|
*/
|
|
@Internal
|
|
CopySpec getDefaultResourceCopySpec() {
|
|
project.copySpec {
|
|
from(sourceDir) {
|
|
include 'images/**'
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gets the CopySpec for additional resources
|
|
* If {@code resources} was never called, it will return a default CopySpec otherwise it will return the
|
|
* one built up via successive calls to {@code resources}
|
|
*
|
|
* @return A {@code CopySpec}, never null
|
|
*/
|
|
@Internal
|
|
CopySpec getResourceCopySpec() {
|
|
this.resourceCopy ?: defaultResourceCopySpec
|
|
}
|
|
|
|
/**
|
|
* Gets the additional resources as a FileCollection.
|
|
* If {@code resources} was never called, it will return the file collections as per default CopySpec otherwise it
|
|
* will return the collections as built up via successive calls to {@code resources}
|
|
*
|
|
* @return A {@code FileCollection}, never null
|
|
*/
|
|
@InputFiles
|
|
@SkipWhenEmpty
|
|
@Optional
|
|
FileCollection getResourceFileCollection() {
|
|
(resourceCopySpec as CopySpecInternal).buildRootResolver().allSource
|
|
}
|
|
|
|
@TaskAction
|
|
void processAsciidocSources() {
|
|
if (sourceFileTree.files.size() == 0) {
|
|
logger.lifecycle 'Asciidoc source file tree is empty. Nothing will be processed.'
|
|
return
|
|
}
|
|
if (classpath == null) {
|
|
classpath = project.configurations.getByName(AsciidoctorPlugin.ASCIIDOCTOR)
|
|
}
|
|
setupClassLoader()
|
|
if (!asciidoctorExtensions?.empty) {
|
|
Class asciidoctorExtensionsDslRegistry = loadClass(AsciidoctorExtensions.class.getName())
|
|
asciidoctorExtensions.each { asciidoctorExtensionsDslRegistry.extensions(it) }
|
|
}
|
|
if (!asciidoctor) {
|
|
instantiateAsciidoctor()
|
|
}
|
|
if (resourceCopyProxy == null) {
|
|
resourceCopyProxy = new ResourceCopyProxyImpl(project)
|
|
}
|
|
if (requires) {
|
|
for (require in requires) {
|
|
asciidoctor.requireLibrary(require)
|
|
}
|
|
}
|
|
for (activeBackend in activeBackends()) {
|
|
if (!AsciidoctorBackend.isBuiltIn(activeBackend)) {
|
|
logger.lifecycle("Passing through unknown backend: $activeBackend")
|
|
}
|
|
processDocumentsAndResources(activeBackend)
|
|
}
|
|
}
|
|
|
|
@groovy.transform.PackageScope
|
|
File outputDirFor(final File source, final String basePath, final File outputDir, final String backend) {
|
|
String filePath = source.directory ? source.absolutePath : source.parentFile.absolutePath
|
|
String relativeFilePath = normalizePath(filePath) - normalizePath(basePath)
|
|
File baseOutputDir = outputBackendDir(outputDir, backend)
|
|
File destinationParentDir = new File(baseOutputDir, relativeFilePath)
|
|
if (!destinationParentDir.exists()) {
|
|
destinationParentDir.mkdirs()
|
|
}
|
|
destinationParentDir
|
|
}
|
|
|
|
private File outputBackendDir(final File outputDir, final String backend) {
|
|
separateOutputDirs ? new File(outputDir, FileUtils.toSafeFileName(backend)) : outputDir
|
|
}
|
|
|
|
private static String normalizePath(String path) {
|
|
if (IS_WINDOWS) {
|
|
path = path.replace(DOUBLE_BACKLASH, BACKLASH)
|
|
path = path.replace(BACKLASH, DOUBLE_BACKLASH)
|
|
}
|
|
path
|
|
}
|
|
|
|
@SuppressWarnings('CatchException')
|
|
private void instantiateAsciidoctor() {
|
|
if (gemPaths.size()) {
|
|
asciidoctor = new AsciidoctorProxyImpl(delegate: loadClass(ASCIIDOCTOR_FACTORY_CLASSNAME).create(asGemPath()))
|
|
} else {
|
|
try {
|
|
asciidoctor = new AsciidoctorProxyImpl(delegate: loadClass(ASCIIDOCTOR_FACTORY_CLASSNAME).create(null as String))
|
|
} catch (Exception e) {
|
|
asciidoctor = new AsciidoctorProxyImpl(delegate: loadClass(ASCIIDOCTOR_FACTORY_CLASSNAME).create())
|
|
}
|
|
}
|
|
}
|
|
|
|
private Set<String> activeBackends() {
|
|
if (this.backends) {
|
|
return this.backends
|
|
} else if (backend) {
|
|
return [backend]
|
|
}
|
|
[DEFAULT_BACKEND]
|
|
}
|
|
|
|
@SuppressWarnings('CatchException')
|
|
@SuppressWarnings('DuplicateStringLiteral')
|
|
private void processDocumentsAndResources(final String backend) {
|
|
try {
|
|
sourceFileTree.files.each { File file ->
|
|
if (file.name.startsWith('_')) {
|
|
throw new InvalidUserDataException('Source documents may not start with an underscore')
|
|
}
|
|
File destinationParentDir = owner.outputDirFor(file, sourceDir.absolutePath, outputDir, backend)
|
|
processSingleFile(backend, destinationParentDir, file)
|
|
}
|
|
|
|
resourceCopyProxy.copy(outputBackendDir(outputDir, backend), resourceCopySpec)
|
|
|
|
} catch (Exception e) {
|
|
throw new GradleException('Error running Asciidoctor', e)
|
|
}
|
|
}
|
|
|
|
protected void processSingleFile(String backend, File destinationParentDir, File file) {
|
|
if (logDocuments) {
|
|
logger.lifecycle("Converting $file")
|
|
}
|
|
asciidoctor.convertFile(file, mergedOptions(file,
|
|
[
|
|
project : project,
|
|
options : options,
|
|
attributes: attrs,
|
|
baseDir : !baseDir && !baseDirSetToNull ? file.parentFile : baseDir,
|
|
projectDir: project.projectDir,
|
|
rootDir : project.rootDir,
|
|
outputDir : destinationParentDir,
|
|
backend : backend]))
|
|
}
|
|
|
|
@SuppressWarnings('AbcMetric')
|
|
private static Map<String, Object> mergedOptions(File file, Map params) {
|
|
Map<String, Object> mergedOptions = [:]
|
|
mergedOptions.putAll(params.options)
|
|
mergedOptions.backend = params.backend
|
|
mergedOptions.in_place = false
|
|
mergedOptions.safe = resolveSafeModeLevel(mergedOptions.safe, 0i)
|
|
mergedOptions.to_dir = params.outputDir
|
|
if (params.baseDir) {
|
|
mergedOptions.base_dir = params.baseDir
|
|
}
|
|
|
|
if (mergedOptions.to_file) {
|
|
File toFile = new File(mergedOptions.to_file)
|
|
mergedOptions.to_file = new File(mergedOptions.remove('to_dir'), toFile.name)
|
|
}
|
|
|
|
Map attributes = [:]
|
|
processMapAttributes(attributes, params.attributes)
|
|
|
|
// Note: Directories passed as relative to work around issue #83
|
|
// Asciidoctor cannot handle absolute paths in Windows properly
|
|
attributes.projectdir = AsciidoctorUtils.getRelativePath(params.projectDir, file.parentFile)
|
|
attributes.rootdir = AsciidoctorUtils.getRelativePath(params.rootDir, file.parentFile)
|
|
|
|
// resolve these properties here as we want to catch both Map and String definitions parsed above
|
|
attributes.'project-name' = attributes.'project-name' ?: params.project.name
|
|
attributes.'project-group' = attributes.'project-group' ?: (params.project.group ?: '')
|
|
attributes.'project-version' = attributes.'project-version' ?: (params.project.version ?: '')
|
|
mergedOptions.attributes = attributes
|
|
|
|
// Issue #14 force GString -> String as jruby will fail
|
|
// to find an exact match when invoking Asciidoctor
|
|
for (entry in mergedOptions) {
|
|
if (entry.value instanceof CharSequence) {
|
|
mergedOptions[entry.key] = entry.value.toString()
|
|
} else if (entry.value instanceof List) {
|
|
mergedOptions[entry.key] = stringifyList(entry.value)
|
|
} else if (entry.value instanceof Map) {
|
|
mergedOptions[entry.key] = stringifyMap(entry.value)
|
|
} else if (entry.value instanceof File) {
|
|
mergedOptions[entry.key] = entry.value.absolutePath
|
|
}
|
|
}
|
|
|
|
mergedOptions
|
|
}
|
|
|
|
private static List stringifyList(List input) {
|
|
input.collect { element ->
|
|
if (element instanceof CharSequence) {
|
|
element.toString()
|
|
} else if (element instanceof List) {
|
|
stringifyList(element)
|
|
} else if (element instanceof Map) {
|
|
stringifyMap(element)
|
|
} else if(element instanceof File) {
|
|
element.absolutePath
|
|
} else {
|
|
element
|
|
}
|
|
}
|
|
}
|
|
|
|
private static Map stringifyMap(Map input) {
|
|
Map output = [:]
|
|
input.each { key, value ->
|
|
if (value instanceof CharSequence) {
|
|
output[key] = value.toString()
|
|
} else if (value instanceof List) {
|
|
output[key] = stringifyList(value)
|
|
} else if (value instanceof Map) {
|
|
output[key] = stringifyMap(value)
|
|
} else if(value instanceof File) {
|
|
output[key] = value.absolutePath
|
|
} else {
|
|
output[key] = value
|
|
}
|
|
}
|
|
output
|
|
}
|
|
|
|
protected static void processMapAttributes(Map attributes, Map rawAttributes) {
|
|
// copy all attributes in order to prevent changes down
|
|
// the Asciidoctor chain that could cause serialization
|
|
// problems with Gradle -> all inputs/outputs get serialized
|
|
// for caching purposes; Ruby objects are non-serializable
|
|
// Issue #14 force GString -> String as jruby will fail
|
|
// to find an exact match when invoking Asciidoctor
|
|
for (entry in rawAttributes) {
|
|
if (entry.value == null || entry.value instanceof Boolean) {
|
|
attributes[entry.key] = entry.value
|
|
} else {
|
|
attributes[entry.key] = entry.value.toString()
|
|
}
|
|
}
|
|
}
|
|
|
|
protected static void processCollectionAttributes(Map attributes, rawAttributes) {
|
|
for (attr in rawAttributes) {
|
|
if (attr instanceof CharSequence) {
|
|
def (k, v) = attr.toString().split('=', 2) as List
|
|
attributes.put(k, v != null ? v : '')
|
|
} else {
|
|
// QUESTION should we just coerce it to a String?
|
|
throw new InvalidUserDataException("Unsupported type for attribute ${attr}: ${attr.getClass()}")
|
|
}
|
|
}
|
|
}
|
|
|
|
@SuppressWarnings('DuplicateStringLiteral')
|
|
@SuppressWarnings('DuplicateNumberLiteral')
|
|
private static Map coerceLegacyAttributeFormats(Object attributes) {
|
|
Map transformedMap = [:]
|
|
switch (attributes) {
|
|
case Map:
|
|
transformedMap = attributes
|
|
break
|
|
case CharSequence:
|
|
attributes.replaceAll('([^\\\\]) ', '$1\0').replaceAll('\\\\ ', ' ').split('\0').collect {
|
|
def split = it.split('=')
|
|
if (split.size() < 2) {
|
|
throw new InvalidUserDataException("Unsupported format for attributes: ${attributes}")
|
|
}
|
|
transformedMap[split[0]] = split.drop(1).join('=')
|
|
}
|
|
break
|
|
case Collection:
|
|
processCollectionAttributes(transformedMap, attributes)
|
|
break
|
|
default:
|
|
if (attributes.class.isArray()) {
|
|
processCollectionAttributes(transformedMap, attributes)
|
|
} else {
|
|
throw new InvalidUserDataException("Unsupported type for attributes: ${attributes.class}")
|
|
}
|
|
}
|
|
|
|
transformedMap
|
|
}
|
|
|
|
private static int resolveSafeModeLevel(Object safe, int defaultLevel) {
|
|
if (safe == null) {
|
|
defaultLevel
|
|
} else if (safe.class.name == SAFE_MODE_CLASSNAME) {
|
|
safe.level
|
|
} else if (safe instanceof CharSequence) {
|
|
try {
|
|
Enum.valueOf(loadClass(SAFE_MODE_CLASSNAME), safe.toString().toUpperCase()).level
|
|
} catch (IllegalArgumentException e) {
|
|
defaultLevel
|
|
}
|
|
} else {
|
|
safe.intValue()
|
|
}
|
|
}
|
|
|
|
private static Class loadClass(String className) {
|
|
cl.loadClass(className)
|
|
}
|
|
|
|
@SuppressWarnings('AssignmentToStaticFieldFromInstanceMethod')
|
|
private void setupClassLoader() {
|
|
if (classpath?.files) {
|
|
def urls = classpath.files.collect { it.toURI().toURL() }
|
|
cl = new URLClassLoader(urls as URL[], Thread.currentThread().contextClassLoader)
|
|
Thread.currentThread().contextClassLoader = cl
|
|
} else {
|
|
cl = Thread.currentThread().contextClassLoader
|
|
}
|
|
}
|
|
|
|
private void deprecated(final String method, final String alternative, final String msg = '') {
|
|
logger.lifecycle "Asciidoctor: ${method} is deprecated and will be removed in a future version. " +
|
|
"Use ${alternative} instead. ${msg}"
|
|
}
|
|
}
|