From 9fdc7a6f3f6feec279a9892b71da8a321b4dd5ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Schalk=20W=2E=20Cronj=C3=A9?= Date: Mon, 28 May 2018 19:35:54 +0200 Subject: [PATCH] New generation Asciidoctor Gradle plugins General: - Use 'base' plugin model for Asciidoctor plugins. - Only support Gradle 4.0+. - For Gradle 4.5+ migration messages are controlled via `--warning-mode`. - For Gradle 4.0-4.4 migration messages are controlled via `--info`. - Supports JDK8+. - Additional text will be displayed on Gradle Plugin Portal for during alpha & beta releases. - Compatibility testing has been added and initialy against 4.0 and 4.6. - Codenarc settings has been updated to find configuration from within subprojects. - Updated license headers. - Integration tests utilises an offline Ivy repository (via Ivypot plugin) to reduce bandwith usage and test time. - AsciidoctorJ, GroovyDSL etc. versions are defined in code and read by the build script. This provides one point of definition and ensure that code can have appropriate default versions. New generation plugins and tasks: - New (JVM) AsciidoctorJ extension can be added to both project & tasks. Extension in task can be used to override global settings from the project extension. It takes into account some of the ideas @manuelprinz had for #231 for is much more extensive. - Extension has support for Asciidoctor verbose mode (#233). If `verboseMode` is set then a temporary file will be created and made part of Asciidoctor `requires`. - JRuby version can be controlled at both project and task level. If no version is specified at either level, the default JRuby version that AsciidoctorJ depends on will be used. - GroovyDSL extensions are now registered eitehr on the project extension or the task extensions. The extensions are only loaded within a worker instance. The asciidoctor task is unloaded at the end of the worker and with it the extensions. (#166) - AsciidoctorTask differentitates between top-level sources, secondary sources and resources in order to have a better idea of being out-of-date. (#185) - It is possible to control whether resources should be copied or not. This allows a build to better deal with a backend such as PDF, which do not need resources to be copied. - It is possible to perform the conversion from an intermediate working directory, which allows for a source directory to be kept pristine from intermediate artifacts generated from extensions such as ditaa. - Using a worker approach for running Asciidoctor instances. (#220) - Asciidoctor tasks can be configured to run AsciidoctorJ conversions in or out of process. Even when running in process AsciidoctorJ will be on an isolated classpath. - Tasks support parallel mode which will run each backend (or in the case or Epub, each output format) in a separate worker. This behevaiour can be turned off in which case only one Asciidoctor instance is used to run all conversions in sequence. - For cases where Gradle's idea of supplementing the classpath for workers do fail, there is a special `inProcess = JAVA_EXEC` that will run the conversion out of process, albeit less efficient than an out-of-process worker. - Due to Gradle API classpath leakage, builds with Gradle 4.0-4.2 will always run in `JAVA_EXEC` mode to prevent issues. - `AsciidoctorJPdf` plugin has been added which adds a single `asciidoctorPdf` task and configures it accordingly to PDF behaviour. It also sets a default version of the `asciidoctorj-pdf` dependency. - Backends and separateOutputDirs are configured via an outputOptions closure or action. - `AsciidoctorJEpub` plugin has been added which adds a single `asciidoctorEpub` task and configures it accordingly to EPUB behaviour. It also sets a default version of the `asciidoctorj-epub` dependency. - Epub output formats can be set via the `ebookFormats` method. - Additional Kindlegen plugin which is invoked by EPUB for when the format is KF8. The plugin will bootstrap kindlegen on all supported platforms. - Work around https://github.com/gradle/gradle/issues/3698 and Xerces API clash by running EPub conversions using `inProcess = JAVA_EXEC`. - All methods on the new task classes that take closures as parameters now also have equivalent Action parameters (#236). - All new tasks are supportd by integration tests and compatibility tests. (#180) EPUB3 + KF8 in one task (PendingFeature) - Both formats in one task is currently failing Backwards compatibility and migration: - Legacy code moved to 'org.asciidoctor.gradle.compat' package, but get AsciidoctorTask in same package for compatibility purpose. - Renamed AsciidoctorPlugin to AsciidoctorCompatibilityPlugin. - Legacy plugin cannot be used with other Asciidoctor plugins within the same project. - Existing 1.5.x deprecated methods has been removed from legacy AsciidoctorTask. --- README.adoc | 305 ++++- appveyor.yml | 1 + asciidoctor-gradle-base/build.gradle | 26 + .../gradle/base/AsciidoctorBasePlugin.groovy | 33 + .../asciidoctor/gradle/base/SafeMode.groovy | 79 ++ .../org.asciidoctor.base.properties | 17 + asciidoctor-gradle-jvm-epub/build.gradle | 48 + .../AsciidoctorEpubTaskFunctionalSpec.groovy | 126 ++ .../internal/FunctionalSpecification.groovy | 70 + .../epub3/src/docs/asciidoc/epub3.adoc | 10 + .../epub3/src/docs/asciidoc/images/fake.txt | 4 + .../epub3/src/docs/asciidoc/subdir/sample2.ad | 25 + .../jvm/epub/AsciidoctorEpubTask.groovy | 152 +++ .../jvm/epub/AsciidoctorJEpubPlugin.groovy | 48 + .../org.asciidoctor.jvm.epub.properties | 17 + asciidoctor-gradle-jvm/build.gradle | 44 +- .../compat-plugin-still-works/build.gradle | 17 + .../src/docs/asciidoc/test.adoc | 3 + .../gradleTest/complex-jvm-setup/build.gradle | 80 ++ .../src/docs/asciidoc/docinfo-footer.xml | 17 + .../src/docs/asciidoc/docinfo.xml | 17 + .../src/docs/asciidoc/images/fake.txt | 4 + .../docs/asciidoc/sample-docinfo-footer.xml | 17 + .../src/docs/asciidoc/sample-docinfo.xml | 17 + .../src/docs/asciidoc/sample.asciidoc | 26 + .../src/docs/asciidoc/subdir/_include.adoc | 1 + .../src/docs/asciidoc/subdir/sample2.ad | 26 + .../AsciidoctorFunctionalSpec.groovy | 82 +- .../internal/FunctionalSpecification.groovy | 55 + .../AsciidoctorPdfTaskFunctionalSpec.groovy | 80 ++ .../jvm/AsciidoctorTaskFunctionalSpec.groovy | 154 +++ .../jvm/ExtensionsFunctionalSpec.groovy | 192 +++ .../src/docs/asciidoc/subdir/sample2.ad | 3 +- .../AsciidoctorCompatibilityPlugin.groovy | 142 +++ .../asciidoctor/gradle/AsciidoctorTask.groovy | 182 ++- .../gradle/AsciidoctorUtils.groovy | 64 - .../{ => compat}/AsciidoctorBackend.groovy | 2 +- .../{ => compat}/AsciidoctorExtension.groovy | 2 +- .../gradle/{ => compat}/AsciidoctorProxy.java | 2 +- .../AsciidoctorProxyCacheKey.groovy | 2 +- .../{ => compat}/AsciidoctorProxyImpl.groovy | 2 +- .../{ => compat}/ResourceCopyProxy.java | 2 +- .../{ => compat}/ResourceCopyProxyImpl.groovy | 2 +- .../gradle/internal/AsciidoctorUtils.groovy | 54 + .../internal/ExecutorConfiguration.groovy | 57 + .../ExecutorConfigurationContainer.groovy | 21 + .../gradle/jvm/AbstractAsciidoctorTask.groovy | 767 +++++++++++ .../jvm/AsciidoctorExecutionException.groovy | 19 + .../gradle/jvm/AsciidoctorJBasePlugin.groovy | 35 + .../gradle/jvm/AsciidoctorJExtension.groovy | 573 +++++++++ .../gradle/jvm/AsciidoctorJPdfPlugin.groovy | 44 + .../gradle/jvm/AsciidoctorJPlugin.groovy | 37 + .../gradle/jvm/AsciidoctorPdfTask.groovy | 33 + .../gradle/jvm/AsciidoctorTask.groovy | 88 ++ .../gradle/jvm/OutputOptions.groovy | 59 + .../asciidoctor/gradle/jvm/ProcessMode.groovy | 27 + .../gradle/remote/AsciidoctorJExecuter.groovy | 147 +++ .../gradle/remote/AsciidoctorJavaExec.groovy | 97 ++ ...AsciidoctorRemoteExecutionException.groovy | 17 + .../gradle/remote/ExecutorBase.groovy | 70 + .../org.asciidoctor.jvm.base.properties | 2 +- .../org.asciidoctor.jvm.convert.properties | 2 +- .../org.asciidoctor.jvm.pdf.properties | 17 + .../gradle/AsciidoctorTaskSpec.groovy | 1125 ----------------- .../gradle/AsciidoctorUtilsSpec.groovy | 42 - .../{ => compat}/AsciidoctorPluginSpec.groovy | 53 +- ...AsciidoctorTaskInlineExtensionsSpec.groovy | 57 +- .../gradle/compat/AsciidoctorTaskSpec.groovy | 1028 +++++++++++++++ .../internal/AsciidoctorUtilsSpec.groovy | 47 + .../jvm/AsciidoctorJBasePluginSpec.groovy | 92 ++ .../gradle/jvm/AsciidoctorJPluginSpec.groovy | 41 + .../gradle/jvm/AsciidoctorTaskSpec.groovy | 403 ++++++ build.gradle | 33 +- gradle.properties | 10 +- gradle/asciidoctorj-versions.gradle | 24 + gradle/code-quality.gradle | 2 +- gradle/compatibility-tests.gradle | 13 + gradle/integration-tests.gradle | 55 +- kindlegen-gradle/build.gradle | 55 + .../kindlegen/KindleGenBasePlugin.groovy | 19 + .../kindlegen/KindleGenDownloader.groovy | 77 ++ .../kindlegen/KindleGenExtension.groovy | 76 ++ .../org.asciidoctor.kindlegen.base.properties | 1 + .../kindlegen/KindleGenFunctionalSpec.groovy | 55 + .../internal/FunctionalSpecification.groovy | 51 + settings.gradle | 3 + 86 files changed, 6239 insertions(+), 1487 deletions(-) create mode 100644 asciidoctor-gradle-base/build.gradle create mode 100644 asciidoctor-gradle-base/src/main/groovy/org/asciidoctor/gradle/base/AsciidoctorBasePlugin.groovy create mode 100644 asciidoctor-gradle-base/src/main/groovy/org/asciidoctor/gradle/base/SafeMode.groovy create mode 100644 asciidoctor-gradle-base/src/main/resources/META-INF/gradle-plugins/org.asciidoctor.base.properties create mode 100644 asciidoctor-gradle-jvm-epub/build.gradle create mode 100644 asciidoctor-gradle-jvm-epub/src/intTest/groovy/org/asciidoctor/gradle/jvm/epub/AsciidoctorEpubTaskFunctionalSpec.groovy create mode 100644 asciidoctor-gradle-jvm-epub/src/intTest/groovy/org/asciidoctor/gradle/jvm/epub/internal/FunctionalSpecification.groovy create mode 100644 asciidoctor-gradle-jvm-epub/src/intTest/projects/epub3/src/docs/asciidoc/epub3.adoc create mode 100644 asciidoctor-gradle-jvm-epub/src/intTest/projects/epub3/src/docs/asciidoc/images/fake.txt create mode 100644 asciidoctor-gradle-jvm-epub/src/intTest/projects/epub3/src/docs/asciidoc/subdir/sample2.ad create mode 100644 asciidoctor-gradle-jvm-epub/src/main/groovy/org/asciidoctor/gradle/jvm/epub/AsciidoctorEpubTask.groovy create mode 100644 asciidoctor-gradle-jvm-epub/src/main/groovy/org/asciidoctor/gradle/jvm/epub/AsciidoctorJEpubPlugin.groovy create mode 100644 asciidoctor-gradle-jvm-epub/src/main/resources/META-INF/gradle-plugins/org.asciidoctor.jvm.epub.properties create mode 100644 asciidoctor-gradle-jvm/src/gradleTest/compat-plugin-still-works/build.gradle create mode 100644 asciidoctor-gradle-jvm/src/gradleTest/compat-plugin-still-works/src/docs/asciidoc/test.adoc create mode 100644 asciidoctor-gradle-jvm/src/gradleTest/complex-jvm-setup/build.gradle create mode 100644 asciidoctor-gradle-jvm/src/gradleTest/complex-jvm-setup/src/docs/asciidoc/docinfo-footer.xml create mode 100644 asciidoctor-gradle-jvm/src/gradleTest/complex-jvm-setup/src/docs/asciidoc/docinfo.xml create mode 100644 asciidoctor-gradle-jvm/src/gradleTest/complex-jvm-setup/src/docs/asciidoc/images/fake.txt create mode 100644 asciidoctor-gradle-jvm/src/gradleTest/complex-jvm-setup/src/docs/asciidoc/sample-docinfo-footer.xml create mode 100644 asciidoctor-gradle-jvm/src/gradleTest/complex-jvm-setup/src/docs/asciidoc/sample-docinfo.xml create mode 100644 asciidoctor-gradle-jvm/src/gradleTest/complex-jvm-setup/src/docs/asciidoc/sample.asciidoc create mode 100644 asciidoctor-gradle-jvm/src/gradleTest/complex-jvm-setup/src/docs/asciidoc/subdir/_include.adoc create mode 100644 asciidoctor-gradle-jvm/src/gradleTest/complex-jvm-setup/src/docs/asciidoc/subdir/sample2.ad rename asciidoctor-gradle-jvm/src/intTest/groovy/org/asciidoctor/gradle/{ => compat}/AsciidoctorFunctionalSpec.groovy (62%) create mode 100644 asciidoctor-gradle-jvm/src/intTest/groovy/org/asciidoctor/gradle/internal/FunctionalSpecification.groovy create mode 100644 asciidoctor-gradle-jvm/src/intTest/groovy/org/asciidoctor/gradle/jvm/AsciidoctorPdfTaskFunctionalSpec.groovy create mode 100644 asciidoctor-gradle-jvm/src/intTest/groovy/org/asciidoctor/gradle/jvm/AsciidoctorTaskFunctionalSpec.groovy create mode 100644 asciidoctor-gradle-jvm/src/intTest/groovy/org/asciidoctor/gradle/jvm/ExtensionsFunctionalSpec.groovy create mode 100755 asciidoctor-gradle-jvm/src/main/groovy/org/asciidoctor/gradle/AsciidoctorCompatibilityPlugin.groovy delete mode 100644 asciidoctor-gradle-jvm/src/main/groovy/org/asciidoctor/gradle/AsciidoctorUtils.groovy rename asciidoctor-gradle-jvm/src/main/groovy/org/asciidoctor/gradle/{ => compat}/AsciidoctorBackend.groovy (97%) rename asciidoctor-gradle-jvm/src/main/groovy/org/asciidoctor/gradle/{ => compat}/AsciidoctorExtension.groovy (96%) rename asciidoctor-gradle-jvm/src/main/groovy/org/asciidoctor/gradle/{ => compat}/AsciidoctorProxy.java (96%) rename asciidoctor-gradle-jvm/src/main/groovy/org/asciidoctor/gradle/{ => compat}/AsciidoctorProxyCacheKey.groovy (95%) rename asciidoctor-gradle-jvm/src/main/groovy/org/asciidoctor/gradle/{ => compat}/AsciidoctorProxyImpl.groovy (97%) rename asciidoctor-gradle-jvm/src/main/groovy/org/asciidoctor/gradle/{ => compat}/ResourceCopyProxy.java (95%) rename asciidoctor-gradle-jvm/src/main/groovy/org/asciidoctor/gradle/{ => compat}/ResourceCopyProxyImpl.groovy (96%) create mode 100644 asciidoctor-gradle-jvm/src/main/groovy/org/asciidoctor/gradle/internal/AsciidoctorUtils.groovy create mode 100644 asciidoctor-gradle-jvm/src/main/groovy/org/asciidoctor/gradle/internal/ExecutorConfiguration.groovy create mode 100644 asciidoctor-gradle-jvm/src/main/groovy/org/asciidoctor/gradle/internal/ExecutorConfigurationContainer.groovy create mode 100644 asciidoctor-gradle-jvm/src/main/groovy/org/asciidoctor/gradle/jvm/AbstractAsciidoctorTask.groovy create mode 100644 asciidoctor-gradle-jvm/src/main/groovy/org/asciidoctor/gradle/jvm/AsciidoctorExecutionException.groovy create mode 100644 asciidoctor-gradle-jvm/src/main/groovy/org/asciidoctor/gradle/jvm/AsciidoctorJBasePlugin.groovy create mode 100644 asciidoctor-gradle-jvm/src/main/groovy/org/asciidoctor/gradle/jvm/AsciidoctorJExtension.groovy create mode 100644 asciidoctor-gradle-jvm/src/main/groovy/org/asciidoctor/gradle/jvm/AsciidoctorJPdfPlugin.groovy create mode 100644 asciidoctor-gradle-jvm/src/main/groovy/org/asciidoctor/gradle/jvm/AsciidoctorJPlugin.groovy create mode 100644 asciidoctor-gradle-jvm/src/main/groovy/org/asciidoctor/gradle/jvm/AsciidoctorPdfTask.groovy create mode 100755 asciidoctor-gradle-jvm/src/main/groovy/org/asciidoctor/gradle/jvm/AsciidoctorTask.groovy create mode 100644 asciidoctor-gradle-jvm/src/main/groovy/org/asciidoctor/gradle/jvm/OutputOptions.groovy create mode 100644 asciidoctor-gradle-jvm/src/main/groovy/org/asciidoctor/gradle/jvm/ProcessMode.groovy create mode 100644 asciidoctor-gradle-jvm/src/main/groovy/org/asciidoctor/gradle/remote/AsciidoctorJExecuter.groovy create mode 100644 asciidoctor-gradle-jvm/src/main/groovy/org/asciidoctor/gradle/remote/AsciidoctorJavaExec.groovy create mode 100644 asciidoctor-gradle-jvm/src/main/groovy/org/asciidoctor/gradle/remote/AsciidoctorRemoteExecutionException.groovy create mode 100644 asciidoctor-gradle-jvm/src/main/groovy/org/asciidoctor/gradle/remote/ExecutorBase.groovy create mode 100644 asciidoctor-gradle-jvm/src/main/resources/META-INF/gradle-plugins/org.asciidoctor.jvm.pdf.properties delete mode 100755 asciidoctor-gradle-jvm/src/test/groovy/org/asciidoctor/gradle/AsciidoctorTaskSpec.groovy delete mode 100755 asciidoctor-gradle-jvm/src/test/groovy/org/asciidoctor/gradle/AsciidoctorUtilsSpec.groovy rename asciidoctor-gradle-jvm/src/test/groovy/org/asciidoctor/gradle/{ => compat}/AsciidoctorPluginSpec.groovy (62%) rename asciidoctor-gradle-jvm/src/test/groovy/org/asciidoctor/gradle/{ => compat}/AsciidoctorTaskInlineExtensionsSpec.groovy (82%) create mode 100755 asciidoctor-gradle-jvm/src/test/groovy/org/asciidoctor/gradle/compat/AsciidoctorTaskSpec.groovy create mode 100755 asciidoctor-gradle-jvm/src/test/groovy/org/asciidoctor/gradle/internal/AsciidoctorUtilsSpec.groovy create mode 100644 asciidoctor-gradle-jvm/src/test/groovy/org/asciidoctor/gradle/jvm/AsciidoctorJBasePluginSpec.groovy create mode 100644 asciidoctor-gradle-jvm/src/test/groovy/org/asciidoctor/gradle/jvm/AsciidoctorJPluginSpec.groovy create mode 100755 asciidoctor-gradle-jvm/src/test/groovy/org/asciidoctor/gradle/jvm/AsciidoctorTaskSpec.groovy create mode 100644 gradle/asciidoctorj-versions.gradle create mode 100644 gradle/compatibility-tests.gradle create mode 100644 kindlegen-gradle/build.gradle create mode 100644 kindlegen-gradle/src/main/groovy/org/asciidoctor/gradle/kindlegen/KindleGenBasePlugin.groovy create mode 100644 kindlegen-gradle/src/main/groovy/org/asciidoctor/gradle/kindlegen/KindleGenDownloader.groovy create mode 100644 kindlegen-gradle/src/main/groovy/org/asciidoctor/gradle/kindlegen/KindleGenExtension.groovy create mode 100644 kindlegen-gradle/src/main/resources/META-INF/gradle-plugins/org.asciidoctor.kindlegen.base.properties create mode 100644 kindlegen-gradle/src/test/groovy/org/asciidoctor/gradle/kindlegen/KindleGenFunctionalSpec.groovy create mode 100644 kindlegen-gradle/src/test/groovy/org/asciidoctor/gradle/kindlegen/internal/FunctionalSpecification.groovy diff --git a/README.adoc b/README.adoc index 870c113ed..dc64ddf15 100644 --- a/README.adoc +++ b/README.adoc @@ -7,6 +7,7 @@ Andres Almiray :issues: https://github.com/asciidoctor/asciidoctor-maven-plugin/issues :gradle-url: http://gradle.org/ :asciidoctor-maven-plugin: https://github.com/asciidoctor/asciidoctor-maven-plugin +:kotlindsl: https://github.com/gradle/kotlin-dsl[Gradle Kotlin DSL] :lightguard: https://github.com/LightGuard :asciidoctorj: https://github.com/asciidoctor/asciidoctorj :lordofthejars: https://github.com/lordofthejars @@ -42,6 +43,8 @@ NOTE: This is a port of the {asciidoctor-maven-plugin}[Asciidoctor Maven Plugin] NOTE: The https://github.com/gradle/kotlin-dsl[Gradle Kotlin DSL] is not properly supported by this plugin yet but it can be used with the GKD. Examples and workarounds are described below. +NOTE: These colleciton of plugins requires at least Gradle 4.0 & JDK 7.0 to run. If you need prior Gradle support please use a plugin from the 1.5.x release series. + ifndef::env-site[] == Contributing @@ -51,26 +54,18 @@ endif::[] == Installation -Use the following snippet inside a Gradle build file: +To start you need to use one of the plugins from the following Gradle snipper [source,groovy] [subs=attributes+] .build.gradle ---- -buildscript { - repositories { - jcenter() - } - - dependencies { - classpath 'org.asciidoctor:{project-name}:{version-published}' - } +plugins { + id 'org.asciidoctor.jvm.convert' version '{version-published}' } - -apply plugin: 'org.asciidoctor.convert' ---- -Use the following snippet with the https://github.com/gradle/kotlin-dsl[Gradle Kotlin DSL]: +For the {kotlindsl} the format is slightly different: [source,kotlin] [subs=attributes+] @@ -81,7 +76,248 @@ plugins { } ---- -== Usage +There are also a of other plugins that could use depending on your context. + +[source,groovy] +[subs=attributes+] +.build.gradle +---- +plugins { + id 'org.asciidoctor.jvm.base' version '{version-published}' // <1> + id 'org.asciidoctor.js.convert' version '{version-published}' // <2> + id 'org.asciidoctor.js.base' version '{version-published}' // <3> + id 'org.asciidoctor.convert' version '{version-published}' // <4> + id 'org.asciidoctor.jvm.pdf' version '{version-published}' // <5> + id 'org.asciidoctor.jvm.epub' version '{version-published}' // <6> +} +---- +<1> Use AsciidoctorJ but without any predefined tasks. +<2> Use AsciidoctorJS instead of AsciidoctorJ with predefined tasks and conventions. +<3> Use AsciidoctorJS without any predefined tasks. +<4> If you upgraded from a 1.5.x version the plugin you might want to use this first. It is a compatibility plugin with the older versions. +<5> Adds a convention task `asciidoctorPdf` - implies `org.asciidoctor.jvm.base` +<6> Adds a convention task `asciidoctorEpub` - implies `org.asciidoctor.jvm.base` + +If you use {kotlindsl} just change the above format slightly as shown earlier. + +== AsciidoctorJ Base Plugin + +Adds an extension for configuring which version of AsciidoctorJ and Groovy Extension DSL which will be used. This is very much similar to the one used in older versions of the Asciidoctor gradle plugin, but now it also offers the ability to add the same functionality to a task thus allowing a task to override the default versions that has been set. + +[source,groovy] +---- +asciidoctorj { + version = '1.5.6' // <1> + groovyDslVersion = '1.0.0.Alpha2' // <2> + + options doctype: 'book', ruby: 'erubis' // <3> + + attributes toclevel : 2 // <4> +} +---- +<1> Set the default version of `asciidoctorj` for all Asciidoctor tasks in a project. +<2> Set the default version of the Groovy extensions DSL for all Asciidoctor tasks in a project. +<3> Add options for all Asciidoctor tasks +<4> Add attributes for all Asciidoctor tasks + +You can also override or extend select settings within a task using the same extension i.e. + +[source,groovy] +---- +asciidoctor { + asciidoctorj { + + setOptions = [ doctype: 'article' ] // <1> + + attributes toc : left // <2> + } +} +---- +<1> Override any global options +<2> Use these attributes in addition to the globally specified ones. + +The entities that can be set are: + +version:: Asciidoctorj version + If not specified a version will be used. +groovyDslVersion:: Version of Groovy Extensions DSL. + If not specified and no extensions are specified, Groovy DSL will not be used. However, if any extensions are added without setting an explicit version and default version will be used. +pdfVersion:: Asciidoctorj-pdf version. + If not specified asciidoctork-pdf will not be on the classpath. If you plan to use the PDF backend, then you need to set a version here. +epubVersion:: Asciidoctorj-epub version. + If not specified asciidoctork-epub will not be on the classpath. If you plan to use the EPUB backend, then you need to set a version here. +gemPaths:: one or more gem installation directories (separated by the system path separator). + Use `gemPaths` to append. Use `setGemPaths` or `gemPaths=['path1','path2']` to overwrite. + Use `asGemPath` to obtain a path string, separated by platform-specific separator. + Type: FileCollection, but any collection of objects convertible with `project.files` can be passed + Default: empty +requires:: a set of Ruby modules to be included. + Use `requires` to append. Use `setRequires` or `requires=['name']` to overwrite. + Type: Set. + Default: empty. + +=== AsciidoctorJ tasks + +All Asciidoctor tasks will have the following methods and properties: + +.Properties +[horizontal] +logDocuments:: Specifies if documents being processed should be logged on console. Type: boolean. Default: `false`. +inProcess:: Specifies whether Asciidoctor conversions should be run in-process or out-of-process. Default: `true` (in-process). +parallelMode:: Specifies whether each backend or other variant of a converting tasks huodl be run in parallel or sequential. + Sequential conversions might have less initialisation overhead, but may suffer from `gemPath` and extension pollution. Default: `true` (parallel). + +.Methods +[horizontal] +asciidoctorj:: a task extension which allows a task to extend of override global configuration for Asciidoctor tasks. + This allow extensive flexibility. Any thing that can be confiugred int he global `asciidoctorj` extension can also + be configured here. +sourceDir:: where the asciidoc sources are. + Use either `sourceDir path`, `setSourceDir path` or `sourceDir=path` + Type: File, but any object convertible with `project.file` can be passed. + Default: `src/docs/asciidoc`. +sources:: Specify which Asciidoctor source files to include as toplevel documents. It uses an + http://www.gradle.org/docs/current/javadoc/org/gradle/api/tasks/util/PatternSet.html[Ant-style PatternSet]. +secondarySources: Specify which source files should be monitor for change. These are typically files which are included by top-level files as well as doctype files. + Default: All files in sourceDir which matches `getDefaultSourceDocumentPattern()` as well as doctype files. +resources:: specify which additional files (image etc.) must be copied to output directory using a + http://www.gradle.org/docs/current/javadoc/org/gradle/api/file/CopySpec.html[CopySpec]. +outputDir:: where generated docs go. + Use either `outputDir path`, `setOutputDir path` or `outputDir=path` + Type: File, but any object convertible with `project.file` can be passed. + Default: `$buildDir/asciidoc`. +options:: a Map specifying different options that can be sent to Asciidoctor. + Use `options` to append, Use `setOptions` or `options=` to overwrite. + This is just a collection of shortcut methods to `asciidoctorj.options`. +attributes:: a Map specifying various document attributes that can be sent to Asciidoctor + Use `attributes` to append, Use `setAttributes` or `attributes=` to overwrite. + This is just a collection of shortcut methods to `asciidoctorj.attributes`. +configurations:: Specify additional configurations + These configurations will be added to the classpath when the task is executed. + +.Dealing with resource copying +copyAllResources:: Copy all resources to the output directory +copyNoResources:: Do not copy any resources to the output directory +copyResourcesOnlyIf:: Only copy resources if the backend matches the listed backend. +useIntermediateWorkDir:: Use an intermediate work directory for sources ances. + Some extensions such as `ditaa` will write content into the source directory. In order to keep + to kee[ the project source directory pristine an intermediate work directory can be used. All sources + and resources will be copied there prior the executing Asciidoctor. + +The base plugin has a conversion task type of `org.asciidoctor.gradle.jvm.AsciidoctorTask` which, in addition the aforementioned will also have the following properties and methods which are configured via an` outputOptions` closure or action: + +.Properties +[horizontal] +separateOutputDirs:: specifies whether each backend should use a separate subfolder under `outputDir`. + Default: `true` + +.Methods +[horizontal] +backends:: the backends to use. + Use `backends` to append. Use `setBackends` or `backends=[]` to overwrite + Type: Set, but any type can be converted to String can be used. + Default: [`html5`]. + +=== Defining Sources + +The plugin will search for sources under `sourceDir`. Sources may have any of the following extensions in +order to be discovered: + +* .adoc _(preferred)_ +* .asciidoc +* .ad +* .asc + +To select only certain files, use the `sources` method. This method takes a closure or an `Action` as an argument, which in turn configures an org.asciidoctor.gradle.jvm.epub.internal +http://www.gradle.org/docs/current/javadoc/org/gradle/api/tasks/util/PatternSet.html[PatternSet]. + +To specify a custom output folder, use the `outputDir` method. + +[source,groovy] +.build.gradle +---- +asciidoctor { + sourceDir file('docs') + sources { + include 'toplevel.adoc', 'another.adoc', 'third.adoc' + } + outputDir file('build/docs') +} +---- + +Paths defined in this PatternSet are resolved relative to the `sourceDir`. + +For the Gradle Kotlin DSL a workaround is needed:footnoteref:[kotlin-delegate,The method delegates to a type that Kotlin cannot infer that from the byte-code. The function `delegateClosureOf()` from the GKD is used to provide the information to Kotlin.] + +[source,kotlin] +.build.gradle.kts +---- +tasks { + "asciidoctor"(AsciidoctorTask::class) { + sourceDir = file("docs") + sources(delegateClosureOf { + include("toplevel.adoc", "another.adoc", "third.adoc") + }) + outputDir = file("build/docs") + } +} +---- + +== The New AsciidoctorJ Plugin + +When applying `org.asciidoctor.jvm.convert` it creates a single task of type `org.asciidoctor.gradle.jvm.AsciidoctorTask` called `asciidoctor`. + +By convention it sets the +* `sourceDir` to `src/docs/asciidoc` +* `outputDir` to `${buildDir}/docs/asciidoc` + +== The AsciidoctorPdf Plugin + +When applying `org.asciidoctor.jvm.pdf` it creates a single task of type `org.asciidoctor.gradle.jvm.AsciidoctorPdfTask` which is then configured to: + +* Output source to "${buildDir}/docs/asciidocPdf" +* Not to copy any resources to the output directory +* It will set also a default version for `asciidoctorj-pdf` artifact. To override set `asciidoctorj.pdfVersion` or `asciidoctorPdf.asciidoctorj.pdfVersion`. + +== The AsciidoctorEpub Plugin + +When applying `org.asciidoctor.jvm.epub` it creates a single task of type `org.asciidoctor.gradle.jvm.epub.AsciidoctorEpubTask` which is then configured to: + +* Output source to "${buildDir}/docs/asciidocEpub" +* Not to copy any resources to the output directory +* It will set also a default version for `asciidoctorj-epub` artifact. To override set `asciidoctorj.epubVersion` or `asciidoctorPdf.asciidoctorj.epubVersion`. + +The `AsciidoctorEpubTask` task type has the following additional methods: + +[horizontal] +ebookFormats:: The epub formats to generate. + Sepcify one of more strings. Anything that is supported by the Asciidoctor EPUB backend can be used. Constants `EPUB3` and `KF8` are available for convenience. To override any previous set fomrats use `setEbookFormats`. To add to the existing list use `eBookFormats`. + +== Upgrading From Older Versions of Asciidoctor + +NOTE: To help with migration the old plugin will print a number of messages to help with a conversion. The amount of text is controlled via `--warnings-mode` in Gradle 4.5+. For Gradle 4.0-4.4 use `--info` to get the full list of recommendations. Use of `--warnings-mode=none` (Gradle 4.5+) or `--quiet` (Gradle 4.0-4.4) will produce migration information. + +Firstly replace plugin `org.asciidoctor.convert` with `org.asciidoctor.jvm.convert`. + +If you have more than one Asciidoctor task, decide which `options`, `attributes` and `requires` should go in the `asciidoctorj` global project extension block and which should be customised within the tasks `asciidoctor` extension block. + +Gradle injected a number of attributes into the build. THese names have now been changed to indicate that they are injected: + +[cols="3*"] +|=== +| *Old name* | *New name* | *Usage* +| `projectdir` | `gradle-projectdir` | The Grsdle project directory which is running the Asciidoctor task. +| `rootdir` | `gradle-rootdir` | The rootproject directory in a multi-project build. +| `project-name` | `gradle-project-naame` | The name of the Gradle project which contains the specifics Asciidoctor task(s). +| `project-group` | `gradle-project-group` | The project/artifact group if it is defined. +| `project-version` | `gradle-project-version` | THe project version if it is defined. +|=== + + +== Asciidoctor Compatibility Plugin + +In order to help people upgrade a compatiblity plugin has been kept which mostly behaves in the same way as the AsciidoctorJ plugins in the 1.5.x series of releases. + The plugin adds a new task named *asciidoctor*. You can configure this task using the following configuration properties and methods. @@ -126,7 +362,9 @@ options:: a Map specifying different options that can be sent to Asciidoctor. attributes:: a Map specifying various document attributes that can be sent to Asciidoctor Use `attributes` to append, Use `setAttributes` or `attributes=` to overwrite. +//// To see examples of many of these configuration options used in practice, refer to the http://asciidoctor.github.io/asciidoctor-gradle-examples[Asciidoctor Gradle Examples] project. +//// === Defining Sources @@ -138,7 +376,7 @@ order to be discovered: * .ad * .asc -To select only certain files, use the `sources` method. This method takes a closure as an argument, which in turn configures an internal +To select only certain files, use the `sources` method. This method takes a closure as an argument, which in turn configures an org.asciidoctor.gradle.jvm.epub.internal http://www.gradle.org/docs/current/javadoc/org/gradle/api/tasks/util/PatternSet.html[PatternSet]. To specify a custom output folder, use the `outputDir` method. @@ -176,7 +414,7 @@ tasks { === Processing Auxiliary Files Some backends require that additional files be copied across. The most common example are images for HTML backends. For -this the `resources` method is used. It is provided with a closure that configures an internal +this the `resources` method is used. It is provided with a closure that configures an org.asciidoctor.gradle.jvm.epub.internal http://www.gradle.org/docs/current/javadoc/org/gradle/api/file/CopySpec.html[CopySpec] [source,groovy] @@ -361,6 +599,24 @@ asciidoctorj { } ---- +== Kindlegen plugin + +Producing KF* formats via the EPUB extension requires `kindlegen` to be installed. This plugin provides the capability of bootstrapping `kindlegen` on Windows, MAx & Linux without the user having to do anything. + +There is a base plugin `org.asciidoctor.kindlegen.base` which just provides a `kindlegen` extension. In order to use it +you will need to agree to the Amazon terms of usage. To confirm this you need to configure + +[source,groovy] +---- +kindlegen { + agreeTotermsOfUse = true +} +---- + +If you do not them the plugin will refuse to botostrap `kindlegen`. + +NOTE: The base plugin is automatically applied by the EPUB plugin. If you only produce EPUB3 formats with the EPUB plugin you do not have to agree to usage of `kindlegen`. + ifndef::env-site[] == Development @@ -516,15 +772,15 @@ backend:: the backend to use. Use `backends` instead. === Behavior -* The default value for `sourceDir` has changed from `src/asciidoc` to `src/docs/asciidoc`. +* The default value for `sourceDir` is `src/docs/asciidoc`. * Files specified in `sourceDocumentNames` must be relative to `sourceDir` and fully contained in `sourceDir`, in other words, -it's no longer possible to process documents placed outside of the project's sources. Attempts will be made to convert absolute paths +it's not possible to process documents placed outside of the project's sources. Attempts will be made to convert absolute paths to relative paths but conversion will not be guaranteed. Do not pass FileCollections as they will not convert correctly. -* Source files that are not reachable from `sourceDir`, will no longer cause a build exception, they will just be silently ignored. +* Source files that are not reachable from `sourceDir`, will just be silently ignored. * For backwards compatibility with older version, embedding `attributes` within `options` are still allowed, including legacy forms. -* Non-source files are no longer automatically copied, unless they are in the `images` folder and `resources` was never +* Non-source files are not copied, unless they are in the `images` folder and `resources` was never called. -* Each backend will now write to a separate subfolder under `outputDir`. To have the old behaviour use +* Each backend will now write to a separate subfolder under `outputDir`. To override this behaviour use `separateOutputDirs = false`. === Options & Attributes @@ -585,12 +841,3 @@ asciidoctor.doLast { } ---- -=== Force processing - -If Gradle detects that there were no changes, asciidoctor processing will be skipped as `UP-TO-DATE`. To force asciidoctor processing even if there were no changes: - -[source,groovy] -.build.gradle ----- -asciidoctor.outputs.upToDateWhen { false } ----- diff --git a/appveyor.yml b/appveyor.yml index 90c4e25df..c2160797f 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -16,6 +16,7 @@ build_script: - gradlew.bat -u -i clean assemble test_script: - gradlew.bat -u -i -S check + - gradlew.bat --stop cache: - .gradle - C:\Users\appveyor\.gradle diff --git a/asciidoctor-gradle-base/build.gradle b/asciidoctor-gradle-base/build.gradle new file mode 100644 index 000000000..d7dd9c039 --- /dev/null +++ b/asciidoctor-gradle-base/build.gradle @@ -0,0 +1,26 @@ + +/* + * Copyright 2013-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +gradlePlugin { + plugins { + asciidoctorBasePlugin { + id = 'org.asciidoctor.org.asciidoctor.base' + implementationClass = 'org.asciidoctor.gradle.base.AsciidoctorBasePlugin' + description = 'Base plugin for AsciidoctorJ & AscidoctorJS plugins' + } + } +} diff --git a/asciidoctor-gradle-base/src/main/groovy/org/asciidoctor/gradle/base/AsciidoctorBasePlugin.groovy b/asciidoctor-gradle-base/src/main/groovy/org/asciidoctor/gradle/base/AsciidoctorBasePlugin.groovy new file mode 100644 index 000000000..82044bfac --- /dev/null +++ b/asciidoctor-gradle-base/src/main/groovy/org/asciidoctor/gradle/base/AsciidoctorBasePlugin.groovy @@ -0,0 +1,33 @@ +/* + * Copyright 2013-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.asciidoctor.gradle.base + +import groovy.transform.CompileStatic +import org.gradle.api.Plugin +import org.gradle.api.Project + +/** + * @since 1.6.0 + */ +@CompileStatic +class AsciidoctorBasePlugin implements Plugin { + + void apply(Project project) { + project.with { + apply plugin : 'base' + } + } +} diff --git a/asciidoctor-gradle-base/src/main/groovy/org/asciidoctor/gradle/base/SafeMode.groovy b/asciidoctor-gradle-base/src/main/groovy/org/asciidoctor/gradle/base/SafeMode.groovy new file mode 100644 index 000000000..7425ddc36 --- /dev/null +++ b/asciidoctor-gradle-base/src/main/groovy/org/asciidoctor/gradle/base/SafeMode.groovy @@ -0,0 +1,79 @@ +/* + * Copyright 2013-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.asciidoctor.gradle.base + +import groovy.transform.CompileStatic + +/** Describes Asciidoc sefty modes. + * + * (This has been ported to the plugin from the AscidoctorJ code base). + * + * @author Alex Soto + * @author Schalk W. Cronjé + * + */ +@CompileStatic +enum SafeMode { + + /** + * A safe mode level that disables any of the security features enforced by Asciidoctor (Ruby is still subject to + * its own restrictions). + */ + UNSAFE(0), + /** + * A safe mode level that closely parallels safe mode in AsciiDoc. This value prevents access to files which reside + * outside of the parent directory of the source file and disables any macro other than the include::[] macro. + */ + SAFE(1), + /** + * A safe mode level that disallows the document from setting attributes that would affect the rendering of the + * document, in addition to all the security features of SafeMode::SAFE. For instance, this level disallows changing + * the backend or the source-highlighter using an attribute defined in the source document. This is the most + * fundamental level of security for server-side deployments (hence the name). + */ + SERVER(10), + /** + * A safe mode level that disallows the document from attempting to read files from the file system and including + * the contents of them into the document, in additional to all the security features of SafeMode::SERVER. For + * instance, this level disallows use of the include::[] macro and the embedding of binary content (data uri), + * stylesheets and JavaScripts referenced by the document.(Asciidoctor and trusted extensions may still be allowed + * to embed trusted content into the document). + * + * Since Asciidoctor is aiming for wide adoption, this level is the default and is recommended for server-side + * deployments. + */ + SECURE(20) + + final private int level + + private SafeMode(int level) { + this.level = level + } + + int getLevel() { + this.level + } + + static final SafeMode safeMode(int level) { + switch(level) { + case 0: return UNSAFE + case 1: return SAFE + case 10: return SERVER + default: return SECURE + } + } + +} diff --git a/asciidoctor-gradle-base/src/main/resources/META-INF/gradle-plugins/org.asciidoctor.base.properties b/asciidoctor-gradle-base/src/main/resources/META-INF/gradle-plugins/org.asciidoctor.base.properties new file mode 100644 index 000000000..e52fb2011 --- /dev/null +++ b/asciidoctor-gradle-base/src/main/resources/META-INF/gradle-plugins/org.asciidoctor.base.properties @@ -0,0 +1,17 @@ +# +# Copyright 2013-2018 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +implementation-class=org.asciidoctor.gradle.base.AsciidoctorBasePlugin \ No newline at end of file diff --git a/asciidoctor-gradle-jvm-epub/build.gradle b/asciidoctor-gradle-jvm-epub/build.gradle new file mode 100644 index 000000000..496665ad0 --- /dev/null +++ b/asciidoctor-gradle-jvm-epub/build.gradle @@ -0,0 +1,48 @@ +apply from: "${rootProject.projectDir}/gradle/asciidoctorj-versions.gradle" + +ext { + jrubyFileContent = file("src/intTest/groovy/org/asciidoctor/gradle/jvm/epub/AsciidoctorEpubTaskFunctionalSpec.groovy").readLines() + + jrubyTestVersion = (jrubyFileContent.grep { + it =~ /final\s+static\s+String\s+JRUBY_TEST_VERSION\s+=/ + })[0].replaceFirst(~/^(.+?["'])/, '').replaceFirst(~/(["'].*)$/, '') + +} + +configurations { + kindlegen +} + +dependencies { + compile project(':asciidoctor-gradle-jvm') + compile project(':kindlegen-gradle') + + intTestOfflineRepo "org.asciidoctor:asciidoctorj:${compileOnlyAsciidoctorJVersion}" + intTestOfflineRepo "org.asciidoctor:asciidoctorj-epub3:${downloadOnlyEpubVersion}" + intTestOfflineRepo "org.jruby:jruby-complete:${jrubyTestVersion}", { + transitive = false + } + kindlegen project( path : ':kindlegen-gradle', configuration : 'kindlegen') +} + +intTest { + ext { + findKindleGenDir = { confFiles -> + confFiles.find { + it.name.toLowerCase().startsWith('kindlegen') + }.parentFile.absoluteFile + } + } + systemProperties 'org.asciidoctor.gradle.kindlegen.uri' : "${findKindleGenDir(configurations.kindlegen.files).toURI()}" + systemProperties TEST_PROJECTS_DIR : file('src/intTest/projects') +} + +gradlePlugin { + plugins { + AsciidoctorEpubPlugin { + id = 'org.asciidoctor.org.asciidoctor.jvm.epub' + implementationClass = 'org.asciidoctor.gradle.jvm.epub.AsciidoctorJEpubPlugin' + description = "Converts Asciidoctor documents to EPUB3/KF8 using AsciidoctorJ${pluginExtraText}" + } + } +} diff --git a/asciidoctor-gradle-jvm-epub/src/intTest/groovy/org/asciidoctor/gradle/jvm/epub/AsciidoctorEpubTaskFunctionalSpec.groovy b/asciidoctor-gradle-jvm-epub/src/intTest/groovy/org/asciidoctor/gradle/jvm/epub/AsciidoctorEpubTaskFunctionalSpec.groovy new file mode 100644 index 000000000..fc5666d54 --- /dev/null +++ b/asciidoctor-gradle-jvm-epub/src/intTest/groovy/org/asciidoctor/gradle/jvm/epub/AsciidoctorEpubTaskFunctionalSpec.groovy @@ -0,0 +1,126 @@ +/* + * Copyright 2013-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.asciidoctor.gradle.jvm.epub + +import org.asciidoctor.gradle.jvm.epub.internal.FunctionalSpecification +import org.gradle.testkit.runner.BuildResult +import spock.lang.PendingFeature +import spock.lang.Unroll + +class AsciidoctorEpubTaskFunctionalSpec extends FunctionalSpecification { + + // This line is read by the build script. + final static String JRUBY_TEST_VERSION = '9.1.17.0' + + void setup() { + createTestProject() + } + + @Unroll + @SuppressWarnings('MethodName') + void 'Run a EPUB generator with format #format (only in JAVA_EXEC mode)'() { + given: + getBuildFile(""" + +asciidoctorEpub { + sourceDir 'src/docs/asciidoc' + ebookFormats ${format} + + kindlegen { + agreeToTermsOfUse = true + } + + asciidoctorj { + jrubyVersion = '${JRUBY_TEST_VERSION}' + } + + sources { + include 'epub3.adoc' + } + + attributes includedir : {getSourceDir()} +} +""") + when: + BuildResult result = getGradleRunner(['asciidoctorEpub', '-s', '-i']).build() + + then: + verifyAll { + new File(testProjectDir.root, "build/docs/asciidocEpub/epub3.${ext}").exists() + !result.output.contains('include file not found:') + } + + where: + format | ext + 'EPUB3' | 'epub' + 'KF8' | 'mobi' + } + + @PendingFeature + @SuppressWarnings('MethodName') + @Unroll + void 'Run a EPUB generator with multiple formats in order #formatOrder (only in JAVA_EXEC mode)'() { + given: + getBuildFile(""" + +asciidoctorEpub { + sourceDir 'src/docs/asciidoc' + ebookFormats ${formatOrder} + + kindlegen { + agreeToTermsOfUse = true + } + + asciidoctorj { + jrubyVersion = '${JRUBY_TEST_VERSION}' + } + + sources { + include 'epub3.adoc' + } + + attributes includedir : {getSourceDir()} +} +""") + when: + BuildResult result = getGradleRunner(['asciidoctorEpub', '-s', '-i']).build() + + then: + verifyAll { + new File(testProjectDir.root, 'build/docs/asciidocEpub/epub3.mobi').exists() + new File(testProjectDir.root, 'build/docs/asciidocEpub/epub3.epub').exists() + !result.output.contains('include file not found:') + } + + where: + formatOrder << [ 'EPUB3, KF8', 'KF8, EPUB3'] + } + + File getBuildFile(String extraContent) { + File buildFile = testProjectDir.newFile('build.gradle') + buildFile << """ +plugins { + id 'org.asciidoctor.jvm.epub' +} + +${offlineRepositories} + +${extraContent} +""" + buildFile + } + +} \ No newline at end of file diff --git a/asciidoctor-gradle-jvm-epub/src/intTest/groovy/org/asciidoctor/gradle/jvm/epub/internal/FunctionalSpecification.groovy b/asciidoctor-gradle-jvm-epub/src/intTest/groovy/org/asciidoctor/gradle/jvm/epub/internal/FunctionalSpecification.groovy new file mode 100644 index 000000000..8fd6f6611 --- /dev/null +++ b/asciidoctor-gradle-jvm-epub/src/intTest/groovy/org/asciidoctor/gradle/jvm/epub/internal/FunctionalSpecification.groovy @@ -0,0 +1,70 @@ +/* + * Copyright 2013-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.asciidoctor.gradle.jvm.epub.internal + +import org.apache.commons.io.FileUtils +import org.gradle.testkit.runner.GradleRunner +import org.junit.Rule +import org.junit.rules.TemporaryFolder +import spock.lang.Shared +import spock.lang.Specification + +class FunctionalSpecification extends Specification { + static + final String TEST_PROJECTS_DIR = System.getProperty('TEST_PROJECTS_DIR') ?: './asciidoctor-gradle-jvm-epub/src/intTest/projects' + static + final String TEST_REPO_DIR = System.getProperty('OFFLINE_REPO') ?: './asciidoctor-gradle-jvm-epub/build/intTestOfflineRepo' + + @Rule + final TemporaryFolder testProjectDir = new TemporaryFolder() + + @Shared + List pluginClasspath + + def setupSpec() { + def pluginClasspathResource = getClass().classLoader.getResource('plugin-classpath.txt') + if (pluginClasspathResource == null) { + pluginClasspathResource = new File('./asciidoctor-gradle-jvm-epub/build/createClasspathManifest/plugin-classpath.txt') + } + if (pluginClasspathResource == null) { + throw new IllegalStateException('Did not find plugin classpath resource, run `intTestClasses` build task.') + } + + pluginClasspath = pluginClasspathResource.readLines().collect { new File(it) } + } + + GradleRunner getGradleRunner(List taskNames = ['asciidoctor']) { + GradleRunner.create() + .withProjectDir(testProjectDir.root) + .withArguments(taskNames) + .withPluginClasspath(pluginClasspath) + .forwardOutput() + .withDebug(true) + } + + @SuppressWarnings(['FactoryMethodName','BuilderMethodWithSideEffects']) + void createTestProject(String docGroup = 'epub3') { + FileUtils.copyDirectory(new File(TEST_PROJECTS_DIR, docGroup), testProjectDir.root) + } + + String getOfflineRepositories() { + File repo = new File(TEST_REPO_DIR, 'repositories.gradle') + if (!repo.exists()) { + throw new FileNotFoundException("${repo} not found. Run 'cacheOfflineBinaries' build task") + } + "apply from: '${repo.absolutePath}'" + } +} \ No newline at end of file diff --git a/asciidoctor-gradle-jvm-epub/src/intTest/projects/epub3/src/docs/asciidoc/epub3.adoc b/asciidoctor-gradle-jvm-epub/src/intTest/projects/epub3/src/docs/asciidoc/epub3.adoc new file mode 100644 index 000000000..c872286cd --- /dev/null +++ b/asciidoctor-gradle-jvm-epub/src/intTest/projects/epub3/src/docs/asciidoc/epub3.adoc @@ -0,0 +1,10 @@ += Asciidoctor EPUB3: Sample Book +Author Name +v1.0, 2014-04-15 +:doctype: book +:producer: Asciidoctor +:keywords: Asciidoctor, samples, e-book, EPUB3, KF8, MOBI, Asciidoctor.js +:copyright: CC-BY-SA 3.0 +:imagesdir: images + +include::{includedir}/subdir/sample2.ad[] diff --git a/asciidoctor-gradle-jvm-epub/src/intTest/projects/epub3/src/docs/asciidoc/images/fake.txt b/asciidoctor-gradle-jvm-epub/src/intTest/projects/epub3/src/docs/asciidoc/images/fake.txt new file mode 100644 index 000000000..a8cc3a92e --- /dev/null +++ b/asciidoctor-gradle-jvm-epub/src/intTest/projects/epub3/src/docs/asciidoc/images/fake.txt @@ -0,0 +1,4 @@ + +===================== +Not a real image file +===================== \ No newline at end of file diff --git a/asciidoctor-gradle-jvm-epub/src/intTest/projects/epub3/src/docs/asciidoc/subdir/sample2.ad b/asciidoctor-gradle-jvm-epub/src/intTest/projects/epub3/src/docs/asciidoc/subdir/sample2.ad new file mode 100644 index 000000000..1e8f9d7f6 --- /dev/null +++ b/asciidoctor-gradle-jvm-epub/src/intTest/projects/epub3/src/docs/asciidoc/subdir/sample2.ad @@ -0,0 +1,25 @@ += Document Title +Doc Writer +:idprefix: id_ + +Preamble paragraph. + +NOTE: This is test, only a test. + +== Section A + +*Section A* paragraph. + +=== Section A Subsection + +*Section A* 'subsection' paragraph. + +== Section B + +*Section B* paragraph. + +.Section B list +* Item 1 +* Item 2 +* Item 3 + diff --git a/asciidoctor-gradle-jvm-epub/src/main/groovy/org/asciidoctor/gradle/jvm/epub/AsciidoctorEpubTask.groovy b/asciidoctor-gradle-jvm-epub/src/main/groovy/org/asciidoctor/gradle/jvm/epub/AsciidoctorEpubTask.groovy new file mode 100644 index 000000000..2be05b0cb --- /dev/null +++ b/asciidoctor-gradle-jvm-epub/src/main/groovy/org/asciidoctor/gradle/jvm/epub/AsciidoctorEpubTask.groovy @@ -0,0 +1,152 @@ +/* + * Copyright 2013-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.asciidoctor.gradle.jvm.epub + +import groovy.transform.CompileStatic +import org.asciidoctor.gradle.internal.ExecutorConfiguration +import org.asciidoctor.gradle.jvm.AbstractAsciidoctorTask +import org.asciidoctor.gradle.jvm.AsciidoctorExecutionException +import org.asciidoctor.gradle.kindlegen.KindleGenExtension +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.util.PatternSet +import org.gradle.process.JavaForkOptions +import org.gradle.workers.WorkerExecutor + +import javax.inject.Inject + +/** + * @since 1.6.0 + * @author Schalk W. Cronjé + */ +@CompileStatic +class AsciidoctorEpubTask extends AbstractAsciidoctorTask { + + static final String KF8 = 'kf8' + static final String EPUB3 = 'epub3' + + private static final String BACKEND = 'epub3' + private static final String EBOOK_FORMAT_ATTR = 'ebook-format' + + private final Set ebookFormats = [] + private final KindleGenExtension kindleGenExtension + + @Inject + AsciidoctorEpubTask(WorkerExecutor we) { + super(we) + + configuredOutputOptions.backends = [BACKEND] + copyNoResources() + inProcess = org.asciidoctor.gradle.jvm.AbstractAsciidoctorTask.JAVA_EXEC + kindleGenExtension = project.extensions.getByType(KindleGenExtension) + } + + /** The eBook formats that needs to be generated. + * + * @return eBook formats that needs to be generated. + */ + @Input + Set getEbookFormats() { + this.ebookFormats + } + + /** Resets the list of formats that needs to be generated + * + * Any format supported by asciidoctorj-epub can be listed here. + * This method will overide any {@code ebook-format} that is set via {@link #attributes}. + * + * If you specificy {@link #KF8}, the task will attempt to download a version of KindleGen + * ({@link https://www.amazon.com/gp/feature.html?docId=1000765211}) for Windows, Mac and Linux. + * + * @param formats List of formats. The plugin does not verify whether the eBook format + * is valid. + */ + void setEbookFormats(Iterable formats) { + this.ebookFormats.clear() + this.ebookFormats.addAll(formats.collect { String it -> it.toLowerCase() } as Set) + } + + /** Adds aditional eBook formats + * + * @param formats List of formats. The plugin does not verify whether the eBook format + * is valid. + */ + @SuppressWarnings('ConfusingMethodName') + void ebookFormats(String... formats) { + this.ebookFormats.addAll(formats*.toLowerCase()) + } + + /** The default pattern set for secondary sources. + * + * @return {@link #getDefaultSourceDocumentPattern} + `*docinfo*`. + */ + @Override + protected PatternSet getDefaultSecondarySourceDocumentPattern() { + PatternSet ps = defaultSourceDocumentPattern + ps + } + + /** Configure Java fork options prior to execution + * + * @param pfo Fork options to be configured. + */ + @Override + protected void configureForkOptions(JavaForkOptions pfo) { + super.configureForkOptions(pfo) + + if (this.ebookFormats.contains(KF8)) { + pfo.environment['KINDLEGEN'] = kindleGenExtension.resolvableExecutable.executable.absolutePath + } + } + +/** Returns all of the executor configurations for this task + * + * @return Executor configurations + */ + @Override + protected Map getExecutorConfigurations(File workingSourceDir, Set sourceFiles) { + Map executorConfigurations = super.getExecutorConfigurations(workingSourceDir, sourceFiles) + + final Closure backendName = { String fmt -> + fmt != this.EPUB3 ? "epub3/${fmt}".toString() : this.BACKEND + } + + final String ebookAttr = EBOOK_FORMAT_ATTR + if (this.ebookFormats.empty) { + throw new AsciidoctorExecutionException("No eBook format specified for task ${name}") + } else if (this.ebookFormats.size() == 1) { + executorConfigurations.collectEntries { configName, config -> + config.attributes[ebookAttr] = this.ebookFormats.first() + [backendName(this.ebookFormats.first()), config] + } as Map + } else { + Map newConfigurations = [:] + String firstFormat = this.ebookFormats.first() + executorConfigurations.each { configName, config -> + this.ebookFormats.each { fmt -> + if (fmt == firstFormat) { + config.attributes[ebookAttr] = fmt + newConfigurations.put(backendName(fmt), config) + } else { + ExecutorConfiguration newConfig = (ExecutorConfiguration) config.clone() + newConfig.attributes[ebookAttr] = fmt + newConfigurations.put(backendName(fmt), newConfig) + } + } + } + newConfigurations + } + } +} diff --git a/asciidoctor-gradle-jvm-epub/src/main/groovy/org/asciidoctor/gradle/jvm/epub/AsciidoctorJEpubPlugin.groovy b/asciidoctor-gradle-jvm-epub/src/main/groovy/org/asciidoctor/gradle/jvm/epub/AsciidoctorJEpubPlugin.groovy new file mode 100644 index 000000000..d72c60ec3 --- /dev/null +++ b/asciidoctor-gradle-jvm-epub/src/main/groovy/org/asciidoctor/gradle/jvm/epub/AsciidoctorJEpubPlugin.groovy @@ -0,0 +1,48 @@ +/* + * Copyright 2013-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.asciidoctor.gradle.jvm.epub + +import groovy.transform.CompileStatic +import org.asciidoctor.gradle.jvm.AsciidoctorJExtension +import org.gradle.api.Plugin +import org.gradle.api.Project + + +/** Provides additional conventions for building EPUBs. + * + *
    + *
  • Creates a task called {@code asciidoctorEpub}. + *
  • Sets a default version for asciidoctor-epub. + *
+ * + * @since 1.6.0 + * @author Schalk W. Cronjé + */ +@CompileStatic +class AsciidoctorJEpubPlugin implements Plugin { + + void apply(Project project) { + project.with { + apply plugin : 'org.asciidoctor.jvm.base' + apply plugin : 'org.asciidoctor.kindlegen.base' + + AsciidoctorEpubTask task = tasks.create('asciidoctorEpub', AsciidoctorEpubTask) + task.outputDir = { "${project.buildDir}/docs/asciidocEpub"} + extensions.getByType(AsciidoctorJExtension).epubVersion = AsciidoctorJExtension.DEFAULT_EPUB_VERSION + + } + } +} diff --git a/asciidoctor-gradle-jvm-epub/src/main/resources/META-INF/gradle-plugins/org.asciidoctor.jvm.epub.properties b/asciidoctor-gradle-jvm-epub/src/main/resources/META-INF/gradle-plugins/org.asciidoctor.jvm.epub.properties new file mode 100644 index 000000000..972f14cb3 --- /dev/null +++ b/asciidoctor-gradle-jvm-epub/src/main/resources/META-INF/gradle-plugins/org.asciidoctor.jvm.epub.properties @@ -0,0 +1,17 @@ +# +# Copyright 2013-2018 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +implementation-class=org.asciidoctor.gradle.jvm.epub.AsciidoctorJEpubPlugin \ No newline at end of file diff --git a/asciidoctor-gradle-jvm/build.gradle b/asciidoctor-gradle-jvm/build.gradle index b6b09953d..b9e3c4005 100644 --- a/asciidoctor-gradle-jvm/build.gradle +++ b/asciidoctor-gradle-jvm/build.gradle @@ -15,19 +15,55 @@ * limitations under the License. */ +apply from: "${rootProject.projectDir}/gradle/asciidoctorj-versions.gradle" + +configurations { + intTestOfflineRepo.extendsFrom compileOnly +} + +dependencies { + compileOnly "org.asciidoctor:asciidoctorj:${compileOnlyAsciidoctorJVersion}" + compileOnly "org.asciidoctor:asciidoctorj-groovy-dsl:${compileOnlyGroovyDslVersion}", { + exclude module : 'groovy-all' + } + compile project(':asciidoctor-gradle-base') + + intTestOfflineRepo "org.asciidoctor:asciidoctorj-pdf:${downloadOnlyPdfVersion}" + intTestOfflineRepo "org.asciidoctor:asciidoctorj-epub3:${downloadOnlyEpubVersion}" + + + // This is here to for extension testing on the ocmpatibility plugin. It will be deleted + // when compatibiloity mode is removed. + testRuntimeOnly "org.asciidoctor:asciidoctorj-groovy-dsl:${compileOnlyGroovyDslVersion}", { + exclude module : 'groovy-all' + } +} + gradlePlugin { plugins { asciidoctorBasePlugin { - id = 'org.asciidoctor.jvm.base' - implementationClass = 'org.asciidoctor.gradle.AsciidoctorBasePlugin' + id = 'org.asciidoctor.org.asciidoctor.jvm.base' + implementationClass = 'org.asciidoctor.gradle.jvm.AsciidoctorJBasePlugin' + description = "Base plugin for running AsciidoctorJ${pluginExtraText}" } asciidoctorCompatibilityPlugin { id = 'org.asciidoctor.convert' implementationClass = 'org.asciidoctor.gradle.AsciidoctorCompatibilityPlugin' + description = "Compatibility AsciidoctorJ plugin for those upgrading from 1.5.x${pluginExtraText}" } asciidoctorPlugin { - id = 'org.asciidoctor.jvm.convert' - implementationClass = 'org.asciidoctor.gradle.AsciidoctorPlugin' + id = 'org.asciidoctor.org.asciidoctor.jvm.convert' + implementationClass = 'org.asciidoctor.gradle.jvm.AsciidoctorJPlugin' + description = "Converts Asciidoctor documents using AsciidoctorJ${pluginExtraText}" + } + asciidoctorPdfPlugin { + id = 'org.asciidoctor.org.asciidoctor.jvm.pdf' + implementationClass = 'org.asciidoctor.gradle.jvm.AsciidoctorJPdfPlugin' + description = "Converts Asciidoctor documents to PDF using AsciidoctorJ${pluginExtraText}" } } } + +intTest { + systemProperties TEST_PROJECTS_DIR : file('src/intTest/projects') +} \ No newline at end of file diff --git a/asciidoctor-gradle-jvm/src/gradleTest/compat-plugin-still-works/build.gradle b/asciidoctor-gradle-jvm/src/gradleTest/compat-plugin-still-works/build.gradle new file mode 100644 index 000000000..d2a84f66f --- /dev/null +++ b/asciidoctor-gradle-jvm/src/gradleTest/compat-plugin-still-works/build.gradle @@ -0,0 +1,17 @@ +plugins { + id 'org.asciidoctor.convert' +} + +asciidoctor { + sources { + include 'test.adoc' + } +} + +task runGradleTest { + dependsOn asciidoctor + + doLast { + assert file("${buildDir}/asciidoc/html5/test.html").exists() + } +} \ No newline at end of file diff --git a/asciidoctor-gradle-jvm/src/gradleTest/compat-plugin-still-works/src/docs/asciidoc/test.adoc b/asciidoctor-gradle-jvm/src/gradleTest/compat-plugin-still-works/src/docs/asciidoc/test.adoc new file mode 100644 index 000000000..0f8a39e2c --- /dev/null +++ b/asciidoctor-gradle-jvm/src/gradleTest/compat-plugin-still-works/src/docs/asciidoc/test.adoc @@ -0,0 +1,3 @@ += Test file + +Check that compatibility plugins still works \ No newline at end of file diff --git a/asciidoctor-gradle-jvm/src/gradleTest/complex-jvm-setup/build.gradle b/asciidoctor-gradle-jvm/src/gradleTest/complex-jvm-setup/build.gradle new file mode 100644 index 000000000..ff7a3ac87 --- /dev/null +++ b/asciidoctor-gradle-jvm/src/gradleTest/complex-jvm-setup/build.gradle @@ -0,0 +1,80 @@ +plugins { + id 'org.asciidoctor.jvm.convert' + id 'org.asciidoctor.jvm.pdf' + + // id 'jruby-gradle.base' version '...' +} + +repositories { + jcenter() +} + +asciidoctorj { + + // options + // attributes + + // extensions + + // gemPaths +} + +configurations { + asciidoctorLeanpub +} + +dependencies { + // asciidoctorLeanpub + //gems +} + +asciidoctor { + + outputOptions { + backends 'html5', 'docbook' + } + + sources { + include 'sample.asciidoc' + } + + resources { + include 'images/**' + } + + asciidoctorj { + // requires + } + + copyResourcesOnlyIf 'html5' + useIntermediateWorkDir() +} + +asciidoctorPdf { + inProcess OUT_OF_PROCESS + logDocuments true + sourceDir 'src/docs/asciidoc' + + sources { + include 'subdir/sample2.ad' + } + + asciidoctorj { + verboseMode true + } +} + +//task asciidoctorLeanpub ( type : org.asciidoctor.gradle.jvm.AsciidoctorTask ) { +// sourceDir 'src/docs/asciidoc' +// configurations 'asciidoctorLeanpub' +// copyNoResources() +// +// asciidoctorj { +// version = '1.6.0-alpha-1' +// safeMode 'unsafe' +// } +//} + +task runGradleTest { + dependsOn asciidoctor, asciidoctorPdf +} \ No newline at end of file diff --git a/asciidoctor-gradle-jvm/src/gradleTest/complex-jvm-setup/src/docs/asciidoc/docinfo-footer.xml b/asciidoctor-gradle-jvm/src/gradleTest/complex-jvm-setup/src/docs/asciidoc/docinfo-footer.xml new file mode 100644 index 000000000..b4ed1b347 --- /dev/null +++ b/asciidoctor-gradle-jvm/src/gradleTest/complex-jvm-setup/src/docs/asciidoc/docinfo-footer.xml @@ -0,0 +1,17 @@ + diff --git a/asciidoctor-gradle-jvm/src/gradleTest/complex-jvm-setup/src/docs/asciidoc/docinfo.xml b/asciidoctor-gradle-jvm/src/gradleTest/complex-jvm-setup/src/docs/asciidoc/docinfo.xml new file mode 100644 index 000000000..b4ed1b347 --- /dev/null +++ b/asciidoctor-gradle-jvm/src/gradleTest/complex-jvm-setup/src/docs/asciidoc/docinfo.xml @@ -0,0 +1,17 @@ + diff --git a/asciidoctor-gradle-jvm/src/gradleTest/complex-jvm-setup/src/docs/asciidoc/images/fake.txt b/asciidoctor-gradle-jvm/src/gradleTest/complex-jvm-setup/src/docs/asciidoc/images/fake.txt new file mode 100644 index 000000000..a8cc3a92e --- /dev/null +++ b/asciidoctor-gradle-jvm/src/gradleTest/complex-jvm-setup/src/docs/asciidoc/images/fake.txt @@ -0,0 +1,4 @@ + +===================== +Not a real image file +===================== \ No newline at end of file diff --git a/asciidoctor-gradle-jvm/src/gradleTest/complex-jvm-setup/src/docs/asciidoc/sample-docinfo-footer.xml b/asciidoctor-gradle-jvm/src/gradleTest/complex-jvm-setup/src/docs/asciidoc/sample-docinfo-footer.xml new file mode 100644 index 000000000..b4ed1b347 --- /dev/null +++ b/asciidoctor-gradle-jvm/src/gradleTest/complex-jvm-setup/src/docs/asciidoc/sample-docinfo-footer.xml @@ -0,0 +1,17 @@ + diff --git a/asciidoctor-gradle-jvm/src/gradleTest/complex-jvm-setup/src/docs/asciidoc/sample-docinfo.xml b/asciidoctor-gradle-jvm/src/gradleTest/complex-jvm-setup/src/docs/asciidoc/sample-docinfo.xml new file mode 100644 index 000000000..b4ed1b347 --- /dev/null +++ b/asciidoctor-gradle-jvm/src/gradleTest/complex-jvm-setup/src/docs/asciidoc/sample-docinfo.xml @@ -0,0 +1,17 @@ + diff --git a/asciidoctor-gradle-jvm/src/gradleTest/complex-jvm-setup/src/docs/asciidoc/sample.asciidoc b/asciidoctor-gradle-jvm/src/gradleTest/complex-jvm-setup/src/docs/asciidoc/sample.asciidoc new file mode 100644 index 000000000..0b8061ee4 --- /dev/null +++ b/asciidoctor-gradle-jvm/src/gradleTest/complex-jvm-setup/src/docs/asciidoc/sample.asciidoc @@ -0,0 +1,26 @@ +Document Title +============== +Doc Writer +:idprefix: id_ + +Preamble paragraph. + +NOTE: This is test, only a test. + +== Section A + +*Section A* paragraph. + +=== Section A Subsection + +*Section A* 'subsection' paragraph. + +== Section B + +*Section B* paragraph. + +.Section B list +* Item 1 +* Item 2 +* Item 3 + diff --git a/asciidoctor-gradle-jvm/src/gradleTest/complex-jvm-setup/src/docs/asciidoc/subdir/_include.adoc b/asciidoctor-gradle-jvm/src/gradleTest/complex-jvm-setup/src/docs/asciidoc/subdir/_include.adoc new file mode 100644 index 000000000..2257d9162 --- /dev/null +++ b/asciidoctor-gradle-jvm/src/gradleTest/complex-jvm-setup/src/docs/asciidoc/subdir/_include.adoc @@ -0,0 +1 @@ +an include file diff --git a/asciidoctor-gradle-jvm/src/gradleTest/complex-jvm-setup/src/docs/asciidoc/subdir/sample2.ad b/asciidoctor-gradle-jvm/src/gradleTest/complex-jvm-setup/src/docs/asciidoc/subdir/sample2.ad new file mode 100644 index 000000000..0b8061ee4 --- /dev/null +++ b/asciidoctor-gradle-jvm/src/gradleTest/complex-jvm-setup/src/docs/asciidoc/subdir/sample2.ad @@ -0,0 +1,26 @@ +Document Title +============== +Doc Writer +:idprefix: id_ + +Preamble paragraph. + +NOTE: This is test, only a test. + +== Section A + +*Section A* paragraph. + +=== Section A Subsection + +*Section A* 'subsection' paragraph. + +== Section B + +*Section B* paragraph. + +.Section B list +* Item 1 +* Item 2 +* Item 3 + diff --git a/asciidoctor-gradle-jvm/src/intTest/groovy/org/asciidoctor/gradle/AsciidoctorFunctionalSpec.groovy b/asciidoctor-gradle-jvm/src/intTest/groovy/org/asciidoctor/gradle/compat/AsciidoctorFunctionalSpec.groovy similarity index 62% rename from asciidoctor-gradle-jvm/src/intTest/groovy/org/asciidoctor/gradle/AsciidoctorFunctionalSpec.groovy rename to asciidoctor-gradle-jvm/src/intTest/groovy/org/asciidoctor/gradle/compat/AsciidoctorFunctionalSpec.groovy index d96f4f798..dcd37c8fe 100755 --- a/asciidoctor-gradle-jvm/src/intTest/groovy/org/asciidoctor/gradle/AsciidoctorFunctionalSpec.groovy +++ b/asciidoctor-gradle-jvm/src/intTest/groovy/org/asciidoctor/gradle/compat/AsciidoctorFunctionalSpec.groovy @@ -13,36 +13,25 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.asciidoctor.gradle +package org.asciidoctor.gradle.compat import org.apache.commons.io.FileUtils +import org.asciidoctor.gradle.internal.FunctionalSpecification import org.gradle.testkit.runner.GradleRunner import org.gradle.testkit.runner.TaskOutcome -import org.junit.Rule -import org.junit.rules.TemporaryFolder -import spock.lang.Specification +import spock.lang.IgnoreIf /** * The first functional specification. * * @author Peter Ledbrook */ -class AsciidoctorFunctionalSpec extends Specification { - public static final String TEST_PROJECTS_DIR = "src/intTest/projects" - @Rule final TemporaryFolder testProjectDir = new TemporaryFolder() - - List pluginClasspath - - def setup() { - def pluginClasspathResource = getClass().classLoader.getResource("plugin-classpath.txt") - if (pluginClasspathResource == null) { - throw new IllegalStateException("Did not find plugin classpath resource, run `testClasses` build task.") - } - - pluginClasspath = pluginClasspathResource.readLines().collect { new File(it) } - } +@SuppressWarnings(['DuplicateStringLiteral', 'MethodName', 'UnnecessaryGString']) +class AsciidoctorFunctionalSpec extends FunctionalSpecification { + public static final String TEST_PROJECTS_DIR = FunctionalSpecification.TEST_PROJECTS_DIR @SuppressWarnings('MethodName') + @IgnoreIf({ System.getProperty('OFFLINE_MODE') }) def "Should do nothing with an empty project"() { given: "A minimal build file" def buildFile = testProjectDir.newFile("build.gradle") @@ -54,20 +43,21 @@ class AsciidoctorFunctionalSpec extends Specification { when: final result = GradleRunner.create() - .withProjectDir(testProjectDir.getRoot()) - .withArguments("asciidoctor") - .withPluginClasspath(pluginClasspath) - .build() + .withProjectDir(testProjectDir.root) + .withArguments("asciidoctor") + .withPluginClasspath(pluginClasspath) + .build() then: result.task(":asciidoctor").outcome == TaskOutcome.NO_SOURCE } @SuppressWarnings('MethodName') + @IgnoreIf({ System.getProperty('OFFLINE_MODE') }) def "Should build normally for a standard project"() { given: "A minimal build file" def buildFile = testProjectDir.newFile("build.gradle") - buildFile << """\ + buildFile << """ plugins { id "org.asciidoctor.convert" } @@ -79,10 +69,10 @@ class AsciidoctorFunctionalSpec extends Specification { when: final result = GradleRunner.create() - .withProjectDir(testProjectDir.getRoot()) - .withArguments("asciidoctor") - .withPluginClasspath(pluginClasspath) - .build() + .withProjectDir(testProjectDir.root) + .withArguments("asciidoctor") + .withPluginClasspath(pluginClasspath) + .build() then: result.task(":asciidoctor").outcome == TaskOutcome.SUCCESS @@ -91,6 +81,7 @@ class AsciidoctorFunctionalSpec extends Specification { } @SuppressWarnings('MethodName') + @IgnoreIf({ System.getProperty('OFFLINE_MODE') }) def "Task should be up-to-date when executed a second time"() { given: "A minimal build file" def buildFile = testProjectDir.newFile("build.gradle") @@ -102,25 +93,25 @@ class AsciidoctorFunctionalSpec extends Specification { and: "Some source files" FileUtils.copyDirectory(new File(TEST_PROJECTS_DIR, "normal"), testProjectDir.root) - final buildDir = new File(testProjectDir.root, "build") when: GradleRunner.create() - .withProjectDir(testProjectDir.getRoot()) - .withArguments("asciidoctor") - .withPluginClasspath(pluginClasspath) - .build() + .withProjectDir(testProjectDir.root) + .withArguments("asciidoctor") + .withPluginClasspath(pluginClasspath) + .build() final result = GradleRunner.create() - .withProjectDir(testProjectDir.getRoot()) - .withArguments("asciidoctor") - .withPluginClasspath(pluginClasspath) - .build() + .withProjectDir(testProjectDir.root) + .withArguments("asciidoctor") + .withPluginClasspath(pluginClasspath) + .build() then: result.task(":asciidoctor").outcome == TaskOutcome.UP_TO_DATE } - @SuppressWarnings('MethodName') + @SuppressWarnings('MethodName') + @IgnoreIf({ System.getProperty('OFFLINE_MODE') }) def "Task should not be up-to-date when classpath is changed"() { given: "A minimal build file" def buildFile = testProjectDir.newFile("build.gradle") @@ -137,19 +128,18 @@ class AsciidoctorFunctionalSpec extends Specification { and: "Some source files" FileUtils.copyDirectory(new File(TEST_PROJECTS_DIR, "normal"), testProjectDir.root) - final buildDir = new File(testProjectDir.root, "build") when: GradleRunner.create() - .withProjectDir(testProjectDir.getRoot()) - .withArguments("asciidoctor") - .withPluginClasspath(pluginClasspath) - .build() + .withProjectDir(testProjectDir.root) + .withArguments("asciidoctor") + .withPluginClasspath(pluginClasspath) + .build() final result = GradleRunner.create() - .withProjectDir(testProjectDir.getRoot()) - .withArguments("asciidoctor", "-PmodifyClasspath") - .withPluginClasspath(pluginClasspath) - .build() + .withProjectDir(testProjectDir.root) + .withArguments("asciidoctor", "-PmodifyClasspath") + .withPluginClasspath(pluginClasspath) + .build() then: result.task(":asciidoctor").outcome == TaskOutcome.SUCCESS diff --git a/asciidoctor-gradle-jvm/src/intTest/groovy/org/asciidoctor/gradle/internal/FunctionalSpecification.groovy b/asciidoctor-gradle-jvm/src/intTest/groovy/org/asciidoctor/gradle/internal/FunctionalSpecification.groovy new file mode 100644 index 000000000..78c8185e3 --- /dev/null +++ b/asciidoctor-gradle-jvm/src/intTest/groovy/org/asciidoctor/gradle/internal/FunctionalSpecification.groovy @@ -0,0 +1,55 @@ +package org.asciidoctor.gradle.internal + +import org.apache.commons.io.FileUtils +import org.gradle.testkit.runner.GradleRunner +import org.junit.Rule +import org.junit.rules.TemporaryFolder +import spock.lang.Shared +import spock.lang.Specification + +class FunctionalSpecification extends Specification { + static + final String TEST_PROJECTS_DIR = System.getProperty('TEST_PROJECTS_DIR') ?: './asciidoctor-gradle-jvm/src/intTest/projects' + static + final String TEST_REPO_DIR = System.getProperty('OFFLINE_REPO') ?: './asciidoctor-gradle-jvm/build/intTestOfflineRepo' + + @Rule + final TemporaryFolder testProjectDir = new TemporaryFolder() + + @Shared + List pluginClasspath + + def setupSpec() { + def pluginClasspathResource = getClass().classLoader.getResource('plugin-classpath.txt') + if (pluginClasspathResource == null) { + pluginClasspathResource = new File('./asciidoctor-gradle-jvm/build/createClasspathManifest/plugin-classpath.txt') + } + if (pluginClasspathResource == null) { + throw new IllegalStateException('Did not find plugin classpath resource, run `intTestClasses` build task.') + } + + pluginClasspath = pluginClasspathResource.readLines().collect { new File(it) } + } + + GradleRunner getGradleRunner(List taskNames = ['asciidoctor']) { + GradleRunner.create() + .withProjectDir(testProjectDir.root) + .withArguments(taskNames) + .withPluginClasspath(pluginClasspath) + .forwardOutput() + .withDebug(true) + } + + @SuppressWarnings(['FactoryMethodName', 'BuilderMethodWithSideEffects']) + void createTestProject(String docGroup = 'normal') { + FileUtils.copyDirectory(new File(TEST_PROJECTS_DIR, docGroup), testProjectDir.root) + } + + String getOfflineRepositories() { + File repo = new File(TEST_REPO_DIR, 'repositories.gradle') + if (!repo.exists()) { + throw new FileNotFoundException("${repo} not found. Run 'cacheOfflineBinaries' build task") + } + "apply from: '${repo.absolutePath}'" + } +} \ No newline at end of file diff --git a/asciidoctor-gradle-jvm/src/intTest/groovy/org/asciidoctor/gradle/jvm/AsciidoctorPdfTaskFunctionalSpec.groovy b/asciidoctor-gradle-jvm/src/intTest/groovy/org/asciidoctor/gradle/jvm/AsciidoctorPdfTaskFunctionalSpec.groovy new file mode 100644 index 000000000..2dd59ab05 --- /dev/null +++ b/asciidoctor-gradle-jvm/src/intTest/groovy/org/asciidoctor/gradle/jvm/AsciidoctorPdfTaskFunctionalSpec.groovy @@ -0,0 +1,80 @@ +package org.asciidoctor.gradle.jvm + +import org.asciidoctor.gradle.internal.FunctionalSpecification +import spock.lang.Unroll + +@SuppressWarnings('MethodName') +class AsciidoctorPdfTaskFunctionalSpec extends FunctionalSpecification { + + static final String DEFAULT_TASK = 'asciidoctor' + static final String DEFAULT_OUTPUT_FILE = 'build/docs/asciidocPdf/sample.pdf' + + void setup() { + createTestProject() + } + + @SuppressWarnings('DuplicateStringLiteral') + @Unroll + void 'Run a PDF generator with intermediateWorkDir=#intermediateMode and parallel=#parallelMode'() { + given: + getBuildFile(""" +asciidoctorPdf { + sourceDir 'src/docs/asciidoc' + + ${intermediateMode ? 'useIntermediateWorkDir()' : ''} + parallelMode ${parallelMode} +} +""") + + when: + getGradleRunner([DEFAULT_TASK, '-s']).build() + + then: + verifyAll { + new File(testProjectDir.root, DEFAULT_OUTPUT_FILE).exists() + } + + where: + parallelMode | intermediateMode + true | true + true | false + false | true + false | false + } + + @SuppressWarnings('DuplicateStringLiteral') + void 'Pdf generation can be run in JAVA_EXEC process mode'() { + given: + getBuildFile(""" +asciidoctorPdf { + sourceDir 'src/docs/asciidoc' + + inProcess = JAVA_EXEC +} +""") + + when: + getGradleRunner([DEFAULT_TASK, '-s']).build() + + then: + verifyAll { + new File(testProjectDir.root, DEFAULT_OUTPUT_FILE).exists() + } + + } + + File getBuildFile(String extraContent) { + File buildFile = testProjectDir.newFile('build.gradle') + buildFile << """ +plugins { + id 'org.asciidoctor.jvm.pdf' +} + +${offlineRepositories} + +${extraContent} +""" + buildFile + } + +} \ No newline at end of file diff --git a/asciidoctor-gradle-jvm/src/intTest/groovy/org/asciidoctor/gradle/jvm/AsciidoctorTaskFunctionalSpec.groovy b/asciidoctor-gradle-jvm/src/intTest/groovy/org/asciidoctor/gradle/jvm/AsciidoctorTaskFunctionalSpec.groovy new file mode 100644 index 000000000..87544b1ef --- /dev/null +++ b/asciidoctor-gradle-jvm/src/intTest/groovy/org/asciidoctor/gradle/jvm/AsciidoctorTaskFunctionalSpec.groovy @@ -0,0 +1,154 @@ +package org.asciidoctor.gradle.jvm + +import org.asciidoctor.gradle.internal.FunctionalSpecification +import spock.lang.Issue +import spock.lang.Unroll + +@SuppressWarnings('MethodName') +class AsciidoctorTaskFunctionalSpec extends FunctionalSpecification { + + static final List DEFAULT_ARGS = ['asciidoctor', '-s'] + static final String VERBOSITY_FILE = 'build/tmp/asciidoctor/asciidoctor-verbose-mode.rb' + static final String VERBOSITY_CONTENT = '$VERBOSE = true' + + void setup() { + createTestProject() + } + + @Issue('https://github.com/gradle/gradle/issues/3698') + @Unroll + void 'Run build with two backends (parallelMode=#parallelMode)'() { + given: + getBuildFile(""" +asciidoctor { + outputOptions { + backends 'html5', 'docbook' + } + logDocuments = true + sourceDir 'src/docs/asciidoc' + parallelMode ${parallelMode} +} +""") + when: + getGradleRunner(DEFAULT_ARGS).build() + + then: 'u content is generated as HTML and XML' + verifyAll { + new File(testProjectDir.root, 'build/docs/asciidoc/html5/sample.html').exists() + new File(testProjectDir.root, 'build/docs/asciidoc/docbook/sample.xml').exists() + } + + and: + verifyAll { + !new File(testProjectDir.root, 'build/docs/asciidoc/docinfo/docinfo.xml').exists() + !new File(testProjectDir.root, 'build/docs/asciidoc/docinfo/sample-docinfo.xml').exists() + } + + where: + parallelMode << [true, false, false] + } + + void 'Support attributes in various formats'() { + given: + getBuildFile(''' +asciidoctorj { + attributes attr1 : 'a string', + attr2 : "A GString", + attr10 : [ 'a', 2, 5 ] +} + +asciidoctor { + outputOptions { + backends 'html5' + } + logDocuments = true + sourceDir 'src/docs/asciidoc' + + asciidoctorj { + attributes attr3 : new File('abc'), + attr4 : { 'a closure' }, + attr20 : [ a : 1 ] + } +} +''') + when: + getGradleRunner(DEFAULT_ARGS).build() + + then: + noExceptionThrown() + } + + void 'Run verbose mode'() { + given: + getBuildFile(''' +asciidoctorj { + verboseMode = true +} + +asciidoctor { + outputOptions { + backends 'html5' + } + sourceDir 'src/docs/asciidoc' +} +''') + when: + getGradleRunner(DEFAULT_ARGS).build() + + then: + new File(testProjectDir.root, VERBOSITY_FILE).text.contains(VERBOSITY_CONTENT) + } + + void 'Can run in JAVA_EXEC process mode'() { + given: + getBuildFile(''' +asciidoctorj { + verboseMode = true +} + +asciidoctor { + + inProcess = JAVA_EXEC + + outputOptions { + backends 'html5' + } + sourceDir 'src/docs/asciidoc' +} +''') + when: + getGradleRunner(DEFAULT_ARGS).build() + + then: + new File(testProjectDir.root, VERBOSITY_FILE).text.contains(VERBOSITY_CONTENT) + } + +// void "When 'resources' not specified, then copy all images to backend"() { +// } +// +// void "When 'resources' not specified and more than one backend, then copy all images to every backend"() { +// } +// +// void "When 'resources' are specified, then copy according to all patterns"() { +// } +// +// @SuppressWarnings('MethodName') +// void "Should require libraries"() { +// } + + File getBuildFile(String extraContent) { + File buildFile = testProjectDir.newFile('build.gradle') + buildFile << """ +plugins { + id 'org.asciidoctor.jvm.convert' +} + +${offlineRepositories} + +${extraContent} +""" + buildFile + } + + +} \ No newline at end of file diff --git a/asciidoctor-gradle-jvm/src/intTest/groovy/org/asciidoctor/gradle/jvm/ExtensionsFunctionalSpec.groovy b/asciidoctor-gradle-jvm/src/intTest/groovy/org/asciidoctor/gradle/jvm/ExtensionsFunctionalSpec.groovy new file mode 100644 index 000000000..ea47b0c7c --- /dev/null +++ b/asciidoctor-gradle-jvm/src/intTest/groovy/org/asciidoctor/gradle/jvm/ExtensionsFunctionalSpec.groovy @@ -0,0 +1,192 @@ +package org.asciidoctor.gradle.jvm + +import org.asciidoctor.gradle.internal.FunctionalSpecification + +class ExtensionsFunctionalSpec extends FunctionalSpecification { + +} + +/* + private static final String ASCIIDOCTOR = 'asciidoctor' + private static final String ASCIIDOC_RESOURCES_DIR = 'build/resources/test/src/asciidocextensions' + private static final String ASCIIDOC_BUILD_DIR = 'build/asciidocextensions' + private static final String ASCIIDOC_MACRO_EXTENSION_SCRIPT = 'blockMacro.groovy' + private static final String ASCIIDOC_INLINE_EXTENSIONS_FILE = 'inlineextensions.asciidoc' + private static final String ASCIIDOC_INLINE_EXTENSIONS_RESULT_FILE = 'inlineextensions.html' + + Project project + File testRootDir + File srcDir + File outDir + + def setup() { + project = ProjectBuilder.builder().withName('test').build() + project.configurations.create(ASCIIDOCTOR) + testRootDir = new File('.') + srcDir = new File(testRootDir, ASCIIDOC_RESOURCES_DIR).absoluteFile + outDir = new File(project.projectDir, ASCIIDOC_BUILD_DIR) + } + + def "Should apply inline BlockProcessor"() { + given: + Task task = project.tasks.create(name: ASCIIDOCTOR, type: AsciidoctorTask) { + sourceDir = srcDir + sources { + include ASCIIDOC_INLINE_EXTENSIONS_FILE + } + outputDir = outDir + extensions { + block(name: "BIG", contexts: [":paragraph"]) { + parent, reader, attributes -> + def upperLines = reader.readLines()*.toUpperCase() + .inject("") {a, b -> a + '\n' + b} + + createBlock(parent, "paragraph", [upperLines], attributes, [:]) + } + block("small") { + parent, reader, attributes -> + def lowerLines = reader.readLines()*.toLowerCase() + .inject("") {a, b -> a + '\n' + b} + + createBlock(parent, "paragraph", [lowerLines], attributes, [:]) + } + + } + } + File resultFile = new File(outDir, 'html5' + File.separator + ASCIIDOC_INLINE_EXTENSIONS_RESULT_FILE) + when: + task.processAsciidocSources() + then: + resultFile.exists() + resultFile.text.contains("WRITE THIS IN UPPERCASE") + resultFile.text.contains("and write this in lowercase") + } + + def "Should apply BlockProcessor from file"() { + given: + print project.files('src/test/resources/src/asciidocextensions/blockMacro.groovy').each {println ">>> $it"} + Task task = project.tasks.create(name: ASCIIDOCTOR, type: AsciidoctorTask) { + sourceDir = srcDir + sources { + include ASCIIDOC_INLINE_EXTENSIONS_FILE + } + outputDir = outDir + extensions new File(sourceDir, ASCIIDOC_MACRO_EXTENSION_SCRIPT) + } + File resultFile = new File(outDir, 'html5' + File.separator + ASCIIDOC_INLINE_EXTENSIONS_RESULT_FILE) + when: + task.processAsciidocSources() + then: + resultFile.exists() + resultFile.text.contains("WRITE THIS IN UPPERCASE") + resultFile.text.contains("and write this in lowercase") + } + + def "Should apply inline Postprocessor"() { + given: + String copyright = "Copyright Acme, Inc." + System.currentTimeMillis() + Task task = project.tasks.create(name: ASCIIDOCTOR, type: AsciidoctorTask) { + sourceDir = srcDir + sources { + include ASCIIDOC_INLINE_EXTENSIONS_FILE + } + outputDir = outDir + extensions { + postprocessor { + document, String output -> + if (document.basebackend("html")) { + Document doc = Jsoup.parse(output, "UTF-8") + + Element contentElement = doc.getElementById("footer-text") + contentElement.append(copyright) + + doc.html() + } else { + throw new IllegalArgumentException("Expected html!") + } + } + } + } + File resultFile = new File(outDir, 'html5' + File.separator + ASCIIDOC_INLINE_EXTENSIONS_RESULT_FILE) + when: + task.processAsciidocSources() + then: + resultFile.exists() + resultFile.text.contains(copyright) + resultFile.text.contains("Inline Extension Test document") + } + + def "Should fail if inline Postprocessor fails"() { + given: + Task task = project.tasks.create(name: ASCIIDOCTOR, type: AsciidoctorTask) { + sourceDir = srcDir + sources { + include ASCIIDOC_INLINE_EXTENSIONS_FILE + } + outputDir = outDir + extensions { + postprocessor { + document, output -> + if (output.contains("blacklisted")) { + throw new IllegalArgumentException("Document contains a blacklisted word") + } + } + } + } + when: + task.processAsciidocSources() + then: + thrown(GradleException) + } + + def "Should apply inline Preprocessor"() { + given: + Task task = project.tasks.create(name: ASCIIDOCTOR, type: AsciidoctorTask) { + sourceDir = srcDir + sources { + include ASCIIDOC_INLINE_EXTENSIONS_FILE + } + outputDir = outDir + extensions { + preprocessor { + document, reader -> + reader.advance() + reader + } + } + } + File resultFile = new File(outDir, 'html5' + File.separator + ASCIIDOC_INLINE_EXTENSIONS_RESULT_FILE) + when: + task.processAsciidocSources() + then: + resultFile.exists() + !resultFile.text.contains("Inline Extension Test document") + } + + def "Should apply inline Includeprocessor"() { + given: + String content = "The content of the URL " + System.currentTimeMillis() + Task task = project.tasks.create(name: ASCIIDOCTOR, type: AsciidoctorTask) { + sourceDir = srcDir + sources { + include ASCIIDOC_INLINE_EXTENSIONS_FILE + } + outputDir = outDir + extensions { + include_processor(filter: { it.startsWith('http') }) { + document, reader, target, attributes -> + reader.push_include(content, target, target, 1, attributes) + } + } + } + File resultFile = new File(outDir, 'html5' + File.separator + ASCIIDOC_INLINE_EXTENSIONS_RESULT_FILE) + + when: + task.processAsciidocSources() + + then: + resultFile.exists() + resultFile.text.contains(content) + } + + */ \ No newline at end of file diff --git a/asciidoctor-gradle-jvm/src/intTest/projects/normal/src/docs/asciidoc/subdir/sample2.ad b/asciidoctor-gradle-jvm/src/intTest/projects/normal/src/docs/asciidoc/subdir/sample2.ad index 0b8061ee4..1e8f9d7f6 100644 --- a/asciidoctor-gradle-jvm/src/intTest/projects/normal/src/docs/asciidoc/subdir/sample2.ad +++ b/asciidoctor-gradle-jvm/src/intTest/projects/normal/src/docs/asciidoc/subdir/sample2.ad @@ -1,5 +1,4 @@ -Document Title -============== += Document Title Doc Writer :idprefix: id_ diff --git a/asciidoctor-gradle-jvm/src/main/groovy/org/asciidoctor/gradle/AsciidoctorCompatibilityPlugin.groovy b/asciidoctor-gradle-jvm/src/main/groovy/org/asciidoctor/gradle/AsciidoctorCompatibilityPlugin.groovy new file mode 100755 index 000000000..474e9567f --- /dev/null +++ b/asciidoctor-gradle-jvm/src/main/groovy/org/asciidoctor/gradle/AsciidoctorCompatibilityPlugin.groovy @@ -0,0 +1,142 @@ +/* + * Copyright 2013-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.asciidoctor.gradle + +import groovy.transform.CompileDynamic +import org.asciidoctor.gradle.compat.AsciidoctorExtension +import org.gradle.api.Action +import org.gradle.api.GradleException +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.api.artifacts.Configuration +import org.gradle.api.artifacts.ResolvableDependencies +import org.gradle.api.artifacts.dsl.DependencyHandler +import org.gradle.api.logging.LogLevel +import org.gradle.util.GradleVersion + +/** + * @author Noam Tenne + * @author Andres Almiray + * @author Patrick Reimers + * @author Markus Schlichting + * @author Schalk W. Cronjé + */ +@Deprecated +class AsciidoctorCompatibilityPlugin implements Plugin { + static final String ASCIIDOCTOR = 'asciidoctor' + static final String ASCIIDOCTORJ = 'asciidoctorj' + static final String ASCIIDOCTORJ_CORE_DEPENDENCY = 'org.asciidoctor:asciidoctorj:' + static final String ASCIIDOCTORJ_GROOVY_DSL_DEPENDENCY = 'org.asciidoctor:asciidoctorj-groovy-dsl:' + + void apply(Project project) { + + ['jvm.convert', 'js.base'].each { String s -> + String pluginName = "org.asciidoctor.${s}" + if (project.gradle.plugins.hasPlugin(pluginName)) { + throw new GradleException("'${pluginName}' and 'org.asciidoctor.convert' cannot be used within the same (sub)project") + } + } + + project.apply plugin: 'org.asciidoctor.base' + + AsciidoctorExtension extension = project.extensions.create(ASCIIDOCTORJ, AsciidoctorExtension, project) + + project.afterEvaluate { + if (!project.extensions.asciidoctorj.noDefaultRepositories) { + project.repositories { + jcenter() + } + } + } + + Configuration configuration = project.configurations.maybeCreate(ASCIIDOCTOR) + project.logger.info("[Asciidoctor] asciidoctorj: ${extension.version}") + project.logger.info("[Asciidoctor] asciidoctorj-groovy-dsl: ${extension.groovyDslVersion}") + + configuration.incoming.beforeResolve(new Action() { + @SuppressWarnings('UnusedMethodParameter') + void execute(ResolvableDependencies resolvableDependencies) { + DependencyHandler dependencyHandler = project.dependencies + def dependencies = configuration.dependencies + dependencies.add(dependencyHandler.create(ASCIIDOCTORJ_CORE_DEPENDENCY + extension.version)) + dependencies.add(dependencyHandler.create(ASCIIDOCTORJ_GROOVY_DSL_DEPENDENCY + extension.groovyDslVersion)) + } + }) + + project.task(ASCIIDOCTOR, + type: AsciidoctorTask, + group: 'Documentation', + description: 'Converts AsciiDoc files and copies the output files and related resources to the build directory.') { + classpath = configuration + } + + addMigrationSupport( + project, + "'org.asciidoctor.convert' is deprecated. When you have time please switch over to 'org.asciidoctor.jvm.convert'.", + 'jcenter() is no longer added by default. If you relied on this behaviour in the past, please add jcenter() to the repositories block.' + ) + } + + @CompileDynamic + private addMigrationSupport(Project project, String... pluginMessages) { + project.afterEvaluate { + Set messages = [] + messages.addAll(pluginMessages) + project.tasks.withType(AsciidoctorTask) { AsciidoctorTask task -> + messages.addAll(task.migrationMessages) + } + + if (messages.empty) { + return + } + + if (GradleVersion.current() >= GradleVersion.version('4.5')) { + switch (project.gradle.startParameter.warningMode.toString().toLowerCase()) { + case 'all': + project.logger.lifecycle(createMigrationOutputMessage(messages)) + break + case 'none': + break + default: + project.logger.lifecycle 'You are using one or more deprecated Asciidoctor task or plugins. To help with migration run with --warnings=all' + } + } else { + if (project.gradle.startParameter.logLevel != LogLevel.QUIET) { + if (project.gradle.startParameter.logLevel == LogLevel.LIFECYCLE) { + project.logger.lifecycle 'You are using one or more deprecated Asciidoctor task or plugins. To help with migration run with -i or --info' + } else { + project.logger.info(createMigrationOutputMessage(messages)) + } + } + } + } + } + + @SuppressWarnings('FactoryMethodName') + static String createMigrationOutputMessage(final Set messages) { + StringWriter output = new StringWriter() + output.withCloseable { + output.println 'You are using one or more deprecated Asciidoctor task or plugins. These will be removed in a future release. To help you migrate we have compiled some tips for you based upon your current usage:' + output.println() + + messages.each { String msg -> + output.println " - ${msg}" + } + output.println() + output.toString() + } + } +} diff --git a/asciidoctor-gradle-jvm/src/main/groovy/org/asciidoctor/gradle/AsciidoctorTask.groovy b/asciidoctor-gradle-jvm/src/main/groovy/org/asciidoctor/gradle/AsciidoctorTask.groovy index dc2c01aff..26020efe9 100755 --- a/asciidoctor-gradle-jvm/src/main/groovy/org/asciidoctor/gradle/AsciidoctorTask.groovy +++ b/asciidoctor-gradle-jvm/src/main/groovy/org/asciidoctor/gradle/AsciidoctorTask.groovy @@ -15,6 +15,13 @@ */ package org.asciidoctor.gradle +import org.asciidoctor.gradle.compat.AsciidoctorBackend +import org.asciidoctor.gradle.compat.AsciidoctorProxy +import org.asciidoctor.gradle.compat.AsciidoctorProxyCacheKey +import org.asciidoctor.gradle.compat.AsciidoctorProxyImpl +import org.asciidoctor.gradle.compat.ResourceCopyProxy +import org.asciidoctor.gradle.compat.ResourceCopyProxyImpl +import org.asciidoctor.gradle.internal.AsciidoctorUtils import org.gradle.api.DefaultTask import org.gradle.api.GradleException import org.gradle.api.InvalidUserDataException @@ -24,10 +31,9 @@ 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.InputFiles import org.gradle.api.tasks.Optional import org.gradle.api.tasks.InputDirectory -import org.gradle.api.tasks.InputFile -import org.gradle.api.tasks.InputFiles import org.gradle.api.tasks.OutputDirectories import org.gradle.api.tasks.OutputDirectory import org.gradle.api.tasks.SkipWhenEmpty @@ -54,6 +60,7 @@ import java.util.concurrent.locks.ReentrantLock * @author Schalk W. Cronjé * @author Robert Panzer */ +@Deprecated @SuppressWarnings(['MethodCount', 'Instanceof']) class AsciidoctorTask extends DefaultTask { private static final boolean IS_WINDOWS = System.getProperty('os.name').contains('Windows') @@ -61,6 +68,8 @@ class AsciidoctorTask extends DefaultTask { 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 MIGRATE_GEMS_MSG = 'When upgrading GEMs, \'requires\' will need to be set via the asciidoctorj project and task extensions. Use setGemPaths method in extension(s) to set GEM paths.' private static final String DEFAULT_BACKEND = AsciidoctorBackend.HTML5.id public static final String ASCIIDOCTOR_FACTORY_CLASSNAME = 'org.asciidoctor.Asciidoctor$Factory' @@ -76,15 +85,25 @@ class AsciidoctorTask extends DefaultTask { private PatternSet sourceDocumentPattern private CopySpec resourceCopy private static ClassLoader cl + private boolean separateOutputDirs = true private final static Map ASCIIDOCTORS = [:] private final static Lock EXEC_LOCK = new ReentrantLock() + private final Set migrationMessages = [] + /** If set to true each backend will be output to a separate subfolder below {@code outputDir} * @since 1.5.1 */ @Input - boolean separateOutputDirs = true + boolean getSeparateOutputDirs() { + migrationMessage('seperateOutputDirs', 'Separate output directories are now configured via outputOptions.separateOutputDirs') + this.separateOutputDirs + } + + void setSeparateOutputDirs(boolean v) { + this.separateOutputDirs = v + } @Optional @InputDirectory @@ -96,22 +115,8 @@ class AsciidoctorTask extends DefaultTask { @Optional boolean logDocuments = false - /** Old way to set only one source document - * @deprecated Use {@code sources} instead - */ - @Optional - @InputFile - File sourceDocumentName - - /** Old way to define the backend to use - * @deprecated Use {@code backends} instead - */ - @Optional - @Input - String backend - /** - * Stores the extensions defined in the configuration phase + * Stores the extensionRegistry defined in the runConfiguration phase * to register them in the execution phase. */ List asciidoctorExtensions = [] @@ -146,8 +151,11 @@ class AsciidoctorTask extends DefaultTask { 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' + migrationMessage('setOptions', """pay attention to these attributes found in options. Currently attributes will be replaced due to assignment. Please use one of the following instead instead as current behaviour will no longer be available when upgrading: + - 'attributes' + - 'project.asciidoctorj.attributes' + - '${name}.asciidoctorj.attributes' +""") attrs = coerceLegacyAttributeFormats(m.attributes) m.remove('attributes') } @@ -167,8 +175,11 @@ class AsciidoctorTask extends DefaultTask { 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' + migrationMessage('setOptions', """pay attention to these attributes found in options. Currently these attributes added to exustign attributes. Please use one of the following instead instead as current behaviour will no longer be available when upgrading: + - 'attributes' + - 'project.asciidoctorj.attributes' + - '${name}.asciidoctorj.attributes' +""") attributes coerceLegacyAttributeFormats(m.attributes) m.remove('attributes') } @@ -189,6 +200,7 @@ class AsciidoctorTask extends DefaultTask { * @since 1.5.1 */ void setAttributes(Map m) { + migrationMessage('setAttributes', 'the behaviour of setAttributes will change when upgrading. You may decide to use the asciidoctorj.setAttributes that exists as a project or task extension instead') if (m) { this.attrs = m } else { @@ -224,7 +236,9 @@ class AsciidoctorTask extends DefaultTask { * @param b One or more ruby modules to be included * @since 1.5.0 */ + @Deprecated void setRequires(Object... b) { + migrationMessage('setRequires', MIGRATE_GEMS_MSG) this.requires?.clear() requires(b) } @@ -235,7 +249,9 @@ class AsciidoctorTask extends DefaultTask { * @since 1.5.1 */ @SuppressWarnings('ConfusingMethodName') + @Deprecated void requires(Object... b) { + migrationMessage('requires', MIGRATE_GEMS_MSG) if (this.requires == null) { this.requires = [] } @@ -250,12 +266,9 @@ class AsciidoctorTask extends DefaultTask { */ @Optional @Input - Set getBackends() { this.backends } - - void setBackend(String b) { - if (!b) return // null check - deprecated 'setBackend', 'backends', 'Using `backend` and `backends` together will result in `backend` being ignored.' - backend = b + Set getBackends() { + migrationMessage('getBackends', 'Use outputOptions.getBackends') + this.backends } /** Applies a new set of Asciidoctor backends that will be used for document generation clearing any @@ -266,6 +279,7 @@ class AsciidoctorTask extends DefaultTask { * @since 0.7.1 */ void setBackends(Object... b) { + migrationMessage('setBackends', 'Use outputOoptons.setBackends') this.backends?.clear() backends(b) } @@ -278,6 +292,7 @@ class AsciidoctorTask extends DefaultTask { */ @SuppressWarnings('ConfusingMethodName') void backends(Object... b) { + migrationMessage('backends', 'Use outputOoptons.setbackends') if (this.backends == null) { this.backends = [] } @@ -285,11 +300,13 @@ class AsciidoctorTask extends DefaultTask { this.backends.addAll(CollectionUtils.stringize(b as List)) } - /** Defines extensions. The given parameters should + /** Defines extensionRegistry. The given parameters should * either contain Asciidoctor Groovy DSL closures or files * with content conforming to the Asciidoctor Groovy DSL. */ + @Deprecated void extensions(Object... exts) { + migrationMessage('extensions', 'Extensions will need to be set via the asciidoctorj project and task extensions') if (!exts) return // null check asciidoctorExtensions.addAll(exts as List) } @@ -300,7 +317,9 @@ class AsciidoctorTask extends DefaultTask { * @since 1.5.1 */ @SuppressWarnings('ConfusingMethodName') + @Deprecated void gemPath(Object... f) { + migrationMessage('gemPath', 'GEM paths will need to be set via the asciidoctorj project and task extensions using the gemPaths method') if (!f) return // null check this.gemPaths.addAll(f as List) } @@ -310,7 +329,9 @@ class AsciidoctorTask extends DefaultTask { * @param f A {@code File} object pointing to list of installed GEMs * @since 1.5.0 */ + @Deprecated void setGemPath(Object... f) { + migrationMessage('setGemPath(Object...)', MIGRATE_GEMS_MSG) this.gemPaths.clear() if (!f) return // null check this.gemPaths.addAll(f as List) @@ -323,6 +344,7 @@ class AsciidoctorTask extends DefaultTask { */ @SuppressWarnings('UnnecessarySetter') void setGemPath(Object path) { + migrationMessage('setGemPath(Object)', MIGRATE_GEMS_MSG) this.gemPaths.clear() if (path instanceof CharSequence) { this.gemPaths.addAll(setGemPath(path.split(PATH_SEPARATOR))) @@ -405,59 +427,6 @@ class AsciidoctorTask extends DefaultTask { 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} - * @since 1.5.0 - * @deprecated - */ - 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} - * @deprecated - */ - @SuppressWarnings('UnnecessarySetter') - 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} - * @since 1.5.0 - * @deprecated - */ - @SuppressWarnings(['DuplicateStringLiteral', 'UnnecessarySetter']) - 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 @@ -485,12 +454,12 @@ class AsciidoctorTask extends DefaultTask { @SkipWhenEmpty FileTree getSourceFileTree() { project.fileTree(sourceDir). - matching(this.sourceDocumentPattern ?: defaultSourceDocumentPattern) + matching(this.sourceDocumentPattern ?: defaultSourceDocumentPattern) } /** Add patterns for source files or source files via a closure * - * @param cfg PatternSet configuration closure + * @param cfg PatternSet runConfiguration closure * @since 1.5.1 */ void sources(Closure cfg) { @@ -505,7 +474,7 @@ class AsciidoctorTask extends DefaultTask { /** 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 + * @param cfg CopySpec runConfiguration closure * @since 1.5.1 */ void resources(Closure cfg) { @@ -625,6 +594,11 @@ class AsciidoctorTask extends DefaultTask { destinationParentDir } + // Helper method to be able to produce migration messages + Set getMigrationMessages() { + this.migrationMessages + } + private File outputBackendDir(final File outputDir, final String backend) { separateOutputDirs ? new File(outputDir, FileUtils.toSafeFileName(backend)) : outputDir } @@ -638,12 +612,7 @@ class AsciidoctorTask extends DefaultTask { } protected Set activeBackends() { - if (this.backends) { - return this.backends - } else if (backend) { - return [backend] - } - [DEFAULT_BACKEND] + this.backends ?: [DEFAULT_BACKEND] } @SuppressWarnings('CatchException') @@ -671,15 +640,15 @@ class AsciidoctorTask extends DefaultTask { logger.lifecycle("Converting $file") } asciidoctor.renderFile(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])) + [ + project : project, + options : options, + attributes: attrs, + baseDir : !baseDir && !baseDirSetToNull ? file.parentFile : baseDir, + projectDir: project.projectDir, + rootDir : project.rootDir, + outputDir : destinationParentDir, + backend : backend])) } @SuppressWarnings('AbcMetric') @@ -792,6 +761,10 @@ class AsciidoctorTask extends DefaultTask { } } + private void migrationMessage(final String currentMethod, final String upgradeInstructions) { + this.migrationMessages.add("You have used '${currentMethod}'. When upgrading you will need to: ${upgradeInstructions}") + } + @SuppressWarnings('DuplicateStringLiteral') @SuppressWarnings('DuplicateNumberLiteral') private static Map coerceLegacyAttributeFormats(Object attributes) { @@ -854,18 +827,13 @@ class AsciidoctorTask extends DefaultTask { } } - 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}" - } - @SuppressWarnings('CatchException') private withAsciidoctor(Closure cl) { EXEC_LOCK.lock() try { def key = new AsciidoctorProxyCacheKey( - classpath?.files?.toList(), - asGemPath() + classpath?.files?.toList(), + asGemPath() ) def asciidoctor = ASCIIDOCTORS.computeIfAbsent(key) { k -> def clazz = loadClass(ASCIIDOCTOR_FACTORY_CLASSNAME) diff --git a/asciidoctor-gradle-jvm/src/main/groovy/org/asciidoctor/gradle/AsciidoctorUtils.groovy b/asciidoctor-gradle-jvm/src/main/groovy/org/asciidoctor/gradle/AsciidoctorUtils.groovy deleted file mode 100644 index 18090330b..000000000 --- a/asciidoctor-gradle-jvm/src/main/groovy/org/asciidoctor/gradle/AsciidoctorUtils.groovy +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright 2013-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.asciidoctor.gradle - -import java.util.regex.Pattern - -class AsciidoctorUtils { - - /** - * Based on - * http://stackoverflow.com/questions/204784/how-to-construct-a-relative-path-in-java-from-two-absolute-paths-or-urls/1290311#1290311 - * - * Returns the path of one File relative to another. - * - * @param target the target directory - * @param base the base directory - * @return target's path relative to the base directory - * @throws IOException if an error occurs while resolving the files' canonical names - */ - static String getRelativePath(File target, File base) throws IOException { - String[] baseComponents = base.canonicalPath.split(Pattern.quote(File.separator)) - String[] targetComponents = target.canonicalPath.split(Pattern.quote(File.separator)) - - // skip common components - int index = 0 - for (; index < targetComponents.length && index < baseComponents.length; ++index) { - if (!targetComponents[index].equals(baseComponents[index])) { - break - } - } - - StringBuilder result = new StringBuilder() - if (index != baseComponents.length) { - // backtrack to base directory - for (int i = index; i < baseComponents.length; ++i) { - if (i != index) { - result.append(File.separator) - } - result.append('..') - } - } - for (int i = index; i < targetComponents.length; ++i) { - if (i != index) { - result.append(File.separator) - } - result.append(targetComponents[i]) - } - - result.toString() - } -} diff --git a/asciidoctor-gradle-jvm/src/main/groovy/org/asciidoctor/gradle/AsciidoctorBackend.groovy b/asciidoctor-gradle-jvm/src/main/groovy/org/asciidoctor/gradle/compat/AsciidoctorBackend.groovy similarity index 97% rename from asciidoctor-gradle-jvm/src/main/groovy/org/asciidoctor/gradle/AsciidoctorBackend.groovy rename to asciidoctor-gradle-jvm/src/main/groovy/org/asciidoctor/gradle/compat/AsciidoctorBackend.groovy index 8c97895e1..9c7042be5 100755 --- a/asciidoctor-gradle-jvm/src/main/groovy/org/asciidoctor/gradle/AsciidoctorBackend.groovy +++ b/asciidoctor-gradle-jvm/src/main/groovy/org/asciidoctor/gradle/compat/AsciidoctorBackend.groovy @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.asciidoctor.gradle +package org.asciidoctor.gradle.compat /** * Supported backends. diff --git a/asciidoctor-gradle-jvm/src/main/groovy/org/asciidoctor/gradle/AsciidoctorExtension.groovy b/asciidoctor-gradle-jvm/src/main/groovy/org/asciidoctor/gradle/compat/AsciidoctorExtension.groovy similarity index 96% rename from asciidoctor-gradle-jvm/src/main/groovy/org/asciidoctor/gradle/AsciidoctorExtension.groovy rename to asciidoctor-gradle-jvm/src/main/groovy/org/asciidoctor/gradle/compat/AsciidoctorExtension.groovy index 0b8765039..9a7ab4f56 100644 --- a/asciidoctor-gradle-jvm/src/main/groovy/org/asciidoctor/gradle/AsciidoctorExtension.groovy +++ b/asciidoctor-gradle-jvm/src/main/groovy/org/asciidoctor/gradle/compat/AsciidoctorExtension.groovy @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.asciidoctor.gradle +package org.asciidoctor.gradle.compat import org.gradle.api.Project diff --git a/asciidoctor-gradle-jvm/src/main/groovy/org/asciidoctor/gradle/AsciidoctorProxy.java b/asciidoctor-gradle-jvm/src/main/groovy/org/asciidoctor/gradle/compat/AsciidoctorProxy.java similarity index 96% rename from asciidoctor-gradle-jvm/src/main/groovy/org/asciidoctor/gradle/AsciidoctorProxy.java rename to asciidoctor-gradle-jvm/src/main/groovy/org/asciidoctor/gradle/compat/AsciidoctorProxy.java index ad3c90323..97135e6fb 100644 --- a/asciidoctor-gradle-jvm/src/main/groovy/org/asciidoctor/gradle/AsciidoctorProxy.java +++ b/asciidoctor-gradle-jvm/src/main/groovy/org/asciidoctor/gradle/compat/AsciidoctorProxy.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.asciidoctor.gradle; +package org.asciidoctor.gradle.compat; import java.io.File; import java.util.List; diff --git a/asciidoctor-gradle-jvm/src/main/groovy/org/asciidoctor/gradle/AsciidoctorProxyCacheKey.groovy b/asciidoctor-gradle-jvm/src/main/groovy/org/asciidoctor/gradle/compat/AsciidoctorProxyCacheKey.groovy similarity index 95% rename from asciidoctor-gradle-jvm/src/main/groovy/org/asciidoctor/gradle/AsciidoctorProxyCacheKey.groovy rename to asciidoctor-gradle-jvm/src/main/groovy/org/asciidoctor/gradle/compat/AsciidoctorProxyCacheKey.groovy index 1bdfd8857..ab1c60709 100644 --- a/asciidoctor-gradle-jvm/src/main/groovy/org/asciidoctor/gradle/AsciidoctorProxyCacheKey.groovy +++ b/asciidoctor-gradle-jvm/src/main/groovy/org/asciidoctor/gradle/compat/AsciidoctorProxyCacheKey.groovy @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.asciidoctor.gradle +package org.asciidoctor.gradle.compat import groovy.transform.Immutable diff --git a/asciidoctor-gradle-jvm/src/main/groovy/org/asciidoctor/gradle/AsciidoctorProxyImpl.groovy b/asciidoctor-gradle-jvm/src/main/groovy/org/asciidoctor/gradle/compat/AsciidoctorProxyImpl.groovy similarity index 97% rename from asciidoctor-gradle-jvm/src/main/groovy/org/asciidoctor/gradle/AsciidoctorProxyImpl.groovy rename to asciidoctor-gradle-jvm/src/main/groovy/org/asciidoctor/gradle/compat/AsciidoctorProxyImpl.groovy index c4470b64f..133db2d62 100644 --- a/asciidoctor-gradle-jvm/src/main/groovy/org/asciidoctor/gradle/AsciidoctorProxyImpl.groovy +++ b/asciidoctor-gradle-jvm/src/main/groovy/org/asciidoctor/gradle/compat/AsciidoctorProxyImpl.groovy @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.asciidoctor.gradle +package org.asciidoctor.gradle.compat /** * @author andres Almiray diff --git a/asciidoctor-gradle-jvm/src/main/groovy/org/asciidoctor/gradle/ResourceCopyProxy.java b/asciidoctor-gradle-jvm/src/main/groovy/org/asciidoctor/gradle/compat/ResourceCopyProxy.java similarity index 95% rename from asciidoctor-gradle-jvm/src/main/groovy/org/asciidoctor/gradle/ResourceCopyProxy.java rename to asciidoctor-gradle-jvm/src/main/groovy/org/asciidoctor/gradle/compat/ResourceCopyProxy.java index b18f318d0..e83a2acd1 100644 --- a/asciidoctor-gradle-jvm/src/main/groovy/org/asciidoctor/gradle/ResourceCopyProxy.java +++ b/asciidoctor-gradle-jvm/src/main/groovy/org/asciidoctor/gradle/compat/ResourceCopyProxy.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.asciidoctor.gradle; +package org.asciidoctor.gradle.compat; import org.gradle.api.file.CopySpec; import org.gradle.api.tasks.WorkResult; diff --git a/asciidoctor-gradle-jvm/src/main/groovy/org/asciidoctor/gradle/ResourceCopyProxyImpl.groovy b/asciidoctor-gradle-jvm/src/main/groovy/org/asciidoctor/gradle/compat/ResourceCopyProxyImpl.groovy similarity index 96% rename from asciidoctor-gradle-jvm/src/main/groovy/org/asciidoctor/gradle/ResourceCopyProxyImpl.groovy rename to asciidoctor-gradle-jvm/src/main/groovy/org/asciidoctor/gradle/compat/ResourceCopyProxyImpl.groovy index 35c961da3..184a3fe11 100644 --- a/asciidoctor-gradle-jvm/src/main/groovy/org/asciidoctor/gradle/ResourceCopyProxyImpl.groovy +++ b/asciidoctor-gradle-jvm/src/main/groovy/org/asciidoctor/gradle/compat/ResourceCopyProxyImpl.groovy @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.asciidoctor.gradle +package org.asciidoctor.gradle.compat import org.gradle.api.Project import org.gradle.api.file.CopySpec diff --git a/asciidoctor-gradle-jvm/src/main/groovy/org/asciidoctor/gradle/internal/AsciidoctorUtils.groovy b/asciidoctor-gradle-jvm/src/main/groovy/org/asciidoctor/gradle/internal/AsciidoctorUtils.groovy new file mode 100644 index 000000000..50780ef87 --- /dev/null +++ b/asciidoctor-gradle-jvm/src/main/groovy/org/asciidoctor/gradle/internal/AsciidoctorUtils.groovy @@ -0,0 +1,54 @@ +/* + * Copyright 2013-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.asciidoctor.gradle.internal + +import groovy.transform.CompileStatic +import org.ysb33r.grolifant.api.OperatingSystem + +@CompileStatic +class AsciidoctorUtils { + + static final OperatingSystem OS = OperatingSystem.current() + + private static final String DOUBLE_BACKLASH = '\\\\' + private static final String BACKLASH = '\\' + + /** Normalises slashes in a path. + * + * @param path + * @return Slashes chanegs to backslahes no Windows, unahcnges otherwise. + */ + static String normalizePath(String path) { + if (OS.windows) { + path = path.replace(DOUBLE_BACKLASH, BACKLASH) + path = path.replace(BACKLASH, DOUBLE_BACKLASH) + } + path + } + + /** + * Returns the path of one File relative to another. + * + * @param target the target directory + * @param base the base directory + * @return target's path relative to the base directory + * @throws IOException if an error occurs while resolving the files' canonical names + */ + static String getRelativePath(File target, File base) throws IOException { + base.toPath().relativize(target.toPath()).toFile().toString() + } + +} diff --git a/asciidoctor-gradle-jvm/src/main/groovy/org/asciidoctor/gradle/internal/ExecutorConfiguration.groovy b/asciidoctor-gradle-jvm/src/main/groovy/org/asciidoctor/gradle/internal/ExecutorConfiguration.groovy new file mode 100644 index 000000000..775af9513 --- /dev/null +++ b/asciidoctor-gradle-jvm/src/main/groovy/org/asciidoctor/gradle/internal/ExecutorConfiguration.groovy @@ -0,0 +1,57 @@ +package org.asciidoctor.gradle.internal + +import groovy.transform.CompileStatic +import groovy.transform.TupleConstructor + +/** Configuration for running Asciidoctor + * + * @since 1.6.0 + */ +@CompileStatic +@SuppressWarnings(['ClassName','CloneableWithoutClone']) +@TupleConstructor +class ExecutorConfiguration implements Serializable, Cloneable { + File sourceDir + File outputDir + File projectDir + File rootDir + File baseDir + Set sourceTree + + String backendName + String gemPath + + boolean logDocuments + boolean copyResources + + int safeModeLevel + + Set requires + Map options + Map attributes + + List asciidoctorExtensions + + String toString() { + """backend(s) = ${backendName} + +File locations: + sourceDir = ${sourceDir} + outputDir = ${outputDir} + projectDir = ${projectDir} + rootProjectDir = ${rootDir} + baseDir = ${baseDir} + +JRuby: + GEMPATH = ${gemPath} + requires = ${requires} + +Asciidoctor: + ${options.size()} options + ${attributes.size()} attributes + ${asciidoctorExtensions?.size() ?: 0} extensions + logDocuments = ${logDocuments}, copyReources = ${copyResources}, safeMode = ${safeModeLevel} +""" + } +} + diff --git a/asciidoctor-gradle-jvm/src/main/groovy/org/asciidoctor/gradle/internal/ExecutorConfigurationContainer.groovy b/asciidoctor-gradle-jvm/src/main/groovy/org/asciidoctor/gradle/internal/ExecutorConfigurationContainer.groovy new file mode 100644 index 000000000..dec1519cd --- /dev/null +++ b/asciidoctor-gradle-jvm/src/main/groovy/org/asciidoctor/gradle/internal/ExecutorConfigurationContainer.groovy @@ -0,0 +1,21 @@ +package org.asciidoctor.gradle.internal + +import groovy.transform.CompileStatic + +/** Contains a number of executor configurations. + * + * @since 1.6.0 + */ +@CompileStatic +class ExecutorConfigurationContainer implements Serializable { + final List configurations + + ExecutorConfigurationContainer(Iterable list) { + this.configurations = list as List + } + + ExecutorConfigurationContainer(ExecutorConfiguration single) { + this.configurations = [single] + } +} + diff --git a/asciidoctor-gradle-jvm/src/main/groovy/org/asciidoctor/gradle/jvm/AbstractAsciidoctorTask.groovy b/asciidoctor-gradle-jvm/src/main/groovy/org/asciidoctor/gradle/jvm/AbstractAsciidoctorTask.groovy new file mode 100644 index 000000000..db3b93945 --- /dev/null +++ b/asciidoctor-gradle-jvm/src/main/groovy/org/asciidoctor/gradle/jvm/AbstractAsciidoctorTask.groovy @@ -0,0 +1,767 @@ +/* + * Copyright 2013-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.asciidoctor.gradle.jvm + +import groovy.transform.CompileDynamic +import groovy.transform.CompileStatic +import org.asciidoctor.gradle.internal.ExecutorConfiguration +import org.asciidoctor.gradle.internal.ExecutorConfigurationContainer +import org.asciidoctor.gradle.remote.AsciidoctorJExecuter +import org.asciidoctor.gradle.remote.AsciidoctorJavaExec +import org.gradle.api.Action +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.* +import org.gradle.api.tasks.util.PatternSet +import org.gradle.process.JavaExecSpec +import org.gradle.process.JavaForkOptions +import org.gradle.util.GradleVersion +import org.gradle.workers.WorkerConfiguration +import org.gradle.workers.WorkerExecutor +import org.ysb33r.grolifant.api.FileUtils +import org.ysb33r.grolifant.api.StringUtils + +import java.nio.file.Path + +import static org.gradle.workers.IsolationMode.CLASSLOADER +import static org.gradle.workers.IsolationMode.PROCESS + +/** + * @since 1.6.0 + * @author Schalk W. Cronjé + */ +@SuppressWarnings('MethodCount') +@CompileStatic +class AbstractAsciidoctorTask extends DefaultTask { + + final static ProcessMode IN_PROCESS = ProcessMode.IN_PROCESS + final static ProcessMode OUT_OF_PROCESS = ProcessMode.OUT_OF_PROCESS + final static ProcessMode JAVA_EXEC = ProcessMode.JAVA_EXEC + + @Nested + protected final OutputOptions configuredOutputOptions = new OutputOptions() + + private final AsciidoctorJExtension asciidoctorj + private final WorkerExecutor worker + private final List asciidocConfigurations = [] + + private Object baseDir + private Object srcDir + private Object outDir + private PatternSet sourceDocumentPattern + private PatternSet secondarySourceDocumentPattern + private CopySpec resourceCopy + + private List copyResourcesForBackends + private boolean withIntermediateWorkDir = false + + /** Logs documents as they are converted + * + */ + boolean logDocuments = false + + /** Run Asciidoctor conversions in or out of process + * + * Valid options are {@link #IN_PROCESS}, {@link #OUT_OF_PROCESS} and {@link #JAVA_EXEC}. + * {@code JAVA_EXEC} should only be used as a last resort ans it runs less effective than the + * other options. It does provide completion isolation and is a workaround when Gradle's + * logic for classpath isolation is not enough. + */ + ProcessMode inProcess = IN_PROCESS + + /** Set the mode for running conversions sequential or in parallel. + * For instance a task that has multiple backends can have the + * conversion in parallel. + * + * When running sequential, the worker classloader, Asciidoctor instances + * and Asciidoctor extensions will be shared across all of the conversions. + * When running parallel each conversion will be in a separate classloader, + * with a new Asciidoctor instance being initialised for every conversion. + * + * Sequential work might execute slightly faster, but if you have backend-specific + * extensions you might want to consider parallel mode (or use another Asciidoctor + * task instance). + * + * Default is parallel. + * + * WHen {@link #inProcess} {@code ==} {@link JAVA_EXEC} this option is ignored. + */ + boolean parallelMode = true + + /** Sets the new Asciidoctor parent source directory. + * + * @param f Any object convertible with {@code project.file}. + */ + void setSourceDir(Object f) { + this.srcDir = f + } + + /** Returns the parent directory for Asciidoctor source. + */ + File getSourceDir() { + project.file(srcDir) + } + + /** Configures sources. + * + * @param cfg Configuration closure. Is passed a {@link PatternSet}. + */ + void sources(final Closure cfg) { + if (sourceDocumentPattern == null) { + sourceDocumentPattern = new PatternSet() + } + Closure configuration = (Closure) cfg.clone() + configuration.delegate = sourceDocumentPattern + configuration() + } + + /** Configures sources. + * + * @param cfg Configuration {@link Action}. Is passed a {@link PatternSet}. + */ + void sources(final Action cfg) { + if (sourceDocumentPattern == null) { + sourceDocumentPattern = new PatternSet() + } + cfg.execute(sourceDocumentPattern) + } + + /** 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. + * + * @since 1.5.1 + */ + @InputFiles + @SkipWhenEmpty + FileTree getSourceFileTree() { + getSourceFileTreeFrom(sourceDir) + } + + /** Clears any of the existing secondary soruces patterns. + * + * This should be used if none of the default patterns should be monitored. + */ + void clearSecondarySources() { + secondarySourceDocumentPattern = new PatternSet() + } + + /** Configures secondary sources. + * + * @param cfg Configuration closure. Is passed a {@link PatternSet}. + */ + void secondarySources(final Closure cfg) { + if (secondarySourceDocumentPattern == null) { + sourceDocumentPattern = defaultSecondarySourceDocumentPattern + } + Closure configuration = (Closure) cfg.clone() + configuration.delegate = secondarySourceDocumentPattern + configuration() + } + + /** Configures sources. + * + * @param cfg Configuration {@link Action}. Is passed a {@link PatternSet}. + */ + void secondarySources(final Action cfg) { + if (secondarySourceDocumentPattern == null) { + sourceDocumentPattern = defaultSecondarySourceDocumentPattern + } + cfg.execute(secondarySourceDocumentPattern) + } + + /** Returns a FileTree containing all of the secondary source documents. + * + * @return Collection of secondary files + * + */ + @InputFiles + FileTree getSecondarySourceFileTree() { + getSecondarySourceFileTreeFrom(sourceDir) + } + + /** Returns the current toplevel output directory + * + */ + @OutputDirectory + File getOutputDir() { + this.outDir != null ? project.file(this.outDir) : null + } + + /** Sets the new Asciidoctor parent output directory. + * + * @param f An object convertible via {@code project.file} + */ + void setOutputDir(Object f) { + this.outDir = f + } + + /** Returns a list of all output directories by backend + * + * @since 1.5.1 + */ + @OutputDirectories + Set getBackendOutputDirectories() { + configuredOutputOptions.backends.collect { getOutputDirFor(it) } as Set + } + + /** Base directory (current working directory) for a conversion. + * + * @return Base directory. + */ + @InputDirectory + File getBaseDir() { + this.baseDir != null ? project.file(this.baseDir) : project.projectDir + } + + /** Sets the base directory for a conversion. + * + * The base directory is used by AsciidoctorJ to set a curretn working directory for + * a conversion. + * + * If never set, then {@code project.projectDir ( )} will be assumed to be the base directory. + * + * WHen {@link} + * + * @param f Base directory + */ + void setBaseDir(Object f) { + this.baseDir = f + } + + /** Returns all of the Asciidoctor options. + * + * This is equivalent of using {@code asciidoctorj.getOptions} + * + */ + @Input + Map getOptions() { + asciidoctorj.options + } + + /** Apply a new set of Asciidoctor options, clearing any options previously set. + * + * If set here all global Asciidoctor options are ignored within this task. + * + * This is equivalent of using {@code asciidoctorj.setOptions}. + * + * @param m Map with new options + */ + void setOptions(Map m) { + asciidoctorj.options = m + } + + /** Add additional asciidoctor options + * + * If set here these options will be used in addition to any global Asciidoctor options. + * + * This is equivalent of using {@code asciidoctorj.options}. + * + * @param m Map with new options + */ + @SuppressWarnings('ConfusingMethodName') + void options(Map m) { + asciidoctorj.options(m) + } + + /** Returns all of the Asciidoctor options. + * + * This is equivalent of using {@code asciidoctorj.getAttributes} + * + */ + @Input + Map getAttributes() { + asciidoctorj.attributes + } + + /** Apply a new set of Asciidoctor options, clearing any options previously set. + * + * If set here all global Asciidoctor options are ignored within this task. + * + * This is equivalent of using {@code asciidoctorj.setAttributes}. + * + * @param m Map with new options + */ + void setAttributes(Map m) { + asciidoctorj.attributes = m + } + + /** Add additional asciidoctor options + * + * If set here these options will be used in addition to any global Asciidoctor options. + * + * This is equivalent of using {@code asciidoctorj.attributes}. + * + * @param m Map with new options + */ + void attributes(Map m) { + asciidoctorj.attributes(m) + } + + /** 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 {@link CopySpec} runConfiguration closure + * @since 1.5.1 + */ + void resources(Closure cfg) { + if (this.resourceCopy == null) { + this.resourceCopy = project.copySpec(cfg) + } else { + Closure configuration = (Closure) cfg.clone() + configuration.delegate = this.resourceCopy + 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 {@link CopySpec} runConfiguration {@link Action} + */ + void resources(Action cfg) { + if (this.resourceCopy == null) { + this.resourceCopy = project.copySpec(cfg) + } else { + cfg.execute(this.resourceCopy) + } + } + + /** Copies all resources to the output directory. + * + * Some backends (such as {@code html5}) require all resources to be copied to the output directory. + * This is the default behaviour for this task. + */ + void copyAllResources() { + this.copyResourcesForBackends = [] + } + + /** Do not copy any resources to the output directory. + * + * Some backends (such as {@code pdf}) process all resources in place. + * + */ + void copyNoResources() { + this.copyResourcesForBackends = null + } + + /** Copy resources to the output directory only if the backend names matches any of the specified + * names. + * + * @param backendNames List of names for which resources should be copied. + * + */ + void copyResourcesOnlyIf(String... backendNames) { + this.copyResourcesForBackends = [] + this.copyResourcesForBackends.addAll(backendNames) + } + + /** Returns all of the specified configurations as a collections of files. + * + * @return FileCollection + */ + @InputFiles + @SuppressWarnings('Instanceof') + FileCollection getConfigurations() { + FileCollection fc = asciidoctorj.configuration + this.asciidocConfigurations.each { + if (it instanceof Configuration) { + fc = fc + (FileCollection) it + } else { + fc = fc + (FileCollection) (project.configurations.getByName(StringUtils.stringize(it))) + } + } + fc + } + + /** Override any existing configurations except the ones available via the {@code asciidoctorj} task extension. + * + * @param configs + */ + void setConfigurations(Iterable configs) { + this.asciidocConfigurations.clear() + configurations(configs) + } + + /** Add additional configurations. + * + * @param configs Instances of {@link Configuration} or anythign convertible to a string than can be used + * as a name of a runConfiguration. + */ + void configurations(Iterable configs) { + this.asciidocConfigurations.addAll(configs) + } + + /** Add additional configurations. + * + * @param configs Instances of {@link Configuration} or anythign convertible to a string than can be used + * as a name of a runConfiguration. + */ + void configurations(Object... configs) { + this.asciidocConfigurations.addAll(configs) + } + + /** Some extensionRegistry such as {@code ditaa} creates images in the source directory. + * + * Use this setting to copy all sources and resources to an intermediate work directory + * before processing starts. This will keep the source directory pristine + */ + void useIntermediateWorkDir() { + withIntermediateWorkDir = true + } + + @SuppressWarnings('UnnecessaryGetter') + @TaskAction + void processAsciidocSources() { + + checkForInvalidSourceDocuments() + checkForIncompatiblePathRoots() + + File workingSourceDir + FileTree sourceTree + + if (this.withIntermediateWorkDir) { + File tmpDir = getIntermediateWorkDir() + prepareTempWorkspace(tmpDir) + workingSourceDir = tmpDir + sourceTree = getSourceFileTreeFrom(tmpDir) + } else { + workingSourceDir = getSourceDir() + sourceTree = getSourceFileTree() + } + + Set sourceFiles = sourceTree.files + + + if (inProcess != JAVA_EXEC) { + if(GradleVersion.current() < GradleVersion.version(('4.3'))) { + logger.warn 'Gradle API classpath leakage will cause issues with Gradle < 4.3. Running with JAVA_EXEC instead.' + runWithJavaExec(workingSourceDir, sourceFiles) + } else { + runWithWorkers(workingSourceDir, sourceFiles) + } + } else { + runWithJavaExec(workingSourceDir, sourceFiles) + } + } + + /** Initialises the core an Asciidoctor task + * + * @param we {@link WorkerExecutor}. This is usually injected into the + * constructor of the subclass. + */ + protected AbstractAsciidoctorTask(WorkerExecutor we) { + worker = we + asciidoctorj = extensions.create(AsciidoctorJExtension.NAME, AsciidoctorJExtension, this) + + addInputProperty 'required-ruby-modules', { asciidoctorj.requires } + addInputProperty 'gemPath', { asciidoctorj.asGemPath() } + addInputProperty 'verboseMode', { asciidoctorj.verboseMode } + inputs.files { asciidoctorj.gemPaths } + + // TODO: This is nasty - needing an org.asciidoctor.gradle.jvm.epub.internal API to solve a problem + inputs.files { (resourceCopySpec as CopySpecInternal).buildRootResolver().allSource } + } + + /** Retunns all of the executor configrations for this task + * + * @return Executor configuratiosn + */ + protected Map getExecutorConfigurations( + final File workingSourceDir, + final Set sourceFiles + ) { + configuredOutputOptions.backends.collectEntries { String activeBackend -> + ["backend=${activeBackend}".toString(), getExecutorConfigurationFor(activeBackend, workingSourceDir, sourceFiles)] + } + } + + /** Provides configuration information for the worker. + * + * @param backendName Name of backend that will be run. + * @param workingSourceDir Source directory that will used for work. This can be + * the original source directory or an intermediate. + * @param sourceFiles THe actual top-level source files that will be used as entry points + * for generating documentation. + * @return Executor configuration + */ + protected ExecutorConfiguration getExecutorConfigurationFor( + final String backendName, + final File workingSourceDir, + final Set sourceFiles + ) { + File outputTo = getOutputDirFor(backendName) + + new ExecutorConfiguration( + sourceDir: workingSourceDir, + sourceTree: sourceFiles, + outputDir: outputTo, + baseDir: getBaseDir(), + projectDir: project.projectDir, + rootDir: project.rootProject.projectDir, + options: options, + attributes: attributes, + backendName: backendName, + logDocuments: logDocuments, + gemPath: gemPath, + asciidoctorExtensions: asciidoctorJExtensions, + requires: requires, + copyResources: this.copyResourcesForBackends != null && (this.copyResourcesForBackends.empty || backendName in this.copyResourcesForBackends), + safeModeLevel: asciidoctorj.safeMode.level + ) + } + + /** 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 + * + * @since 1.5.1 + */ + protected PatternSet getDefaultSourceDocumentPattern() { + PatternSet ps = new PatternSet() + ps.include '**/*.adoc' + ps.include '**/*.ad' + ps.include '**/*.asc' + ps.include '**/*.asciidoc' + ps.exclude '**/_*' + } + + /** The default pattern set for secondary sources. + * + * @return {@link #getDefaultSourceDocumentPattern} + `*docinfo*`. + */ + protected PatternSet getDefaultSecondarySourceDocumentPattern() { + PatternSet ps = defaultSourceDocumentPattern + ps.include '*docinfo*' + } + + /** 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 {@link CopySpec}. Never {@code null}. + */ + @CompileDynamic + protected 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 {@link CopySpec}. Never {@code null}. + */ + protected CopySpec getResourceCopySpec() { + this.resourceCopy ?: defaultResourceCopySpec + } + + /** Returns all of the associated extensionRegistry. + * + * @return AsciidoctorJ extensionRegistry + */ + protected List getAsciidoctorJExtensions() { + asciidoctorj.extensions + } + + /** Obtains a source tree based on patterns. + * + * @param dir Toplevel source directory. + * @return Source tree based upon configured pattern. + */ + protected FileTree getSourceFileTreeFrom(File dir) { + project.fileTree(dir). + matching(this.sourceDocumentPattern ?: defaultSourceDocumentPattern) + } + + /** Obtains a secondary source tree based on patterns. + * + * @param dir Toplevel source directory. + * @return Source tree based upon configured pattern. + */ + protected FileTree getSecondarySourceFileTreeFrom(File dir) { + project.fileTree(dir). + matching(this.secondarySourceDocumentPattern ?: defaultSecondarySourceDocumentPattern) + } + + /** Get the output directory for a specific backend. + * + * @param backendName Name of backend + * @return Output directory. + */ + protected File getOutputDirFor(final String backendName) { + if (outputDir == null) { + throw new GradleException("outputDir has not been defined for task '${name}'") + } + configuredOutputOptions.separateOutputDirs ? new File(outputDir, backendName) : outputDir + } + + /** Configure Java fork options prior to execution + * + * The default method does nothing. It is up to derived classes to implement appropriate behaviour. + * + * @param pfo Fork options to be configured. + */ + @SuppressWarnings('UnusedMethodParameter') + protected void configureForkOptions(JavaForkOptions pfo) { + } + + /** Adds an input property. + * + * Serves as a proxy method in order to deal with the API differences between Gradle 4.0-4.2 and 4.3 + * + * @param propName Name of property + * @param value Value of the input property + */ + @CompileDynamic + protected void addInputProperty(String propName, Object value) { + inputs.property propName, value + } + + private void checkForInvalidSourceDocuments() { + if (!sourceFileTree.filter { File f -> + f.name.startsWith('_') + }.empty) { + throw new InvalidUserDataException('Source documents may not start with an underscore') + } + } + + @CompileDynamic + private void prepareTempWorkspace(final File tmpDir) { + if (tmpDir.exists()) { + tmpDir.deleteDir() + } + tmpDir.mkdirs() + project.copy { + into tmpDir + from sourceFileTree + with resourceCopySpec + } + } + + private File getIntermediateWorkDir() { + project.file("${project.buildDir}/tmp/${FileUtils.toSafeFileName(this.name)}.intermediate") + } + + private void checkForIncompatiblePathRoots() { + if (outputDir == null) { + throw new GradleException("outputDir has not been defined for task '${name}'") + } + + Path sourceRoot = sourceDir.toPath().root + Path baseRoot = getBaseDir().toPath().root + Path outputRoot = outputDir.toPath().root + + if (sourceRoot != baseRoot || outputRoot != baseRoot) { + throw new AsciidoctorExecutionException('sourceDir, outputDir and baseDir needs to have the same root filesystem for AsciidoctorJ to function correctly. This is typically caused on Winwdows where everything is not on the same drive letter.') + } + } + + private File getVerboseScriptModule() { + project.file("${project.buildDir}/tmp/${FileUtils.toSafeFileName(name)}/${FileUtils.toSafeFileName(name)}-verbose-mode.rb") + } + + private Set getRequires() { + if (asciidoctorj.verboseMode) { + File verbose = verboseScriptModule + verbose.parentFile.mkdirs() + verbose.text = '$VERBOSE = true' + Set newRequires = [] + newRequires.addAll(asciidoctorj.requires) + newRequires.add(project.file(verbose).absolutePath) + newRequires + } else { + asciidoctorj.requires + } + } + + private String getGemPath() { + asciidoctorj.asGemPath() + } + + private void runWithWorkers(final File workingSourceDir, final Set sourceFiles) { + FileCollection asciidoctorClasspath = configurations + logger.debug "Running AsciidoctorJ with workers. Classpath = ${asciidoctorClasspath.files}" + if (parallelMode) { + getExecutorConfigurations(workingSourceDir, sourceFiles).each { String configName, ExecutorConfiguration executorConfiguration -> + worker.submit(AsciidoctorJExecuter) { WorkerConfiguration config -> + config.isolationMode = inProcess == IN_PROCESS ? CLASSLOADER : PROCESS + config.classpath = asciidoctorClasspath + config.displayName = "Asciidoctor (task=${name}) conversion for ${configName}" + config.params( + new ExecutorConfigurationContainer(executorConfiguration), + asciidoctorClasspath.files + ) + configureForkOptions(config.forkOptions) + } + } + } else { + Map executorConfigurations = getExecutorConfigurations(workingSourceDir, sourceFiles) + worker.submit(AsciidoctorJExecuter) { WorkerConfiguration config -> + config.isolationMode = inProcess ? CLASSLOADER : PROCESS + config.classpath = asciidoctorClasspath + config.displayName = "Asciidoctor (task=${name}) conversions for ${executorConfigurations.keySet().join(', ')}" + config.params( + new ExecutorConfigurationContainer(executorConfigurations.values()), + asciidoctorClasspath.files + ) + configureForkOptions(config.forkOptions) + } + } + } + + private void runWithJavaExec(final File workingSourceDir, final Set sourceFiles) { + FileCollection asciidoctorClasspath = configurations + + File entryPoint = new File(AsciidoctorJavaExec.protectionDomain.codeSource.location.toURI()).absoluteFile + File groovyJar = new File(GroovyObject.protectionDomain.codeSource.location.toURI()).absoluteFile + FileCollection javaExecClasspath = project.files(entryPoint, groovyJar, asciidoctorClasspath) + + File execConfigurationData = project.file("${project.buildDir}/tmp/${FileUtils.toSafeFileName(this.name)}.javaexec-data") + execConfigurationData.parentFile.mkdirs() + execConfigurationData.withOutputStream { fout -> + new ObjectOutputStream(fout).withCloseable { oos -> + oos.writeObject( + new ExecutorConfigurationContainer(getExecutorConfigurations(workingSourceDir, sourceFiles).values()) + ) + } + } + + logger.debug("Serialised AsciidoctorJ configuration to ${execConfigurationData}") + logger.debug "Running AsciidoctorJ instance with classpath ${javaExecClasspath.files}" + + project.javaexec { JavaExecSpec jes -> + configureForkOptions(jes) + logger.debug "Running AsciidoctorJ instance with environment: ${jes.environment}" + jes.with { + main = AsciidoctorJavaExec.canonicalName + classpath = javaExecClasspath + args execConfigurationData.absolutePath + } + } + } +} diff --git a/asciidoctor-gradle-jvm/src/main/groovy/org/asciidoctor/gradle/jvm/AsciidoctorExecutionException.groovy b/asciidoctor-gradle-jvm/src/main/groovy/org/asciidoctor/gradle/jvm/AsciidoctorExecutionException.groovy new file mode 100644 index 000000000..7c16d22cd --- /dev/null +++ b/asciidoctor-gradle-jvm/src/main/groovy/org/asciidoctor/gradle/jvm/AsciidoctorExecutionException.groovy @@ -0,0 +1,19 @@ +package org.asciidoctor.gradle.jvm + +import groovy.transform.CompileStatic +import org.gradle.api.GradleException + +/** + * @since 1.6.0 + */ +@CompileStatic +class AsciidoctorExecutionException extends GradleException { + + AsciidoctorExecutionException(final String msg) { + super(msg) + } + + AsciidoctorExecutionException(final String msg, Throwable t) { + super(msg, t) + } +} diff --git a/asciidoctor-gradle-jvm/src/main/groovy/org/asciidoctor/gradle/jvm/AsciidoctorJBasePlugin.groovy b/asciidoctor-gradle-jvm/src/main/groovy/org/asciidoctor/gradle/jvm/AsciidoctorJBasePlugin.groovy new file mode 100644 index 000000000..99452b5d6 --- /dev/null +++ b/asciidoctor-gradle-jvm/src/main/groovy/org/asciidoctor/gradle/jvm/AsciidoctorJBasePlugin.groovy @@ -0,0 +1,35 @@ +/* + * Copyright 2013-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.asciidoctor.gradle.jvm + +import groovy.transform.CompileStatic +import org.gradle.api.Plugin +import org.gradle.api.Project + +/** + * @since 1.6.0 + */ +@CompileStatic +class AsciidoctorJBasePlugin implements Plugin { + + void apply(Project project) { + project.with { + apply plugin : 'org.asciidoctor.base' + + extensions.create AsciidoctorJExtension.NAME, AsciidoctorJExtension, project + } + } +} diff --git a/asciidoctor-gradle-jvm/src/main/groovy/org/asciidoctor/gradle/jvm/AsciidoctorJExtension.groovy b/asciidoctor-gradle-jvm/src/main/groovy/org/asciidoctor/gradle/jvm/AsciidoctorJExtension.groovy new file mode 100644 index 000000000..ab7420285 --- /dev/null +++ b/asciidoctor-gradle-jvm/src/main/groovy/org/asciidoctor/gradle/jvm/AsciidoctorJExtension.groovy @@ -0,0 +1,573 @@ +/* + * Copyright 2013-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.asciidoctor.gradle.jvm + +import groovy.transform.CompileStatic +import org.asciidoctor.gradle.base.SafeMode +import org.gradle.api.GradleException +import org.gradle.api.Project +import org.gradle.api.Task +import org.gradle.api.artifacts.Configuration +import org.gradle.api.artifacts.Dependency +import org.gradle.api.file.FileCollection +import org.gradle.util.GradleVersion +import org.ysb33r.grolifant.api.AbstractCombinedProjectTaskExtension +import org.ysb33r.grolifant.api.OperatingSystem + +import static org.ysb33r.grolifant.api.StringUtils.stringize + +/** Extension for configuring AsciidoctorJ. + * + * It can be used as both a project and a task extension. + * + * @since 1.6.0 + * @author Schalk W. Cronjé + */ +@CompileStatic +@SuppressWarnings('MethodCount') +class AsciidoctorJExtension extends AbstractCombinedProjectTaskExtension { + + // Be careful about modifiying the keyword ordering in these two lines. + // They are parsed by the build script yo set up some compilation dependencies. + final static String DEFAULT_ASCIIDOCTORJ_VERSION = '1.5.6' + final static String DEFAULT_GROOVYDSL_VERSION = '1.0.0.Alpha3' + final static String DEFAULT_PDF_VERSION = '1.5.0-alpha.11' + final static String DEFAULT_EPUB_VERSION = '1.5.0-alpha.8.1' + + static final String ASCIIDOCTORJ_CORE_DEPENDENCY = 'org.asciidoctor:asciidoctorj:' + static final String ASCIIDOCTORJ_GROOVY_DSL_DEPENDENCY = 'org.asciidoctor:asciidoctorj-groovy-dsl:' + static final String ASCIIDOCTORJ_PDF_DEPENDENCY = 'org.asciidoctor:asciidoctorj-pdf:' + static final String ASCIIDOCTORJ_EPUB_DEPENDENCY = 'org.asciidoctor:asciidoctorj-epub3:' + static final String JRUBY_COMPLETE_DEPENDENCY = 'org.jruby:jruby-complete:' + + final static String NAME = 'asciidoctorj' + + static final OperatingSystem OS = OperatingSystem.current() + + private Object version = DEFAULT_ASCIIDOCTORJ_VERSION + private Optional groovyDslVersion + private Optional pdfVersion + private Optional epubVersion + private Optional jrubyVersion + + private final Map options = [:] + private final Map attributes = [:] + private final List requires = [] + private final List asciidoctorExtensions = [] + private final List gemPaths = [] + + private boolean onlyTaskOptions = false + private boolean onlyTaskAttributes = false + private boolean onlyTaskRequires = false + private boolean onlyTaskExtensions = false + private boolean onlyTaskGems = false + + private Boolean verboseMode + + private SafeMode safeMode + + /** Attach extension to a project. + * + * @param project + */ + AsciidoctorJExtension(Project project) { + super(project) + + this.attributes['gradle-project-name'] = project.name + this.attributes['gradle-project-group'] = { project.group ?: '' } + this.attributes['gradle-project-version'] = { project.version ?: '' } + + this.safeMode = SafeMode.SAFE + this.verboseMode = false + this.groovyDslVersion = Optional.empty() + } + + /** Attach extension to a task. + * + * @param task + */ + AsciidoctorJExtension(Task task) { + super(task, NAME) + } + + /** Version of AsciidoctorJ that should be used. + * + */ + String getVersion() { + if (task) { + this.version ? stringize(this.version) : extFromProject.getVersion() + } else { + stringize(this.version) + } + } + + /** Set a new version to use. + * + * @param v New version to be used. Can be of anything that be be resolved by {@link stringize ( Object o } + */ + void setVersion(Object v) { + this.version = v + } + + /** Version of the Groovy Extension DSL that should be used. + * + * @return Version of extension DSL or {@code null} if extensions will not be used. + */ + String getGroovyDslVersion() { + if (task) { + if(this.groovyDslVersion != null && this.groovyDslVersion.present) { + stringize(this.groovyDslVersion.get()) + } else { + extFromProject.getGroovyDslVersion() + } + } else { + this.groovyDslVersion?.present ? stringize(this.groovyDslVersion.get()) : null + } + } + + /** Set a new Groovy DSL version to use + * + * @param v Groovy DSL version. + */ + void setGroovyDslVersion(Object v) { + this.groovyDslVersion = Optional.of(v) + } + + /** The version of JRuby to use. + * + * @return Version of JRuby to use or {@code null} is the the deafult version for AsciidoctorJ + * should be used. + */ + String getJrubyVersion() { + if (task) { + if(this.jrubyVersion != null && this.jrubyVersion.present) { + stringize(this.jrubyVersion.get()) + } else { + extFromProject.getJrubyVersion() + } + } else { + this.jrubyVersion?.present ? stringize(this.jrubyVersion.get()) : null + } + } + + /** Set a version of JRuby to use. + * + * @param v JRuby version + */ + void setJrubyVersion(Object v) { + this.jrubyVersion = Optional.of(v) + } + + /** Version of the Asciidoctor PDF that should be used. + * + * @return Version of Asciidoctor PDF or {@code null} if PDF conversion is not used. + */ + String getPdfVersion() { + if (task) { + if(this.pdfVersion != null && this.pdfVersion.present) { + stringize(this.pdfVersion.get()) + } else { + extFromProject.getPdfVersion() + } + } else { + this.pdfVersion?.present ? stringize(this.pdfVersion.get()) : null + } + } + + /** Set a new asciidoctor PDF version to use + * + * @param v Asciidoctor PDF version. + */ + void setPdfVersion(Object v) { + this.pdfVersion = Optional.of(v) + } + + /** Version of the Asciidoctor EPUB that should be used. + * + * @return Version of Asciidoctor EPUB or {@code null} if EPUB conversion is not used. + */ + String getEpubVersion() { + if (task) { + if(this.epubVersion != null && this.epubVersion.present) { + stringize(this.epubVersion.get()) + } else { + extFromProject.getEpubVersion() + } + } else { + this.epubVersion?.present ? stringize(this.epubVersion.get()) : null + } + } + + /** Set a new asciidoctor EPUB version to use + * + * @param v Asciidoctor EPUB version. + */ + void setEpubVersion(Object v) { + this.epubVersion = Optional.of(v) + } + + /** Returns all of the Asciidoctor options. + * + * @return Map with all scalars converted to strings. items in containers are + * also converted to strings but the contianer stucture is maintained. + */ + Map getOptions() { + stringizeMapRecursive(this.options, onlyTaskOptions) { AsciidoctorJExtension it -> + it.options + } + } + + /** Apply a new set of Asciidoctor options, clearing any options previously set. + * + * This can be set globally for all Asciidoctor tasks in a project. If this is set in a task + * it will override the global options. + * + * @param m Map with new options + */ + void setOptions(Map m) { + checkForAttributesInOptions(m) + this.options.clear() + this.options.putAll(m) + + if (task) { + onlyTaskOptions = true + } + } + + /** Add additional Asciidoctor options + * + * This can be set globally for all Asciidoctor tasks in a project. If this is set in a task + * it will use this options in the task in addition to any global options. + * + * @param m Map with new options + */ + @SuppressWarnings('ConfusingMethodName') + void options(Map m) { + checkForAttributesInOptions(m) + this.options.putAll(m) + } + + /** Returns all of the Asciidoctor options. + * + */ + Map getAttributes() { + stringizeMapRecursive(this.attributes, onlyTaskOptions) { AsciidoctorJExtension it -> + it.attributes + } + } + + /** Apply a new set of Asciidoctor attributes, clearing any attributes previously set. + * + * This can be set globally for all Asciidoctor tasks in a project. If this is set in a task + * it will override the global attributes. + * + * @param m Map with new options + */ + void setAttributes(Map m) { + this.attributes.clear() + this.attributes.putAll(m) + + if (task) { + onlyTaskAttributes = true + } + } + + /** Add additional Asciidoctor attributes. + * + * This can be set globally for all Asciidoctor tasks in a project. If this is set in a task + * it will use this attributes in the task in addition to any global attributes. + * + * @param m Map with new options + */ + @SuppressWarnings('ConfusingMethodName') + void attributes(Map m) { + this.attributes.putAll(m) + } + + /** Returns the set of Ruby modules to be included. + * + * @since 1.5.0 + */ + Set getRequires() { + stringizeList(this.requires, onlyTaskRequires) { AsciidoctorJExtension it -> + it.requires.toList() + }.toSet() + } + + /** Applies a new set of Ruby modules to be included, clearing any previous set. + * + * If this is called on a task extension, the global project requires will be ignored. + * + * @param b One or more ruby modules to be included + */ + void setRequires(Object... b) { + this.requires.clear() + this.requires.addAll(b) + + if (task) { + onlyTaskRequires = true + } + } + + /** 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) { + this.requires.addAll(b) + } + + /** Returns the list of paths to be used for {@code GEM_HOME} + * + */ + FileCollection getGemPaths() { + if (!task || onlyTaskGems) { + project.files(this.gemPaths) + } else { + project.files(this.gemPaths) + extFromProject.gemPaths + } + } + + /** Sets a new list of GEM paths to be used. + * + * @param paths Paths resolvable by {@ocde project.files} + */ + void setGemPaths(Iterable paths) { + this.gemPaths.clear() + this.gemPaths.addAll(paths) + + if (task) { + this.onlyTaskGems = true + } + } + + /** Adds more paths for discovering GEMs. + * + * @param f Path objects that can be be converted with {@code project.file}. + */ + @SuppressWarnings('ConfusingMethodName') + void gemPaths(Object... f) { + this.gemPaths.addAll(f) + } + + /** 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() { + getGemPaths().files*.toString().join(OS.pathSeparator) + } + + /** Returns the ASciidoctor SafeMode under which a conversion will be run. + * + * @return Asciidoctor Safe Mode + */ + SafeMode getSafeMode() { + (task && this.safeMode || !task) ? this.safeMode : extFromProject.safeMode + } + + /** Set Asciidoctor safe mode. + * + * @param mode An instance of Asciidoctor SafeMode. + */ + void setSafeMode(SafeMode mode) { + this.safeMode = mode + } + + /** Set Asciidoctor safe mode. + * + * @param mode A valid integer representing a Safe Mode + */ + void setSafeMode(int mode) { + this.safeMode = SafeMode.safeMode(mode) + } + + /** Set Asciidoctor safe mode. + * + * @param mode A valid string representing a Safe Mode + */ + void setSafeMode(String mode) { + this.safeMode = SafeMode.valueOf(mode.toUpperCase()) + } + + /** Returns a runConfiguration of the configured AsciidoctorJ dependencies. + * + * @return A non-attached runConfiguration. + */ + Configuration getConfiguration() { + String gDslVer = getGroovyDslVersion() + String pdfVer = getPdfVersion() + String epubVer = getEpubVersion() + String jrubyVer = getJrubyVersion() + + List deps = [createDependency("${ASCIIDOCTORJ_CORE_DEPENDENCY}${getVersion()}")] + + if(gDslVer != null) { + deps.add(createDependency("${ASCIIDOCTORJ_GROOVY_DSL_DEPENDENCY}${gDslVer}")) + } + + if(pdfVer != null) { + deps.add(createDependency("${ASCIIDOCTORJ_PDF_DEPENDENCY}${pdfVer}")) + } + + if(epubVer != null) { + deps.add(createDependency("${ASCIIDOCTORJ_EPUB_DEPENDENCY}${epubVer}")) + } + + if(jrubyVer != null) { + deps.add(createDependency("${JRUBY_COMPLETE_DEPENDENCY}${jrubyVer}")) + } + + project.configurations.detachedConfiguration( + deps.toArray() as Dependency[] + ) + } + + /** Return extensionRegistry. + * + * These extensionRegistry are not registered at this call. That action is left + * to the specific task at i'ts execution time. + * + * @return + */ + List getExtensions() { + if (!task || onlyTaskExtensions) { + this.asciidoctorExtensions + } else if (this.asciidoctorExtensions.empty) { + extFromProject.extensions + } else { + extFromProject.extensions + this.asciidoctorExtensions + } + } + + /** Defines extensionRegistry. The given parameters should + * either contain Asciidoctor Groovy DSL closures or files + * with content conforming to the Asciidoctor Groovy DSL. + */ + void extensions(Object... exts) { + setDefaultGroovyDslVersionIfRequired() + asciidoctorExtensions.addAll(exts as List) + } + + /** Clears the existing extensionRegistry and replace with a new set. + * + * If this is declared on a task extension all extention from the global + * project extension will be ignored. + */ + void setExtensions(Iterable newExtensions) { + setDefaultGroovyDslVersionIfRequired() + asciidoctorExtensions.clear() + asciidoctorExtensions.addAll(newExtensions) + onlyTaskExtensions = true + } + + void setVerboseMode(boolean mode) { + this.verboseMode = mode + } + + boolean getVerboseMode() { + if(task) { + this.verboseMode == null ? extFromProject.verboseMode : this.verboseMode + } else { + this.verboseMode + } + } + + @SuppressWarnings('FactoryMethodName') + private Dependency createDependency(final String notation) { + project.dependencies.create(notation) + } + + private Collection stringizeList(Collection list, boolean fromTaskOnly, Closure> other) { + if (!task || fromTaskOnly) { + stringize(list) + } else if (list.isEmpty()) { + other.call(extFromProject) + } else { + List newOptions = [] + newOptions.addAll(other.call(extFromProject)) + newOptions.addAll(list) + stringize(newOptions) + } + } + + private Map stringizeMapRecursive(Map map, boolean fromTaskOnly, Closure> other) { + if (!task || fromTaskOnly) { + stringizeScalarMapItems(map) + } else if (map.isEmpty()) { + other.call(extFromProject) + } else { + Map newOptions = [:] + newOptions.putAll(other.call(extFromProject)) + newOptions.putAll(map) + stringizeScalarMapItems(newOptions) + } + } + + private List stringizeScalarListItems(List list) { + list.collect { item -> + switch (item) { + case List: + return stringizeScalarListItems((List) item) + case Map: + return stringizeScalarMapItems((Map) item) + case boolean: + case Boolean: + return (Boolean) item + case File: + return ((File) item).absolutePath + default: + return stringize(item) + } + } + } + + private Map stringizeScalarMapItems(Map map) { + map.collectEntries { String key, Object item -> + switch (item) { + case List: + return [key, stringizeScalarListItems((List) item)] + case Map: + return [key, stringizeScalarMapItems((Map) item)] + case boolean: + case Boolean: + return [key, ((Boolean) item)] + case File: + return [key, ((File) item).absolutePath] + default: + return [key, stringize(item)] + } + } as Map + } + + private AsciidoctorJExtension getExtFromProject() { + task ? (AsciidoctorJExtension) projectExtension : this + } + + private void checkForAttributesInOptions(Map m) { + if (m.containsKey('attributes')) { + throw new GradleException('Attributes found in options. Please use \'attributes\' method') + } + } + + private void setDefaultGroovyDslVersionIfRequired() { + if(getGroovyDslVersion() == null) { + setGroovyDslVersion DEFAULT_GROOVYDSL_VERSION + } + } + +} diff --git a/asciidoctor-gradle-jvm/src/main/groovy/org/asciidoctor/gradle/jvm/AsciidoctorJPdfPlugin.groovy b/asciidoctor-gradle-jvm/src/main/groovy/org/asciidoctor/gradle/jvm/AsciidoctorJPdfPlugin.groovy new file mode 100644 index 000000000..16d279621 --- /dev/null +++ b/asciidoctor-gradle-jvm/src/main/groovy/org/asciidoctor/gradle/jvm/AsciidoctorJPdfPlugin.groovy @@ -0,0 +1,44 @@ +/* + * Copyright 2013-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.asciidoctor.gradle.jvm + +import groovy.transform.CompileStatic +import org.gradle.api.Plugin +import org.gradle.api.Project + +/** Provides additional conventions for building PDFs. + * + *
    + *
  • Creates a task called {@code asciidoctorPdf}. + *
  • Sets a default version for asciidoctor-pdf. + *
+ * + * @since 1.6.0 + */ +@CompileStatic +class AsciidoctorJPdfPlugin implements Plugin { + + void apply(Project project) { + project.with { + apply plugin : 'org.asciidoctor.jvm.base' + + AsciidoctorPdfTask task = tasks.create('asciidoctorPdf', AsciidoctorPdfTask) + task.outputDir = { "${project.buildDir}/docs/asciidocPdf"} + extensions.getByType(AsciidoctorJExtension).pdfVersion = AsciidoctorJExtension.DEFAULT_PDF_VERSION + + } + } +} diff --git a/asciidoctor-gradle-jvm/src/main/groovy/org/asciidoctor/gradle/jvm/AsciidoctorJPlugin.groovy b/asciidoctor-gradle-jvm/src/main/groovy/org/asciidoctor/gradle/jvm/AsciidoctorJPlugin.groovy new file mode 100644 index 000000000..0f51956f6 --- /dev/null +++ b/asciidoctor-gradle-jvm/src/main/groovy/org/asciidoctor/gradle/jvm/AsciidoctorJPlugin.groovy @@ -0,0 +1,37 @@ +/* + * Copyright 2013-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.asciidoctor.gradle.jvm + +import groovy.transform.CompileStatic +import org.gradle.api.Plugin +import org.gradle.api.Project + +/** + * @since 1.6.0 + */ +@CompileStatic +class AsciidoctorJPlugin implements Plugin { + + void apply(Project project) { + project.with { + apply plugin : 'org.asciidoctor.jvm.base' + + tasks.create('asciidoctor', AsciidoctorTask) { + + } + } + } +} diff --git a/asciidoctor-gradle-jvm/src/main/groovy/org/asciidoctor/gradle/jvm/AsciidoctorPdfTask.groovy b/asciidoctor-gradle-jvm/src/main/groovy/org/asciidoctor/gradle/jvm/AsciidoctorPdfTask.groovy new file mode 100644 index 000000000..8ba6248c6 --- /dev/null +++ b/asciidoctor-gradle-jvm/src/main/groovy/org/asciidoctor/gradle/jvm/AsciidoctorPdfTask.groovy @@ -0,0 +1,33 @@ +package org.asciidoctor.gradle.jvm + +import groovy.transform.CompileStatic +import org.gradle.api.tasks.util.PatternSet +import org.gradle.workers.WorkerExecutor + +import javax.inject.Inject + +/** + * @since 1.6.0 + */ +@CompileStatic +class AsciidoctorPdfTask extends AbstractAsciidoctorTask { + + @Inject + AsciidoctorPdfTask(WorkerExecutor we) { + super(we) + + configuredOutputOptions.backends = ['pdf'] + copyNoResources() + } + + /** The default pattern set for secondary sources. + * + * @return {@link #getDefaultSourceDocumentPattern} + `*docinfo*`. + */ + @Override + protected PatternSet getDefaultSecondarySourceDocumentPattern() { + PatternSet ps = defaultSourceDocumentPattern + ps.include '*-theme.ym*l' + ps + } +} diff --git a/asciidoctor-gradle-jvm/src/main/groovy/org/asciidoctor/gradle/jvm/AsciidoctorTask.groovy b/asciidoctor-gradle-jvm/src/main/groovy/org/asciidoctor/gradle/jvm/AsciidoctorTask.groovy new file mode 100755 index 000000000..b8360ea8b --- /dev/null +++ b/asciidoctor-gradle-jvm/src/main/groovy/org/asciidoctor/gradle/jvm/AsciidoctorTask.groovy @@ -0,0 +1,88 @@ +/* + * Copyright 2013-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.asciidoctor.gradle.jvm + +import groovy.transform.CompileStatic +import org.gradle.api.Action +import org.gradle.workers.WorkerExecutor +import org.ysb33r.grolifant.api.FileUtils + +import javax.inject.Inject + +import static groovy.lang.Closure.DELEGATE_FIRST + +/** Standard generic task for converting Asciidoctor documents. + * + * For convention and cmopatibility with older releases of this pugin, the default source directory + * is {@code "src/docs/asciidoc"} if the task is named {@code aciidoctor}. For all other instances + * of this task type the default source directory is {@code "src/docs/${task.name.capitalize()}"}. + * + * In a similar fasion the default output directory is either {@code "${buildDir}/asciidoc"} or + * {@code "${buildDir}/asciidoc${task.name.capitalize()}"}. + * + * @author Noam Tenne + * @author Andres Almiray + * @author Tom Bujok + * @author Lukasz Pielak + * @author Dmitri Vyazelenko + * @author Benjamin Muschko + * @author Dan Allen + * @author Rob Winch + * @author Stefan Schlott + * @author Stephan Classen + * @author Marcus Fihlon + * @author Schalk W. Cronjé + * @author Robert Panzer + */ +@SuppressWarnings(['MethodCount', 'Instanceof']) +@CompileStatic +class AsciidoctorTask extends AbstractAsciidoctorTask { + + /** Configures output options for this task. + * + * @param cfg Closure which will delegate to a {@link OutputOptions} instance. + */ + void outputOptions(Closure cfg) { + Closure configurator = (Closure) cfg.clone() + configurator.delegate = this.configuredOutputOptions + configurator.resolveStrategy = DELEGATE_FIRST + configurator.call() + } + + /** Configures output options for this task. + * + * @param cfg Action which will be passed an instances of {@link OutputOptions} to configure. + */ + void outputOptions(Action cfg) { + cfg.execute(this.configuredOutputOptions) + } + + @Inject + AsciidoctorTask(WorkerExecutor we) { + super(we) + final String taskPrefix = 'asciidoctor' + String folderName + if (name.startsWith(taskPrefix)) { + folderName = name.replaceFirst(taskPrefix, 'asciidoc') + } else { + folderName = "asciidoc${name.capitalize()}" + } + final String safeFolderName = FileUtils.toSafeFileName(folderName) + sourceDir = "src/docs/${folderName}" + outputDir = { project.file("${project.buildDir}/docs/${safeFolderName}") } + } +} + diff --git a/asciidoctor-gradle-jvm/src/main/groovy/org/asciidoctor/gradle/jvm/OutputOptions.groovy b/asciidoctor-gradle-jvm/src/main/groovy/org/asciidoctor/gradle/jvm/OutputOptions.groovy new file mode 100644 index 000000000..a0512d6f6 --- /dev/null +++ b/asciidoctor-gradle-jvm/src/main/groovy/org/asciidoctor/gradle/jvm/OutputOptions.groovy @@ -0,0 +1,59 @@ +package org.asciidoctor.gradle.jvm + +import groovy.transform.CompileStatic +import org.gradle.api.tasks.Input + +/** + * @since 1.6.0 + */ +@CompileStatic +class OutputOptions { + + private Boolean separateOutputDirPerBackend + private final Set backends = [] + + /** If set to true each backend will be output to a separate subfolder below {@code outputDir} + * + * @since 1.5.1 + */ + @Input + @SuppressWarnings('UnnecessaryGetter') + boolean getSeparateOutputDirs() { + this.separateOutputDirPerBackend != null ? this.separateOutputDirPerBackend : (this.backends.size() > 1) + } + + /** Sets whether multiple output directories must be used. + * + * @param v {@code true} for multiple directories. + */ + @SuppressWarnings('UnnecessarySetter') + void setSeparateOutputDirs(boolean v) { + this.separateOutputDirPerBackend = v + } + + /** Returns the set of backends that was configured for this task + * + * @return Set of backends. + */ + Set getBackends() { + this.backends.empty ? ['html5'].toSet() : this.backends + } + + /** Sets a new set of backends. + * + * @param newBackends New collection of backends + */ + void setBackends(Iterable newBackends) { + this.backends.clear() + this.backends.addAll newBackends + } + + /** Adds additional backends. + * + * @param addBackends Additional backends + */ + @SuppressWarnings('ConfusingMethodName') + void backends(String... addBackends) { + this.backends.addAll addBackends + } +} diff --git a/asciidoctor-gradle-jvm/src/main/groovy/org/asciidoctor/gradle/jvm/ProcessMode.groovy b/asciidoctor-gradle-jvm/src/main/groovy/org/asciidoctor/gradle/jvm/ProcessMode.groovy new file mode 100644 index 000000000..118804e12 --- /dev/null +++ b/asciidoctor-gradle-jvm/src/main/groovy/org/asciidoctor/gradle/jvm/ProcessMode.groovy @@ -0,0 +1,27 @@ +package org.asciidoctor.gradle.jvm + +import groovy.transform.CompileStatic + +/** Ways of execuring Asciidoctor processes. + * + * @since 1.6.0 + * @author Schalk W. Cronjé + */ +@CompileStatic +enum ProcessMode { + + /** Use Gradle worker in-process. + * + */ + IN_PROCESS, + + /** Use out-of-process Gradle worker. + * + */ + OUT_OF_PROCESS, + + /** Use a classic out-of-process Java execution. + * + */ + JAVA_EXEC +} \ No newline at end of file diff --git a/asciidoctor-gradle-jvm/src/main/groovy/org/asciidoctor/gradle/remote/AsciidoctorJExecuter.groovy b/asciidoctor-gradle-jvm/src/main/groovy/org/asciidoctor/gradle/remote/AsciidoctorJExecuter.groovy new file mode 100644 index 000000000..6ab3fdd2d --- /dev/null +++ b/asciidoctor-gradle-jvm/src/main/groovy/org/asciidoctor/gradle/remote/AsciidoctorJExecuter.groovy @@ -0,0 +1,147 @@ +/* + * Copyright 2013-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.asciidoctor.gradle.remote + +import groovy.transform.CompileDynamic +import groovy.transform.CompileStatic +import groovy.util.logging.Slf4j +import org.asciidoctor.Asciidoctor +import org.asciidoctor.gradle.internal.ExecutorConfiguration +import org.asciidoctor.gradle.internal.ExecutorConfigurationContainer +import org.asciidoctor.groovydsl.AsciidoctorExtensions + +import javax.inject.Inject + +/** Actual executor used for running an Asciidoctorj instance. + * + * @since 1.6.0 + * @author Schalk W. Cronje + */ +@CompileStatic +@Slf4j +class AsciidoctorJExecuter extends ExecutorBase implements Runnable { + + private final Set asciidoctorClasspath + + @Inject + AsciidoctorJExecuter( + final ExecutorConfigurationContainer execConfig, + final Set asciidoctorClasspath + ) { + super(execConfig) + this.asciidoctorClasspath = asciidoctorClasspath + } + + @Override + @SuppressWarnings('CatchException') + void run() { + + if (runConfigurations.size() == 1) { + runSingle() + } else { + runMultiple() + } + } + + @SuppressWarnings('CatchThrowable') + private void runSingle() { + + ExecutorConfiguration runConfiguration = runConfigurations[0] + + Asciidoctor asciidoctor = Asciidoctor.Factory.create( + asciidoctorClassLoader, + runConfiguration.gemPath.empty ? null : runConfiguration.gemPath + ) + + runConfiguration.with { + for (require in requires) { + asciidoctor.requireLibrary(require) + } + + if (asciidoctorExtensions?.size()) { + registerExtensions(asciidoctor, asciidoctorExtensions) + } + } + + runConfiguration.outputDir.mkdirs() + + runConfiguration.sourceTree.each { File file -> + try { + if (runConfiguration.logDocuments) { + log.info("Converting ${file}") + } + asciidoctor.convertFile(file, normalisedOptionsFor(file, runConfiguration)) + } catch (Throwable t) { + throw new AsciidoctorRemoteExecutionException("Error running Asciidoctor whilst attempting to process ${file} using backend ${runConfiguration.backendName}", t) + } + } + } + + @SuppressWarnings('CatchThrowable') + private void runMultiple() { + + String combinedGemPath = runConfigurations*.gemPath.join(File.pathSeparator) + + ClassLoader adClassLoader = asciidoctorClassLoader + + Asciidoctor asciidoctor = (combinedGemPath.empty || combinedGemPath == ':') ? + Asciidoctor.Factory.create(adClassLoader) : + Asciidoctor.Factory.create(adClassLoader, combinedGemPath) + + runConfigurations.each { runConfiguration -> + for (require in runConfiguration.requires) { + asciidoctor.requireLibrary(require) + } + } + + runConfigurations.each { runConfiguration -> + if (runConfiguration.asciidoctorExtensions?.size()) { + registerExtensions(asciidoctor, runConfiguration.asciidoctorExtensions) + } + + runConfiguration.outputDir.mkdirs() + + runConfiguration.sourceTree.each { File file -> + try { + if (runConfiguration.logDocuments) { + log.info("Converting ${file}") + } + asciidoctor.convertFile(file, normalisedOptionsFor(file, runConfiguration)) + } catch (Throwable t) { + throw new AsciidoctorRemoteExecutionException("Error running Asciidoctor whilst attempting to process ${file} using backend ${runConfiguration.backendName}", t) + } + } + + asciidoctor.unregisterAllExtensions() + } + } + + @CompileDynamic + private void registerExtensions(Object asciidoctor, List exts) { + + AsciidoctorExtensions extensionRegistry = new AsciidoctorExtensions() + + for (Object ext in exts) { + extensionRegistry.addExtension(ext) + } + + extensionRegistry.registerExtensionsWith((Asciidoctor) asciidoctor) + } + + private ClassLoader getAsciidoctorClassLoader() { + this.class.classLoader + } +} diff --git a/asciidoctor-gradle-jvm/src/main/groovy/org/asciidoctor/gradle/remote/AsciidoctorJavaExec.groovy b/asciidoctor-gradle-jvm/src/main/groovy/org/asciidoctor/gradle/remote/AsciidoctorJavaExec.groovy new file mode 100644 index 000000000..7e8fbccff --- /dev/null +++ b/asciidoctor-gradle-jvm/src/main/groovy/org/asciidoctor/gradle/remote/AsciidoctorJavaExec.groovy @@ -0,0 +1,97 @@ +package org.asciidoctor.gradle.remote + +import groovy.transform.CompileDynamic +import groovy.transform.CompileStatic +import org.asciidoctor.Asciidoctor +import org.asciidoctor.gradle.internal.ExecutorConfiguration +import org.asciidoctor.gradle.internal.ExecutorConfigurationContainer +import org.asciidoctor.groovydsl.AsciidoctorExtensions + +/** Runs Asciidoctor as an externally invoked Java process. + * + * @since 1.6.0 + */ +@CompileStatic +class AsciidoctorJavaExec extends ExecutorBase { + + AsciidoctorJavaExec(ExecutorConfigurationContainer ecc) { + super(ecc) + } + + void run() { + + Asciidoctor asciidoctor = asciidoctorInstance + addRequires(asciidoctor) + + runConfigurations.each { runConfiguration -> + + if (runConfiguration.asciidoctorExtensions?.size()) { + registerExtensions(asciidoctor, runConfiguration.asciidoctorExtensions) + } + + runConfiguration.outputDir.mkdirs() + convertFiles(asciidoctor, runConfiguration) + asciidoctor.unregisterAllExtensions() + } + } + + @SuppressWarnings(['Println', 'CatchThrowable']) + private void convertFiles(Asciidoctor asciidoctor, ExecutorConfiguration runConfiguration) { + runConfiguration.sourceTree.each { File file -> + try { + if (runConfiguration.logDocuments) { + println("Converting ${file}") + } + asciidoctor.convertFile(file, normalisedOptionsFor(file, runConfiguration)) + } catch (Throwable t) { + throw new AsciidoctorRemoteExecutionException("Error running Asciidoctor whilst attempting to process ${file} using backend ${runConfiguration.backendName}", t) + } + } + } + + private Asciidoctor getAsciidoctorInstance() { + String combinedGemPath = runConfigurations*.gemPath.join(File.pathSeparator) + + ClassLoader adClassLoader = this.class.classLoader + + (combinedGemPath.empty || combinedGemPath == ':') ? + Asciidoctor.Factory.create(adClassLoader) : + Asciidoctor.Factory.create(adClassLoader, combinedGemPath) + } + + private void addRequires(Asciidoctor asciidoctor) { + runConfigurations.each { runConfiguration -> + for (require in runConfiguration.requires) { + asciidoctor.requireLibrary(require) + } + } + } + + @CompileDynamic + private void registerExtensions(Asciidoctor asciidoctor, List exts) { + + AsciidoctorExtensions extensionRegistry = new AsciidoctorExtensions() + + for (Object ext in exts) { + extensionRegistry.addExtension(ext) + } + + extensionRegistry.registerExtensionsWith((Asciidoctor) asciidoctor) + } + + static void main(String[] args) { + if (args.size() != 1) { + throw new AsciidoctorRemoteExecutionException('No serialised location specified') + } + + ExecutorConfigurationContainer ecc + new File(args[0]).withInputStream { input -> + new ObjectInputStream(input).withCloseable { ois -> + ecc = (ExecutorConfigurationContainer) ois.readObject() + } + } + + new AsciidoctorJavaExec(ecc).run() + } + +} diff --git a/asciidoctor-gradle-jvm/src/main/groovy/org/asciidoctor/gradle/remote/AsciidoctorRemoteExecutionException.groovy b/asciidoctor-gradle-jvm/src/main/groovy/org/asciidoctor/gradle/remote/AsciidoctorRemoteExecutionException.groovy new file mode 100644 index 000000000..81e9e47fe --- /dev/null +++ b/asciidoctor-gradle-jvm/src/main/groovy/org/asciidoctor/gradle/remote/AsciidoctorRemoteExecutionException.groovy @@ -0,0 +1,17 @@ +package org.asciidoctor.gradle.remote + +import groovy.transform.CompileStatic + +/** + * @since + */ +@CompileStatic +class AsciidoctorRemoteExecutionException extends Exception { + AsciidoctorRemoteExecutionException(String var1, Throwable var2) { + super(var1, var2) + } + + AsciidoctorRemoteExecutionException(String var1) { + super(var1) + } +} diff --git a/asciidoctor-gradle-jvm/src/main/groovy/org/asciidoctor/gradle/remote/ExecutorBase.groovy b/asciidoctor-gradle-jvm/src/main/groovy/org/asciidoctor/gradle/remote/ExecutorBase.groovy new file mode 100644 index 000000000..bdad7eea6 --- /dev/null +++ b/asciidoctor-gradle-jvm/src/main/groovy/org/asciidoctor/gradle/remote/ExecutorBase.groovy @@ -0,0 +1,70 @@ +package org.asciidoctor.gradle.remote + +import groovy.transform.CompileStatic +import org.asciidoctor.Options +import org.asciidoctor.gradle.internal.ExecutorConfiguration +import org.asciidoctor.gradle.internal.ExecutorConfigurationContainer + +/** Base class for building claspath-isolated executors for Asciidoctor. + * + * @since 2.6.0 + */ +@CompileStatic +class ExecutorBase { + + protected final List runConfigurations + + protected ExecutorBase(final ExecutorConfigurationContainer execConfig) { + this.runConfigurations = execConfig.configurations + } + + @SuppressWarnings('Instanceof') + protected + Map normalisedOptionsFor(final File file, ExecutorConfiguration runConfiguration) { + + Map mergedOptions = [:] + + runConfiguration.with { + mergedOptions.putAll(options) + mergedOptions.putAll([ + (Options.BACKEND) : backendName, + (Options.IN_PLACE): false, + (Options.SAFE) : safeModeLevel, + (Options.TO_DIR) : outputDir.absolutePath + ]) + + mergedOptions[Options.BASEDIR] = (baseDir ?: file.parentFile).absolutePath + + if (mergedOptions.containsKey(Options.TO_FILE)) { + Object toFileValue = mergedOptions[Options.TO_FILE] + Object toDirValue = mergedOptions.remove(Options.TO_DIR) + File toFile = toFileValue instanceof File ? (File) toFileValue : new File(toFileValue.toString()) + File toDir = toDirValue instanceof File ? (File) toDirValue : new File(toDirValue.toString()) + mergedOptions[Options.TO_FILE] = new File(toDir, toFile.name).absolutePath + } + + // Note: Directories passed as relative to work around issue #83 + // Asciidoctor cannot handle absolute paths in Windows properly + Map newAttrs = [:] + newAttrs.putAll(attributes) + newAttrs['gradle-projectdir'] = getRelativePath(projectDir, file.parentFile) + newAttrs['gradle-rootdir'] = getRelativePath(rootDir, file.parentFile) + mergedOptions[Options.ATTRIBUTES] = newAttrs + } + + mergedOptions + } + + /** + * Returns the path of one File relative to another. + * + * @param target the target directory + * @param base the base directory + * @return target's path relative to the base directory + * @throws IOException if an error occurs while resolving the files' canonical names + */ + protected String getRelativePath(File target, File base) throws IOException { + base.toPath().relativize(target.toPath()).toFile().toString() + } + +} diff --git a/asciidoctor-gradle-jvm/src/main/resources/META-INF/gradle-plugins/org.asciidoctor.jvm.base.properties b/asciidoctor-gradle-jvm/src/main/resources/META-INF/gradle-plugins/org.asciidoctor.jvm.base.properties index 0667647f4..908c9f800 100644 --- a/asciidoctor-gradle-jvm/src/main/resources/META-INF/gradle-plugins/org.asciidoctor.jvm.base.properties +++ b/asciidoctor-gradle-jvm/src/main/resources/META-INF/gradle-plugins/org.asciidoctor.jvm.base.properties @@ -14,4 +14,4 @@ # limitations under the License. # -implementation-class=org.asciidoctor.gradle.AsciidoctorJBasePlugin \ No newline at end of file +implementation-class=org.asciidoctor.gradle.jvm.AsciidoctorJBasePlugin \ No newline at end of file diff --git a/asciidoctor-gradle-jvm/src/main/resources/META-INF/gradle-plugins/org.asciidoctor.jvm.convert.properties b/asciidoctor-gradle-jvm/src/main/resources/META-INF/gradle-plugins/org.asciidoctor.jvm.convert.properties index 92613a60f..ffa1d796f 100644 --- a/asciidoctor-gradle-jvm/src/main/resources/META-INF/gradle-plugins/org.asciidoctor.jvm.convert.properties +++ b/asciidoctor-gradle-jvm/src/main/resources/META-INF/gradle-plugins/org.asciidoctor.jvm.convert.properties @@ -14,4 +14,4 @@ # limitations under the License. # -implementation-class=org.asciidoctor.gradle.AsciidoctorJPlugin \ No newline at end of file +implementation-class=org.asciidoctor.gradle.jvm.AsciidoctorJPlugin \ No newline at end of file diff --git a/asciidoctor-gradle-jvm/src/main/resources/META-INF/gradle-plugins/org.asciidoctor.jvm.pdf.properties b/asciidoctor-gradle-jvm/src/main/resources/META-INF/gradle-plugins/org.asciidoctor.jvm.pdf.properties new file mode 100644 index 000000000..45e3337ac --- /dev/null +++ b/asciidoctor-gradle-jvm/src/main/resources/META-INF/gradle-plugins/org.asciidoctor.jvm.pdf.properties @@ -0,0 +1,17 @@ +# +# Copyright 2013-2018 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +implementation-class=org.asciidoctor.gradle.jvm.AsciidoctorJPdfPlugin \ No newline at end of file diff --git a/asciidoctor-gradle-jvm/src/test/groovy/org/asciidoctor/gradle/AsciidoctorTaskSpec.groovy b/asciidoctor-gradle-jvm/src/test/groovy/org/asciidoctor/gradle/AsciidoctorTaskSpec.groovy deleted file mode 100755 index 8c2dd14bc..000000000 --- a/asciidoctor-gradle-jvm/src/test/groovy/org/asciidoctor/gradle/AsciidoctorTaskSpec.groovy +++ /dev/null @@ -1,1125 +0,0 @@ -/* - * Copyright 2013-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.asciidoctor.gradle - -import org.asciidoctor.SafeMode -import org.gradle.api.GradleException -import org.gradle.api.InvalidUserDataException -import org.gradle.api.Project -import org.gradle.api.Task -import org.gradle.api.internal.file.collections.SimpleFileCollection -import org.gradle.testfixtures.ProjectBuilder -import spock.lang.Ignore -import spock.lang.Specification - -/** - * Asciidoctor task specification - * - * @author Benjamin Muschko - * @author Stephan Classen - * @author Marcus Fihlon - * @author Schalk W. Cronjé - */ -class AsciidoctorTaskSpec extends Specification { - private static final String ASCIIDOCTOR = 'asciidoctor' - private static final String ASCIIDOC_RESOURCES_DIR = 'build/resources/test/src/asciidoc' - private static final String ASCIIDOC_RESOURCES_SUB_DIR = 'build/resources/test/src/asciidoc/subdir' - private static final String ASCIIDOC_BUILD_DIR = 'build/asciidoc' - private static final String ASCIIDOC_SAMPLE_FILE = 'sample.asciidoc' - private static final String ASCIIDOC_SAMPLE2_FILE = 'subdir/sample2.ad' - private static final String ASCIIDOC_INVALID_FILE = 'subdir/_include.adoc' - private static final DOCINFO_FILE_PATTERN = ~/^(.+\-)?docinfo(-footer)?\.[^.]+$/ - - Project project - AsciidoctorProxy mockAsciidoctor - ResourceCopyProxy mockCopyProxy - File testRootDir - File srcDir - File outDir - ByteArrayOutputStream systemOut - - PrintStream originSystemOut - - def setup() { - project = ProjectBuilder.builder().withName('test').build() - project.configurations.create(ASCIIDOCTOR) - mockAsciidoctor = Mock(AsciidoctorProxy) - mockCopyProxy = Mock(ResourceCopyProxy) - testRootDir = new File('.') - srcDir = new File(testRootDir, ASCIIDOC_RESOURCES_DIR).absoluteFile - outDir = new File(project.projectDir, ASCIIDOC_BUILD_DIR) - systemOut = new ByteArrayOutputStream() - originSystemOut = System.out; - System.out = new PrintStream(systemOut) - AsciidoctorTask.ASCIIDOCTORS.put(new AsciidoctorProxyCacheKey(gemPath:"", classpath: []), mockAsciidoctor) - } - - def cleanup(){ - System.out = originSystemOut - AsciidoctorTask.ASCIIDOCTORS.clear() - } - - @SuppressWarnings('MethodName') - def "Allow setting of options via method"() { - when: - Task task = project.tasks.create(name: ASCIIDOCTOR, type: AsciidoctorTask) { - options eruby : 'erb' - options eruby : 'erubis' - options doctype : 'book', toc : 'right' - } - - then: - ! systemOut.toString().contains('deprecated') - task.options['eruby'] == 'erubis' - task.options['doctype'] == 'book' - task.options['toc'] == 'right' - } - - @SuppressWarnings('MethodName') - def "Allow setting of options via assignment"() { - when: - Task task = project.tasks.create(name: ASCIIDOCTOR, type: AsciidoctorTask) { - options = [eruby : 'erb', toc : 'right'] - options = [eruby : 'erubis', doctype : 'book'] - } - - then: - ! systemOut.toString().contains('deprecated') - task.options['eruby'] == 'erubis' - task.options['doctype'] == 'book' - !task.options.containsKey('toc') - } - - @SuppressWarnings('MethodName') - def "Allow setting of attributes via method (Map variant)"() { - when: - Task task = project.tasks.create(name: ASCIIDOCTOR, type: AsciidoctorTask) { - attributes 'source-highlighter': 'foo' - attributes 'source-highlighter': 'coderay' - attributes idprefix : '$', idseparator : '-' - } - - then: - ! systemOut.toString().contains('deprecated') - task.attributes['source-highlighter'] == 'coderay' - task.attributes['idprefix'] == '$' - task.attributes['idseparator'] == '-' - } - - @SuppressWarnings('MethodName') - def "Allow setting of attributes via method (List variant)"() { - when: - Task task = project.tasks.create(name: ASCIIDOCTOR, type: AsciidoctorTask) { - attributes(['source-highlighter=foo', 'source-highlighter=coderay', 'idprefix=$', 'idseparator=-']) - } - - then: - ! systemOut.toString().contains('deprecated') - task.attributes['source-highlighter'] == 'coderay' - task.attributes['idprefix'] == '$' - task.attributes['idseparator'] == '-' - } - - @SuppressWarnings('MethodName') - def "Allow setting of attributes via method (String variant)"() { - when: - Task task = project.tasks.create(name: ASCIIDOCTOR, type: AsciidoctorTask) { - attributes 'source-highlighter=foo source-highlighter=coderay idprefix=$ idseparator=-' - } - - then: - ! systemOut.toString().contains('deprecated') - task.attributes['source-highlighter'] == 'coderay' - task.attributes['idprefix'] == '$' - task.attributes['idseparator'] == '-' - } - - @SuppressWarnings('MethodName') - def "Allow setting of attributes via assignment"() { - when: - Task task = project.tasks.create(name: ASCIIDOCTOR, type: AsciidoctorTask) { - attributes = ['source-highlighter': 'foo',idprefix : '$'] - attributes = ['source-highlighter': 'coderay', idseparator : '-'] - } - - then: - ! systemOut.toString().contains('deprecated') - task.attributes['source-highlighter'] == 'coderay' - task.attributes['idseparator'] == '-' - !task.attributes.containsKey('idprefix') - } - - @SuppressWarnings('MethodName') - def "Mixing attributes with options, should produce a warning, but updates should be appended"() { - when: - Task task = project.tasks.create(name: ASCIIDOCTOR, type: AsciidoctorTask) { - options eruby : 'erubis', attributes : ['source-highlighter': 'foo',idprefix : '$'] - options doctype: 'book', attributes : [idseparator : '-' ] - } - - then: - !task.attributes.containsKey('attributes') - task.attributes['source-highlighter'] == 'foo' - task.attributes['idseparator'] == '-' - task.attributes['idprefix'] == '$' - task.options['eruby'] == 'erubis' - task.options['doctype'] == 'book' - // @Ignore('Wrong sysout capture') - // systemOut.toString().contains('Attributes found in options.') - } - - @SuppressWarnings('MethodName') - def "Mixing attributes with options with assignment, should produce a warning, and attributes will be replaced"() { - when: - Map tmpStore = [ eruby : 'erubis', attributes : ['source-highlighter': 'foo',idprefix : '$'] ] - Task task = project.tasks.create(name: ASCIIDOCTOR, type: AsciidoctorTask) { - options = tmpStore - options = [ doctype: 'book', attributes : [idseparator : '-' ] ] - } - - then: - !task.attributes.containsKey('attributes') - task.attributes['idseparator'] == '-' - !task.attributes.containsKey('source-highlighter') - !task.attributes.containsKey('idprefix') - !task.options.containsKey('eruby') - task.options['doctype'] == 'book' - // @Ignore('Wrong sysout capture') - // systemOut.toString().contains('Attributes found in options.') - } - - @SuppressWarnings('MethodName') - def "Mixing string legacy form of attributes with options with assignment, should produce a warning, and attributes will be replaced"() { - when: - Task task = project.tasks.create(name: ASCIIDOCTOR, type: AsciidoctorTask) { - options = [ doctype: 'book', attributes : 'toc=right source-highlighter=coderay toc-title=Table\\ of\\ Contents' ] - } - - then: - task.options['doctype'] == 'book' - !task.attributes.containsKey('attributes') - task.attributes['toc'] == 'right' - task.attributes['source-highlighter'] == 'coderay' - task.attributes['toc-title'] == 'Table of Contents' - // @Ignore('Wrong sysout capture') - // systemOut.toString().contains('Attributes found in options.') - } - - @SuppressWarnings('MethodName') - def "Mixing list legacy form of attributes with options with assignment, should produce a warning, and attributes will be replaced"() { - when: - Task task = project.tasks.create(name: ASCIIDOCTOR, type: AsciidoctorTask) { - options = [ doctype: 'book', attributes : [ - 'toc=right', - 'source-highlighter=coderay', - 'toc-title=Table of Contents' - ]] - } - - then: - task.options['doctype'] == 'book' - !task.attributes.containsKey('attributes') - task.attributes['toc'] == 'right' - task.attributes['source-highlighter'] == 'coderay' - task.attributes['toc-title'] == 'Table of Contents' - // @Ignore('Wrong sysout capture') - // systemOut.toString().contains('Attributes found in options.') - } - - @SuppressWarnings('MethodName') - def "Allow setting of backends via method"() { - when: - Task task = project.tasks.create(name: ASCIIDOCTOR, type: AsciidoctorTask) { - backends 'foo','bar' - backends 'pdf' - } - - then: - ! systemOut.toString().contains('deprecated') - task.backends.contains('pdf') - task.backends.contains('foo') - task.backends.contains('bar') - } - - @SuppressWarnings('MethodName') - def "Allow setting of backends via assignment"() { - when: - Task task = project.tasks.create(name: ASCIIDOCTOR, type: AsciidoctorTask) { - backends = ['pdf'] - backends = ['foo','bar'] - } - - then: - ! systemOut.toString().contains('deprecated') - !task.backends.contains('pdf') - task.backends.contains('foo') - task.backends.contains('bar') - } - - @SuppressWarnings('MethodName') - def "Allow setting of requires via method"() { - when: - Task task = project.tasks.create(name: ASCIIDOCTOR, type: AsciidoctorTask) { - requires 'slim','tilt' - requires 'asciidoctor-pdf' - } - - then: - ! systemOut.toString().contains('deprecated') - task.requires.contains('asciidoctor-pdf') - task.requires.contains('tilt') - task.requires.contains('slim') - } - - @SuppressWarnings('MethodName') - def "Allow setting of requires via assignment"() { - when: - Task task = project.tasks.create(name: ASCIIDOCTOR, type: AsciidoctorTask) { - requires = ['asciidoctor-pdf'] - requires = ['slim','tilt'] - } - - then: - ! systemOut.toString().contains('deprecated') - !task.requires.contains('asciidoctor-pdf') - task.requires.contains('tilt') - task.requires.contains('slim') - } - - @SuppressWarnings('MethodName') - def "Allow setting of sourceDir via method"() { - when: - Task task = project.tasks.create(name: ASCIIDOCTOR, type: AsciidoctorTask) { - sourceDir project.projectDir - } - - then: - ! systemOut.toString().contains('deprecated') - task.getSourceDir().absolutePath == project.projectDir.absolutePath - task.sourceDir.absolutePath == project.projectDir.absolutePath - } - - - @SuppressWarnings('MethodName') - def "When setting sourceDir via assignment"() { - when: - Task task = project.tasks.create(name: ASCIIDOCTOR, type: AsciidoctorTask) { - sourceDir = project.projectDir - } - - then: - task.getSourceDir().absolutePath == project.projectDir.absolutePath - task.sourceDir.absolutePath == project.projectDir.absolutePath - - } - - @SuppressWarnings('MethodName') - def "When setting sourceDir via setSourceDir"() { - when: - Task task = project.tasks.create(name: ASCIIDOCTOR, type: AsciidoctorTask) { - setSourceDir project.projectDir - } - - then: - task.getSourceDir().absolutePath == project.projectDir.absolutePath - task.sourceDir.absolutePath == project.projectDir.absolutePath - !systemOut.toString().contains('deprecated') - } - - @SuppressWarnings('MethodName') - def "Allow setting of gemPath via method"() { - when: - Task task = project.tasks.create(name: ASCIIDOCTOR, type: AsciidoctorTask) { - gemPath project.projectDir - } - - then: - ! systemOut.toString().contains('deprecated') - task.asGemPath() == project.projectDir.absolutePath - } - - @SuppressWarnings('MethodName') - def "When setting gemPath via assignment"() { - when: - Task task = project.tasks.create(name: ASCIIDOCTOR, type: AsciidoctorTask) { - gemPath = project.projectDir - } - - then: - task.asGemPath() == project.projectDir.absolutePath - ! systemOut.toString().contains('deprecated') - } - - @SuppressWarnings('MethodName') - def "When setting gemPath via setGemPath"() { - when: - Task task = project.tasks.create(name: ASCIIDOCTOR, type: AsciidoctorTask) { - setGemPath project.projectDir - } - - then: - task.asGemPath() == project.projectDir.absolutePath - ! systemOut.toString().contains('deprecated') - } - - @SuppressWarnings('MethodName') - def "sourceDocumentNames should resolve descendant files of sourceDir if supplied as relatives"() { - when: "I specify two files relative to sourceDir,including one in a subfoler" - Task task = project.tasks.create(name: ASCIIDOCTOR, type: AsciidoctorTask) { - sourceDir srcDir - sourceDocumentNames = [ASCIIDOC_SAMPLE_FILE, ASCIIDOC_SAMPLE2_FILE] - } - def fileCollection = task.sourceDocumentNames - - then: "both files should be in collection, but any other files found in folder should be excluded" - fileCollection.contains(new File(srcDir,ASCIIDOC_SAMPLE_FILE).canonicalFile) - fileCollection.contains(new File(srcDir,ASCIIDOC_SAMPLE2_FILE).canonicalFile) - !fileCollection.contains(new File(srcDir,'sample-docinfo.xml').canonicalFile) - fileCollection.files.size() == 2 - } - - @SuppressWarnings('MethodName') - def "sourceDocumentNames should resolve descendant files of sourceDir even if given as absolute files"() { - given: - File sample1 = new File(srcDir,ASCIIDOC_SAMPLE_FILE).absoluteFile - File sample2 = new File(srcDir,ASCIIDOC_SAMPLE2_FILE).absoluteFile - - when: "I specify two absolute path files, that are descendents of sourceDit" - Task task = project.tasks.create(name: ASCIIDOCTOR, type: AsciidoctorTask) { - sourceDir srcDir - sourceDocumentNames = [sample1,sample2] - } - def fileCollection = task.sourceDocumentNames - - then: "both files should be in collection, but any other files found in folder should be excluded" - fileCollection.contains(new File(srcDir,ASCIIDOC_SAMPLE_FILE).canonicalFile) - fileCollection.contains(new File(srcDir,ASCIIDOC_SAMPLE2_FILE).canonicalFile) - !fileCollection.contains(new File(srcDir,'sample-docinfo.xml').canonicalFile) - fileCollection.files.size() == 2 - } - - @SuppressWarnings('MethodName') - def "sourceDocumentNames should not resolve files that are not descendants of sourceDir"() { - given: - File sample1 = new File(project.projectDir,ASCIIDOC_SAMPLE_FILE).absoluteFile - - when: - Task task = project.tasks.create(name: ASCIIDOCTOR, type: AsciidoctorTask) { - sourceDir srcDir - sourceDocumentNames = [sample1] - } - def fileCollection = task.sourceDocumentNames - - then: - fileCollection.files.size() == 0 - } - - @SuppressWarnings('MethodName') - @SuppressWarnings('DuplicateNumberLiteral') - def "Add asciidoctor task with multiple backends"() { - when: - Task task = project.tasks.create(name: ASCIIDOCTOR, type: AsciidoctorTask) { - resourceCopyProxy = mockCopyProxy - sourceDir = srcDir - outputDir = outDir - backends AsciidoctorBackend.DOCBOOK.id, AsciidoctorBackend.HTML5.id - } - - task.processAsciidocSources() - then: - 2 * mockAsciidoctor.renderFile(_, { Map map -> map.backend == AsciidoctorBackend.DOCBOOK.id}) - 2 * mockAsciidoctor.renderFile(_, { Map map -> map.backend == AsciidoctorBackend.HTML5.id}) - } - - @SuppressWarnings('MethodName') - @SuppressWarnings('DuplicateNumberLiteral') - def "Adds asciidoctor task with multiple backends and single backend"() { - when: - Task task = project.tasks.create(name: ASCIIDOCTOR, type: AsciidoctorTask) { - resourceCopyProxy = mockCopyProxy - sourceDir = srcDir - outputDir = outDir - backends = [AsciidoctorBackend.DOCBOOK.id, AsciidoctorBackend.HTML5.id] - backend = AsciidoctorBackend.DOCBOOK5.id - } - - task.processAsciidocSources() - then: - 2 * mockAsciidoctor.renderFile(_, { Map map -> map.backend == AsciidoctorBackend.DOCBOOK.id}) - 2 * mockAsciidoctor.renderFile(_, { Map map -> map.backend == AsciidoctorBackend.HTML5.id}) - // @Ignore('Wrong sysout capture') - // systemOut.toString().contains('Using `backend` and `backends` together will result in `backend` being ignored.') - } - - @SuppressWarnings('MethodName') - @SuppressWarnings('DuplicateNumberLiteral') - def "Adds asciidoctor task with supported backend"() { - expect: - project.tasks.findByName(ASCIIDOCTOR) == null - when: - Task task = project.tasks.create(name: ASCIIDOCTOR, type: AsciidoctorTask) { - resourceCopyProxy = mockCopyProxy - sourceDir srcDir - outputDir = outDir - } - - task.processAsciidocSources() - then: - 2 * mockAsciidoctor.renderFile(_, { Map map -> map.backend == AsciidoctorBackend.HTML5.id}) - ! systemOut.toString().contains('deprecated') - } - - @SuppressWarnings('MethodName') - @SuppressWarnings('DuplicateNumberLiteral') - def "Using setBackend should output a warning"() { - expect: - project.tasks.findByName(ASCIIDOCTOR) == null - when: - Task task = project.tasks.create(name: ASCIIDOCTOR, type: AsciidoctorTask) { - resourceCopyProxy = mockCopyProxy - sourceDir = srcDir - outputDir = outDir - backend = AsciidoctorBackend.DOCBOOK.id - } - - task.processAsciidocSources() - then: - 2 * mockAsciidoctor.renderFile(_, { Map map -> map.backend == AsciidoctorBackend.DOCBOOK.id}) - // @Ignore('Wrong sysout capture') - // systemOut.toString().contains('Using `backend` and `backends` together will result in `backend` being ignored.') - } - - @SuppressWarnings('MethodName') - def "Adds asciidoctor task throws exception"() { - expect: - project.tasks.findByName(ASCIIDOCTOR) == null - when: - Task task = project.tasks.create(name: ASCIIDOCTOR, type: AsciidoctorTask) { - resourceCopyProxy = mockCopyProxy - sourceDir = srcDir - outputDir = outDir - } - - task.processAsciidocSources() - then: - mockAsciidoctor.renderFile(_, _) >> { throw new IllegalArgumentException() } - thrown(GradleException) - } - - @SuppressWarnings('MethodName') - def "Processes a single document given a value for sourceDocumentName"() { - when: - Task task = project.tasks.create(name: ASCIIDOCTOR, type: AsciidoctorTask) { - resourceCopyProxy = mockCopyProxy - sourceDir = srcDir - outputDir = outDir - sourceDocumentName = new File(srcDir, ASCIIDOC_SAMPLE_FILE) - } - - task.processAsciidocSources() - then: - 1 * mockAsciidoctor.renderFile(_, _) - } - - @SuppressWarnings('MethodName') - def "Reuses asciidoctor instance for multiple tasks"() { - when: - (1..2).collect { - project.tasks.create(name: "asciidoctor$it", type: AsciidoctorTask) { - resourceCopyProxy = mockCopyProxy - sourceDir = srcDir - outputDir = new File(outDir, "dir$it") - sourceDocumentName = new File(srcDir, ASCIIDOC_SAMPLE_FILE) - extensions {} - } - - }.each { - it.processAsciidocSources() - } - - then: - 2 * mockAsciidoctor.unregisterAllExtensions() - 2 * mockAsciidoctor.renderFile(_, _) - 2 * mockAsciidoctor.registerExtensions(_) - } - - - @Ignore('Wrong sysout capture') - @SuppressWarnings('MethodName') - def "Output warning when a sourceDocumentName was given"() { - given: - Task task = project.tasks.create(name: ASCIIDOCTOR, type: AsciidoctorTask) { - resourceCopyProxy = mockCopyProxy - sourceDir = srcDir - outputDir = outDir - sourceDocumentName = new File(srcDir, ASCIIDOC_SAMPLE_FILE) - } - when: - task.processAsciidocSources() - then: - systemOut.toString().contains('setSourceDocumentName is deprecated') - } - - @Ignore('Wrong sysout capture') - @SuppressWarnings('MethodName') - def "Output error when sourceDocumentName and sourceDocumentNames are given"() { - given: - Task task = project.tasks.create(name: ASCIIDOCTOR, type: AsciidoctorTask) { - resourceCopyProxy = mockCopyProxy - sourceDir = srcDir - outputDir = outDir - sourceDocumentName = new File(srcDir, ASCIIDOC_SAMPLE_FILE) - sourceDocumentNames = new SimpleFileCollection(new File(srcDir, ASCIIDOC_SAMPLE_FILE)) - } - when: - task.processAsciidocSources() - then: - systemOut.toString().contains('setSourceDocumentNames is deprecated') - } - - @SuppressWarnings('MethodName') - def "Source documents in directories end up in the corresponding output directory"() { - given: - Task task = project.tasks.create(name: ASCIIDOCTOR, type: AsciidoctorTask) { - resourceCopyProxy = mockCopyProxy - sourceDir = srcDir - outputDir = outDir - separateOutputDirs = false - } - when: - task.processAsciidocSources() - then: - 1 * mockAsciidoctor.renderFile(new File(task.sourceDir, ASCIIDOC_SAMPLE2_FILE), { it.to_dir == new File(task.outputDir, 'subdir').absolutePath }) - 1 * mockAsciidoctor.renderFile(new File(task.sourceDir, ASCIIDOC_SAMPLE_FILE), { it.to_dir == task.outputDir.absolutePath }) - and: - 0 * mockAsciidoctor.renderFile(_, _) - } - - @SuppressWarnings('MethodName') - def "Should support String value for attributes option"() { - given: - Task task = project.tasks.create(name: ASCIIDOCTOR, type: AsciidoctorTask) { - resourceCopyProxy = mockCopyProxy - sourceDir = srcDir - outputDir = outDir - sourceDocumentName = new File(srcDir, ASCIIDOC_SAMPLE_FILE) - options = [ - attributes: 'toc=right source-highlighter=coderay' - ] - } - when: - task.processAsciidocSources() - then: - 1 * mockAsciidoctor.renderFile(_, _) - } - - @SuppressWarnings('MethodName') - @SuppressWarnings('DuplicateStringLiteral') - def "Should support GString value for attributes option"() { - given: - Task task = project.tasks.create(name: ASCIIDOCTOR, type: AsciidoctorTask) { - resourceCopyProxy = mockCopyProxy - sourceDir = srcDir - outputDir = outDir - sourceDocumentName = new File(srcDir, ASCIIDOC_SAMPLE_FILE) - def attrs = 'toc=right source-highlighter=coderay' - options = [ - attributes: "$attrs" - ] - } - when: - task.processAsciidocSources() - then: - 1 * mockAsciidoctor.renderFile(_, _) - } - - @SuppressWarnings('MethodName') - @SuppressWarnings('DuplicateStringLiteral') - def "Should support List value for attributes option"() { - given: - Task task = project.tasks.create(name: ASCIIDOCTOR, type: AsciidoctorTask) { - asciidoctor = mockAsciidoctor - resourceCopyProxy = mockCopyProxy - sourceDir = srcDir - outputDir = outDir - sourceDocumentName = new File(srcDir, ASCIIDOC_SAMPLE_FILE) - def highlighter = 'coderay' - options = [ - attributes: ['toc=right', "source-highlighter=$highlighter"] - ] - } - when: - task.processAsciidocSources() - then: - 1 * mockAsciidoctor.renderFile(_, _) - } - - @SuppressWarnings('MethodName') - def "Throws exception when attributes embedded in options is an unsupported type"() { - when: - Task task = project.tasks.create(name: ASCIIDOCTOR, type: AsciidoctorTask) { - resourceCopyProxy = mockCopyProxy - sourceDir = srcDir - outputDir = outDir - sourceDocumentName = new File(srcDir, ASCIIDOC_SAMPLE_FILE) - options = [ - attributes: 23 - ] - } - task.processAsciidocSources() - then: - thrown(InvalidUserDataException) - } - - @SuppressWarnings('MethodName') - def "Setting baseDir results in the correct value being sent to Asciidoctor"() { - given: - File basedir = new File(testRootDir, 'my_base_dir') - Task task = project.tasks.create(name: ASCIIDOCTOR, type: AsciidoctorTask) { - resourceCopyProxy = mockCopyProxy - sourceDir = srcDir - outputDir = outDir - baseDir = basedir - } - when: - task.processAsciidocSources() - then: - 1 * mockAsciidoctor.renderFile(new File(task.sourceDir, ASCIIDOC_SAMPLE_FILE), { it.base_dir == basedir.absolutePath }) - } - - @SuppressWarnings('MethodName') - def "Omitting a value for baseDir results in sending the dir of the processed file"() { - given: - Task task = project.tasks.create(name: ASCIIDOCTOR, type: AsciidoctorTask) { - asciidoctor = mockAsciidoctor - resourceCopyProxy = mockCopyProxy - sourceDir = srcDir - outputDir = outDir - } - when: - task.processAsciidocSources() - then: - 1 * mockAsciidoctor.renderFile(new File(task.sourceDir, ASCIIDOC_SAMPLE_FILE), - { it.base_dir == new File(task.sourceDir, ASCIIDOC_SAMPLE_FILE).getParentFile().absolutePath }) - } - - @SuppressWarnings('MethodName') - def "Setting baseDir to null results in no value being sent to Asciidoctor"() { - given: - Task task = project.tasks.create(name: ASCIIDOCTOR, type: AsciidoctorTask) { - resourceCopyProxy = mockCopyProxy - sourceDir = srcDir - outputDir = outDir - baseDir = null - } - when: - task.processAsciidocSources() - then: - 1 * mockAsciidoctor.renderFile(new File(task.sourceDir, ASCIIDOC_SAMPLE_FILE), { !it.base_dir }) - } - - @SuppressWarnings('MethodName') - def "Safe mode option is equal to level of SafeMode.UNSAFE by default"() { - given: - Task task = project.tasks.create(name: ASCIIDOCTOR, type: AsciidoctorTask) { - resourceCopyProxy = mockCopyProxy - sourceDir = srcDir - outputDir = outDir - } - when: - task.processAsciidocSources() - then: - 1 * mockAsciidoctor.renderFile(new File(task.sourceDir, ASCIIDOC_SAMPLE_FILE), { - it.safe == SafeMode.UNSAFE.level - }) - } - - @SuppressWarnings('MethodName') - def "Safe mode configuration option as integer is honored"() { - given: - Task task = project.tasks.create(name: ASCIIDOCTOR, type: AsciidoctorTask) { - resourceCopyProxy = mockCopyProxy - sourceDir = srcDir - outputDir = outDir - options = [ - safe: SafeMode.SERVER.level - ] - } - when: - task.processAsciidocSources() - then: - 1 * mockAsciidoctor.renderFile(new File(task.sourceDir, ASCIIDOC_SAMPLE_FILE), { - it.safe == SafeMode.SERVER.level - }) - } - - @SuppressWarnings('MethodName') - def "Safe mode configuration option as string is honored"() { - given: - Task task = project.tasks.create(name: ASCIIDOCTOR, type: AsciidoctorTask) { - resourceCopyProxy = mockCopyProxy - sourceDir = srcDir - outputDir = outDir - options = [ - safe: 'server' - ] - } - when: - task.processAsciidocSources() - then: - 1 * mockAsciidoctor.renderFile(new File(task.sourceDir, ASCIIDOC_SAMPLE_FILE), { - it.safe == SafeMode.SERVER.level - }) - } - - @SuppressWarnings('MethodName') - def "Safe mode configuration option as enum is honored"() { - given: - Task task = project.tasks.create(name: ASCIIDOCTOR, type: AsciidoctorTask) { - resourceCopyProxy = mockCopyProxy - sourceDir = srcDir - outputDir = outDir - options = [ - safe: SafeMode.SERVER - ] - } - when: - task.processAsciidocSources() - then: - 1 * mockAsciidoctor.renderFile(new File(task.sourceDir, ASCIIDOC_SAMPLE_FILE), { - it.safe == SafeMode.SERVER.level - }) - } - - @SuppressWarnings('MethodName') - def "Attributes projectdir and rootdir are always set to relative dirs of the processed file"() { - given: - Task task = project.tasks.create(name: ASCIIDOCTOR, type: AsciidoctorTask) { - resourceCopyProxy = mockCopyProxy - sourceDir = srcDir - outputDir = outDir - } - when: - task.processAsciidocSources() - then: - 1 * mockAsciidoctor.renderFile(new File(task.sourceDir, ASCIIDOC_SAMPLE_FILE), { - it.attributes.projectdir == AsciidoctorUtils.getRelativePath(project.projectDir, new File(task.sourceDir, ASCIIDOC_SAMPLE_FILE).getParentFile()) - it.attributes.rootdir == AsciidoctorUtils.getRelativePath(project.rootDir, new File(task.sourceDir, ASCIIDOC_SAMPLE_FILE).getParentFile()) - }) - } - - @SuppressWarnings('MethodName') - def "Docinfo files are not copied to target directory"() { - given: - Task task = project.tasks.create(name: ASCIIDOCTOR, type: AsciidoctorTask) { - asciidoctor = mockAsciidoctor - resourceCopyProxy = mockCopyProxy - sourceDir = srcDir - outputDir = outDir - } - when: - task.processAsciidocSources() - then: - 1 * mockAsciidoctor.renderFile(new File(task.sourceDir, ASCIIDOC_SAMPLE_FILE), _) - !outDir.listFiles({ !it.directory && !(it.name =~ DOCINFO_FILE_PATTERN) } as FileFilter) - } - - @SuppressWarnings('MethodName') - def "Project coordinates are set automatically as attributes"() { - given: - project.version = '1.0.0-SNAPSHOT' - project.group = 'com.acme' - Task task = project.tasks.create(name: ASCIIDOCTOR, type: AsciidoctorTask) { - resourceCopyProxy = mockCopyProxy - sourceDir = srcDir - outputDir = outDir - } - when: - task.processAsciidocSources() - then: - 1 * mockAsciidoctor.renderFile(new File(task.sourceDir, ASCIIDOC_SAMPLE_FILE), { - it.attributes.'project-name' == 'test' && - it.attributes.'project-group' == 'com.acme' && - it.attributes.'project-version' == '1.0.0-SNAPSHOT' - }) - } - - @SuppressWarnings('MethodName') - def "Override project coordinates with explicit attributes"() { - given: - project.version = '1.0.0-SNAPSHOT' - project.group = 'com.acme' - Task task = project.tasks.create(name: ASCIIDOCTOR, type: AsciidoctorTask) { - resourceCopyProxy = mockCopyProxy - sourceDir = srcDir - outputDir = outDir - options = [ - attributes: [ - 'project-name': 'awesome', - 'project-group': 'unicorns', - 'project-version': '1.0.0.Final' - ] - ] - } - when: - task.processAsciidocSources() - then: - 1 * mockAsciidoctor.renderFile(new File(task.sourceDir, ASCIIDOC_SAMPLE_FILE), { - it.attributes.'project-name' == 'awesome' && - it.attributes.'project-group' == 'unicorns' && - it.attributes.'project-version' == '1.0.0.Final' - }) - } - - @SuppressWarnings('MethodName') - def "Should support a single source document if a name is given"() { - given: - Task task = project.tasks.create(name: ASCIIDOCTOR, type: AsciidoctorTask) { - resourceCopyProxy = mockCopyProxy - sourceDir = srcDir - outputDir = outDir - sourceDocumentName = new File(ASCIIDOC_RESOURCES_DIR, ASCIIDOC_SAMPLE_FILE) - } - when: - task.processAsciidocSources() - then: - 1 * mockAsciidoctor.renderFile(new File(task.sourceDir, ASCIIDOC_SAMPLE_FILE),_ ) - } - - @SuppressWarnings('MethodName') - def "Should support multiple source documents if names are given"() { - given: - Task task = project.tasks.create(name: ASCIIDOCTOR, type: AsciidoctorTask) { - resourceCopyProxy = mockCopyProxy - sourceDir = srcDir - outputDir = outDir - sources { - include ASCIIDOC_SAMPLE_FILE,ASCIIDOC_SAMPLE2_FILE - } - } - when: - task.processAsciidocSources() - then: - 1 * mockAsciidoctor.renderFile(new File(task.sourceDir, ASCIIDOC_SAMPLE_FILE),_ ) - 1 * mockAsciidoctor.renderFile(new File(task.sourceDir, ASCIIDOC_SAMPLE2_FILE),_ ) - } - - @SuppressWarnings('MethodName') - def "Should throw exception if the file sourceDocumentName starts with underscore"() { - given: - Task task = project.tasks.create(name: ASCIIDOCTOR, type: AsciidoctorTask) { - resourceCopyProxy = mockCopyProxy - sourceDir = srcDir - outputDir = outDir - sourceDocumentName = new File(srcDir, ASCIIDOC_INVALID_FILE) - } - when: - task.processAsciidocSources() - then: - 0 * mockAsciidoctor.renderFile(_, _) - thrown(GradleException) - } - - @SuppressWarnings('MethodName') - def "Should throw exception if a file in sourceDocumentNames starts with underscore"() { - given: - Task task = project.tasks.create(name: ASCIIDOCTOR, type: AsciidoctorTask) { - resourceCopyProxy = mockCopyProxy - sourceDir srcDir - outputDir outDir - setSourceDocumentNames ASCIIDOC_INVALID_FILE - } - when: - task.processAsciidocSources() - then: - 0 * mockAsciidoctor.renderFile(_, _) - thrown(GradleException) - } - - def "When 'resources' not specified, then copy all images to backend"() { - given: - Task task = project.tasks.create(name: ASCIIDOCTOR, type: AsciidoctorTask) { - resourceCopyProxy = mockCopyProxy - - sourceDir srcDir - outputDir outDir - backends AsciidoctorBackend.HTML5.id - - sources { - include ASCIIDOC_SAMPLE_FILE - } - - } - when: - task.processAsciidocSources() - then: - 1 * mockCopyProxy.copy(_, _) - } - - def "When 'resources' not specified and more than one backend, then copy all images to every backend"() { - given: - Task task = project.tasks.create(name: ASCIIDOCTOR, type: AsciidoctorTask) { - resourceCopyProxy = mockCopyProxy - - sourceDir srcDir - outputDir outDir - backends AsciidoctorBackend.HTML5.id,AsciidoctorBackend.DOCBOOK.id - - sources { - include ASCIIDOC_SAMPLE_FILE - } - - } - when: - task.processAsciidocSources() - then: - 1 * mockCopyProxy.copy( new File(outDir,AsciidoctorBackend.HTML5.id) , _) - 1 * mockCopyProxy.copy( new File(outDir,AsciidoctorBackend.DOCBOOK.id) , _) - } - - def "When 'resources' are specified, then copy according to all patterns"() { - given: - Task task = project.tasks.create(name: ASCIIDOCTOR, type: AsciidoctorTask) { - resourceCopyProxy = mockCopyProxy - - sourceDir srcDir - outputDir outDir - backends AsciidoctorBackend.HTML5.id,AsciidoctorBackend.DOCBOOK.id - - sources { - include ASCIIDOC_SAMPLE_FILE - } - - resources { - from (sourceDir) { - include 'images/**' - } - } - } - - when: - task.processAsciidocSources() - then: - 1 * mockCopyProxy.copy( new File(outDir,AsciidoctorBackend.HTML5.id) , _) - 1 * mockCopyProxy.copy( new File(outDir,AsciidoctorBackend.DOCBOOK.id) , _) - } - - def "sanity test for default configuration" () { - when: - Task task = project.tasks.create(name: ASCIIDOCTOR, type: AsciidoctorTask) - - then: - task.sourceDir.absolutePath.replace('\\', '/').endsWith('src/docs/asciidoc') - task.outputDir.absolutePath.replace('\\', '/').endsWith('build/asciidoc') - } - - def "set output dir to something else than the default" () { - given: - project.version = '1.0.0-SNAPSHOT' - project.group = 'com.acme' - - Task task = project.tasks.create(name: ASCIIDOCTOR, type: AsciidoctorTask) { - asciidoctor = mockAsciidoctor - resourceCopyProxy = mockCopyProxy - sourceDir = srcDir - } - project.buildDir = '/tmp/testbuild/asciidoctor' - - when: - task.processAsciidocSources() - - then: 'the html files must be in testbuild/asciidoctor ' - task.outputDir.absolutePath.replace('\\', '/').contains('testbuild/asciidoctor') - 2 * mockAsciidoctor.renderFile(_, { it.to_dir.startsWith(project.buildDir.absolutePath) }) - } - - def "Files in the resources copyspec should be recognised as input files" () { - given: - File imagesDir = new File(outDir,'images') - File imageFile = new File(imagesDir,'fake.txt') - imagesDir.mkdirs() - imageFile.text = 'foo' - - Task task = project.tasks.create(name: ASCIIDOCTOR, type: AsciidoctorTask) { - resourceCopyProxy = mockCopyProxy - - sourceDir srcDir - outputDir "${outDir}/foo" - backends AsciidoctorBackend.HTML5.id - - sources { - include ASCIIDOC_SAMPLE_FILE - } - - resources { - from (outDir) { - include 'images/**' - } - } - } - - when: - project.evaluate() - - then: - task.inputs.files.contains(project.file("${srcDir}/sample.asciidoc")) - task.inputs.files.contains(project.file("${imagesDir}/fake.txt")) - } - - def "GroovyStrings in options and attributes are converted into Strings" () { - given: - String variable = 'bar' - Task task = project.tasks.create(name: ASCIIDOCTOR, type: AsciidoctorTask) { - options = [ - template_dirs: ["${project.projectDir}/templates/haml"] - ] - attributes = [ - foo: "${variable}" - ] - resourceCopyProxy = mockCopyProxy - sourceDir srcDir - outputDir = outDir - } - - when: - task.processAsciidocSources() - - then: - 1 * mockAsciidoctor.renderFile(new File(task.sourceDir, ASCIIDOC_SAMPLE_FILE), { Map props -> - props.template_dirs*.class == [String] && - props.attributes.foo.class == String - }) - } - - @SuppressWarnings('MethodName') - def "Should require libraries"() { - expect: - project.tasks.findByName(ASCIIDOCTOR) == null - when: - Task task = project.tasks.create(name: ASCIIDOCTOR, type: AsciidoctorTask) { - resourceCopyProxy = mockCopyProxy - sourceDir = srcDir - outputDir = outDir - sourceDocumentName = new File(srcDir, ASCIIDOC_SAMPLE_FILE) - requires 'asciidoctor-pdf' - } - - task.processAsciidocSources() - then: - 1 * mockAsciidoctor.requireLibrary("asciidoctor-pdf") - } - -} diff --git a/asciidoctor-gradle-jvm/src/test/groovy/org/asciidoctor/gradle/AsciidoctorUtilsSpec.groovy b/asciidoctor-gradle-jvm/src/test/groovy/org/asciidoctor/gradle/AsciidoctorUtilsSpec.groovy deleted file mode 100755 index 7e8bfedd5..000000000 --- a/asciidoctor-gradle-jvm/src/test/groovy/org/asciidoctor/gradle/AsciidoctorUtilsSpec.groovy +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright 2013-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.asciidoctor.gradle - -import spock.lang.Specification - -class AsciidoctorUtilsSpec extends Specification { - - static s = File.separator - - def 'finds relative paths'(String target, String base, String relative) { - expect: - AsciidoctorUtils.getRelativePath(new File(target), new File(base)) == relative - where: - target | base | relative - 'src/test/groovy' | 'src' | "test${s}groovy" - 'src/test/groovy/' | 'src/' | "test${s}groovy" - 'src/test/groovy' | 'src/' | "test${s}groovy" - 'src/test/groovy/' | 'src' | "test${s}groovy" - 'src' | 'src/test/groovy' | "..${s}.." - 'src/' | 'src/test/groovy/' | "..${s}.." - 'src' | 'src/test/groovy/' | "..${s}.." - 'src/' | 'src/test/groovy' | "..${s}.." - 'src/test' | 'src/test' | '' - 'src/test/' | 'src/test/' | '' - 'src/test' | 'src/test/' | '' - 'src/test/' | 'src/test' | '' - } -} diff --git a/asciidoctor-gradle-jvm/src/test/groovy/org/asciidoctor/gradle/AsciidoctorPluginSpec.groovy b/asciidoctor-gradle-jvm/src/test/groovy/org/asciidoctor/gradle/compat/AsciidoctorPluginSpec.groovy similarity index 62% rename from asciidoctor-gradle-jvm/src/test/groovy/org/asciidoctor/gradle/AsciidoctorPluginSpec.groovy rename to asciidoctor-gradle-jvm/src/test/groovy/org/asciidoctor/gradle/compat/AsciidoctorPluginSpec.groovy index 7f3379363..a4ab1535a 100755 --- a/asciidoctor-gradle-jvm/src/test/groovy/org/asciidoctor/gradle/AsciidoctorPluginSpec.groovy +++ b/asciidoctor-gradle-jvm/src/test/groovy/org/asciidoctor/gradle/compat/AsciidoctorPluginSpec.groovy @@ -13,8 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.asciidoctor.gradle +package org.asciidoctor.gradle.compat +import org.asciidoctor.gradle.AsciidoctorCompatibilityPlugin +import org.asciidoctor.gradle.jvm.AsciidoctorJPlugin +import org.gradle.api.GradleException import org.gradle.api.Project import org.gradle.api.Task import org.gradle.api.artifacts.DependencyResolutionListener @@ -32,7 +35,7 @@ import spock.lang.Specification */ class AsciidoctorPluginSpec extends Specification { private static final String ASCIIDOCTOR = 'asciidoctor' - + private static final String BINTRAY = 'BintrayJCenter' Project project def setup() { @@ -42,17 +45,17 @@ class AsciidoctorPluginSpec extends Specification { @SuppressWarnings('MethodName') def "Applies plugin and checks default setup"() { expect: - project.tasks.findByName(ASCIIDOCTOR) == null + project.tasks.findByName(ASCIIDOCTOR) == null when: - project.apply plugin: AsciidoctorCompatibilityPlugin + project.apply plugin: AsciidoctorCompatibilityPlugin then: - Task asciidoctorTask = project.tasks.findByName(ASCIIDOCTOR) - asciidoctorTask != null - asciidoctorTask.group == 'Documentation' - asciidoctorTask.sourceDir == project.file('src/docs/asciidoc') - asciidoctorTask.outputDir == new File(project.buildDir, 'asciidoc') + Task asciidoctorTask = project.tasks.findByName(ASCIIDOCTOR) + asciidoctorTask != null + asciidoctorTask.group == 'Documentation' + asciidoctorTask.sourceDir == project.file('src/docs/asciidoc') + asciidoctorTask.outputDir == new File(project.buildDir, 'asciidoc') - project.tasks.findByName('clean') != null + project.tasks.findByName('clean') != null } @Ignore("Method 'getDependencyResolutionBroadcast' is unknown") @@ -69,38 +72,50 @@ class AsciidoctorPluginSpec extends Specification { def expectedDslVersion = 'dsl.' + expectedVersion project.asciidoctorj.groovyDslVersion = expectedDslVersion - def config = project.project.configurations.getByName('asciidoctor') + def config = project.project.configurations.getByName(ASCIIDOCTOR) def dependencies = config.dependencies - assert dependencies.isEmpty(); + assert dependencies.isEmpty() // mock-trigger beforeResolve() to avoid 'real' resolution of dependencies - DependencyResolutionListener broadcast = config.getDependencyResolutionBroadcast() - ResolvableDependencies incoming = config.getIncoming() + DependencyResolutionListener broadcast = config.dependencyResolutionBroadcast + ResolvableDependencies incoming = config.incoming broadcast.beforeResolve(incoming) - def dependencyHandler = project.getDependencies(); + def dependencyHandler = project.dependencies then: assert dependencies.contains(dependencyHandler.create(AsciidoctorCompatibilityPlugin.ASCIIDOCTORJ_GROOVY_DSL_DEPENDENCY + expectedDslVersion)) assert dependencies.contains(dependencyHandler.create(AsciidoctorCompatibilityPlugin.ASCIIDOCTORJ_CORE_DEPENDENCY + expectedVersion)) } + @SuppressWarnings('MethodName') def "JCenter repository is added by default"() { when: - project.apply plugin : AsciidoctorCompatibilityPlugin + project.apply plugin: AsciidoctorCompatibilityPlugin project.evaluate() then: - project.repositories.findByName('BintrayJCenter') + project.repositories.findByName(BINTRAY) } + @SuppressWarnings('MethodName') def "JCenter repository must not be added when noDefaultRepositories is set"() { when: - project.apply plugin : AsciidoctorCompatibilityPlugin + project.apply plugin: AsciidoctorCompatibilityPlugin project.extensions.asciidoctorj.noDefaultRepositories = true project.evaluate() then: - project.repositories.findByName('BintrayJCenter') == null + project.repositories.findByName(BINTRAY) == null + } + + @SuppressWarnings('MethodName') + def 'Cannot combine compatibility plugin with newer plugins'() { + when: + project.apply plugin: AsciidoctorJPlugin + project.apply plugin: AsciidoctorCompatibilityPlugin + + then: + thrown(GradleException) } } diff --git a/asciidoctor-gradle-jvm/src/test/groovy/org/asciidoctor/gradle/AsciidoctorTaskInlineExtensionsSpec.groovy b/asciidoctor-gradle-jvm/src/test/groovy/org/asciidoctor/gradle/compat/AsciidoctorTaskInlineExtensionsSpec.groovy similarity index 82% rename from asciidoctor-gradle-jvm/src/test/groovy/org/asciidoctor/gradle/AsciidoctorTaskInlineExtensionsSpec.groovy rename to asciidoctor-gradle-jvm/src/test/groovy/org/asciidoctor/gradle/compat/AsciidoctorTaskInlineExtensionsSpec.groovy index 352332c24..e2cc54964 100644 --- a/asciidoctor-gradle-jvm/src/test/groovy/org/asciidoctor/gradle/AsciidoctorTaskInlineExtensionsSpec.groovy +++ b/asciidoctor-gradle-jvm/src/test/groovy/org/asciidoctor/gradle/compat/AsciidoctorTaskInlineExtensionsSpec.groovy @@ -13,8 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.asciidoctor.gradle +package org.asciidoctor.gradle.compat +import org.asciidoctor.gradle.AsciidoctorTask import org.gradle.api.GradleException import org.gradle.api.Project import org.gradle.api.Task @@ -26,11 +27,13 @@ import org.jsoup.nodes.Element import spock.lang.Specification /** - * Asciidoctor task inline extensions specification + * Asciidoctor task inline extensionRegistry specification * * @author Robert Panzer */ +@SuppressWarnings(['DuplicateStringLiteral', 'MethodName', 'UnnecessaryGString', 'Println']) class AsciidoctorTaskInlineExtensionsSpec extends Specification { + private static final String ASCIIDOCTOR = 'asciidoctor' private static final String ASCIIDOC_RESOURCES_DIR = 'build/resources/test/src/asciidocextensions' private static final String ASCIIDOC_BUILD_DIR = 'build/asciidocextensions' @@ -55,21 +58,21 @@ class AsciidoctorTaskInlineExtensionsSpec extends Specification { given: Task task = project.tasks.create(name: ASCIIDOCTOR, type: AsciidoctorTask) { sourceDir = srcDir - sourceDocumentNames = [ASCIIDOC_INLINE_EXTENSIONS_FILE] + sources { + include ASCIIDOC_INLINE_EXTENSIONS_FILE + } outputDir = outDir extensions { block(name: "BIG", contexts: [":paragraph"]) { parent, reader, attributes -> - def upperLines = reader.readLines() - .collect {it.toUpperCase()} + def upperLines = reader.readLines()*.toUpperCase() .inject("") {a, b -> a + '\n' + b} createBlock(parent, "paragraph", [upperLines], attributes, [:]) } block("small") { parent, reader, attributes -> - def lowerLines = reader.readLines() - .collect {it.toLowerCase()} + def lowerLines = reader.readLines()*.toLowerCase() .inject("") {a, b -> a + '\n' + b} createBlock(parent, "paragraph", [lowerLines], attributes, [:]) @@ -82,8 +85,8 @@ class AsciidoctorTaskInlineExtensionsSpec extends Specification { task.processAsciidocSources() then: resultFile.exists() - resultFile.getText().contains("WRITE THIS IN UPPERCASE") - resultFile.getText().contains("and write this in lowercase") + resultFile.text.contains("WRITE THIS IN UPPERCASE") + resultFile.text.contains("and write this in lowercase") } def "Should apply BlockProcessor from file"() { @@ -91,7 +94,9 @@ class AsciidoctorTaskInlineExtensionsSpec extends Specification { print project.files('src/test/resources/src/asciidocextensions/blockMacro.groovy').each {println ">>> $it"} Task task = project.tasks.create(name: ASCIIDOCTOR, type: AsciidoctorTask) { sourceDir = srcDir - sourceDocumentNames = [ASCIIDOC_INLINE_EXTENSIONS_FILE] + sources { + include ASCIIDOC_INLINE_EXTENSIONS_FILE + } outputDir = outDir extensions new File(sourceDir, ASCIIDOC_MACRO_EXTENSION_SCRIPT) } @@ -100,8 +105,8 @@ class AsciidoctorTaskInlineExtensionsSpec extends Specification { task.processAsciidocSources() then: resultFile.exists() - resultFile.getText().contains("WRITE THIS IN UPPERCASE") - resultFile.getText().contains("and write this in lowercase") + resultFile.text.contains("WRITE THIS IN UPPERCASE") + resultFile.text.contains("and write this in lowercase") } def "Should apply inline Postprocessor"() { @@ -109,7 +114,9 @@ class AsciidoctorTaskInlineExtensionsSpec extends Specification { String copyright = "Copyright Acme, Inc." + System.currentTimeMillis() Task task = project.tasks.create(name: ASCIIDOCTOR, type: AsciidoctorTask) { sourceDir = srcDir - sourceDocumentNames = [ASCIIDOC_INLINE_EXTENSIONS_FILE] + sources { + include ASCIIDOC_INLINE_EXTENSIONS_FILE + } outputDir = outDir extensions { postprocessor { @@ -132,15 +139,17 @@ class AsciidoctorTaskInlineExtensionsSpec extends Specification { task.processAsciidocSources() then: resultFile.exists() - resultFile.getText().contains(copyright) - resultFile.getText().contains("Inline Extension Test document") + resultFile.text.contains(copyright) + resultFile.text.contains("Inline Extension Test document") } def "Should fail if inline Postprocessor fails"() { given: Task task = project.tasks.create(name: ASCIIDOCTOR, type: AsciidoctorTask) { sourceDir = srcDir - sourceDocumentNames = [ASCIIDOC_INLINE_EXTENSIONS_FILE] + sources { + include ASCIIDOC_INLINE_EXTENSIONS_FILE + } outputDir = outDir extensions { postprocessor { @@ -161,7 +170,9 @@ class AsciidoctorTaskInlineExtensionsSpec extends Specification { given: Task task = project.tasks.create(name: ASCIIDOCTOR, type: AsciidoctorTask) { sourceDir = srcDir - sourceDocumentNames = [ASCIIDOC_INLINE_EXTENSIONS_FILE] + sources { + include ASCIIDOC_INLINE_EXTENSIONS_FILE + } outputDir = outDir extensions { preprocessor { @@ -176,7 +187,7 @@ class AsciidoctorTaskInlineExtensionsSpec extends Specification { task.processAsciidocSources() then: resultFile.exists() - !resultFile.getText().contains("Inline Extension Test document") + !resultFile.text.contains("Inline Extension Test document") } def "Should apply inline Includeprocessor"() { @@ -184,20 +195,24 @@ class AsciidoctorTaskInlineExtensionsSpec extends Specification { String content = "The content of the URL " + System.currentTimeMillis() Task task = project.tasks.create(name: ASCIIDOCTOR, type: AsciidoctorTask) { sourceDir = srcDir - sourceDocumentNames = [ASCIIDOC_INLINE_EXTENSIONS_FILE] + sources { + include ASCIIDOC_INLINE_EXTENSIONS_FILE + } outputDir = outDir extensions { include_processor(filter: { it.startsWith('http') }) { document, reader, target, attributes -> - reader.push_include(content, target, target, 1, attributes); + reader.push_include(content, target, target, 1, attributes) } } } File resultFile = new File(outDir, 'html5' + File.separator + ASCIIDOC_INLINE_EXTENSIONS_RESULT_FILE) + when: task.processAsciidocSources() + then: resultFile.exists() - resultFile.getText().contains(content) + resultFile.text.contains(content) } } diff --git a/asciidoctor-gradle-jvm/src/test/groovy/org/asciidoctor/gradle/compat/AsciidoctorTaskSpec.groovy b/asciidoctor-gradle-jvm/src/test/groovy/org/asciidoctor/gradle/compat/AsciidoctorTaskSpec.groovy new file mode 100755 index 000000000..21dc52b19 --- /dev/null +++ b/asciidoctor-gradle-jvm/src/test/groovy/org/asciidoctor/gradle/compat/AsciidoctorTaskSpec.groovy @@ -0,0 +1,1028 @@ +/* + * Copyright 2013-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.asciidoctor.gradle.compat + +import org.asciidoctor.SafeMode +import org.asciidoctor.gradle.AsciidoctorTask +import org.asciidoctor.gradle.internal.AsciidoctorUtils +import org.gradle.api.GradleException +import org.gradle.api.InvalidUserDataException +import org.gradle.api.Project +import org.gradle.api.Task +import org.gradle.testfixtures.ProjectBuilder +import spock.lang.Specification + +/** + * Asciidoctor task specification + * + * @author Benjamin Muschko + * @author Stephan Classen + * @author Marcus Fihlon + * @author Schalk W. Cronjé + */ +// We suppress a lot of wraning as this test will be rmeoved at some stage. +@SuppressWarnings(['DuplicateStringLiteral', 'MethodName', 'MethodCount', 'DuplicateNumberLiteral', 'DuplicateMapLiteral']) +class AsciidoctorTaskSpec extends Specification { + private static final String ASCIIDOCTOR = 'asciidoctor' + private static final String ASCIIDOC_RESOURCES_DIR = 'build/resources/test/src/asciidoc' + private static final String ASCIIDOC_BUILD_DIR = 'build/asciidoc' + private static final String ASCIIDOC_SAMPLE_FILE = 'sample.asciidoc' + private static final String ASCIIDOC_SAMPLE2_FILE = 'subdir/sample2.ad' + private static final String ASCIIDOC_INVALID_FILE = 'subdir/_include.adoc' + private static final DOCINFO_FILE_PATTERN = ~/^(.+\-)?docinfo(-footer)?\.[^.]+$/ + + Project project + AsciidoctorProxy mockAsciidoctor + ResourceCopyProxy mockCopyProxy + File testRootDir + File srcDir + File outDir + ByteArrayOutputStream systemOut + + PrintStream originSystemOut + + def setup() { + project = ProjectBuilder.builder().withName('test').build() + project.configurations.create(ASCIIDOCTOR) + mockAsciidoctor = Mock(AsciidoctorProxy) + mockCopyProxy = Mock(ResourceCopyProxy) + testRootDir = new File('.') + srcDir = new File(testRootDir, ASCIIDOC_RESOURCES_DIR).absoluteFile + outDir = new File(project.projectDir, ASCIIDOC_BUILD_DIR) + systemOut = new ByteArrayOutputStream() + originSystemOut = System.out + System.out = new PrintStream(systemOut) + AsciidoctorTask.ASCIIDOCTORS.put(new AsciidoctorProxyCacheKey(gemPath: '', classpath: []), mockAsciidoctor) + } + + def cleanup() { + System.out = originSystemOut + AsciidoctorTask.ASCIIDOCTORS.clear() + } + + @SuppressWarnings('MethodName') + def "Allow setting of options via method"() { + when: + Task task = project.tasks.create(name: ASCIIDOCTOR, type: AsciidoctorTask) { + options eruby: 'erb' + options eruby: 'erubis' + options doctype: 'book', toc: 'right' + } + + then: + !systemOut.toString().contains('deprecated') + task.options['eruby'] == 'erubis' + task.options['doctype'] == 'book' + task.options['toc'] == 'right' + } + + @SuppressWarnings('MethodName') + def "Allow setting of options via assignment"() { + when: + Task task = project.tasks.create(name: ASCIIDOCTOR, type: AsciidoctorTask) { + options = [eruby: 'erb', toc: 'right'] + options = [eruby: 'erubis', doctype: 'book'] + } + + then: + !systemOut.toString().contains('deprecated') + task.options['eruby'] == 'erubis' + task.options['doctype'] == 'book' + !task.options.containsKey('toc') + } + + @SuppressWarnings('MethodName') + def "Allow setting of attributes via method (Map variant)"() { + when: + Task task = project.tasks.create(name: ASCIIDOCTOR, type: AsciidoctorTask) { + attributes 'source-highlighter': 'foo' + attributes 'source-highlighter': 'coderay' + attributes idprefix: '$', idseparator: '-' + } + + then: + !systemOut.toString().contains('deprecated') + task.attributes['source-highlighter'] == 'coderay' + task.attributes['idprefix'] == '$' + task.attributes['idseparator'] == '-' + } + + @SuppressWarnings('MethodName') + def "Allow setting of attributes via method (List variant)"() { + when: + Task task = project.tasks.create(name: ASCIIDOCTOR, type: AsciidoctorTask) { + attributes(['source-highlighter=foo', 'source-highlighter=coderay', 'idprefix=$', 'idseparator=-']) + } + + then: + !systemOut.toString().contains('deprecated') + task.attributes['source-highlighter'] == 'coderay' + task.attributes['idprefix'] == '$' + task.attributes['idseparator'] == '-' + } + + @SuppressWarnings('MethodName') + def "Allow setting of attributes via method (String variant)"() { + when: + Task task = project.tasks.create(name: ASCIIDOCTOR, type: AsciidoctorTask) { + attributes 'source-highlighter=foo source-highlighter=coderay idprefix=$ idseparator=-' + } + + then: + !systemOut.toString().contains('deprecated') + task.attributes['source-highlighter'] == 'coderay' + task.attributes['idprefix'] == '$' + task.attributes['idseparator'] == '-' + } + + @SuppressWarnings('MethodName') + def "Allow setting of attributes via assignment"() { + when: + Task task = project.tasks.create(name: ASCIIDOCTOR, type: AsciidoctorTask) { + attributes = ['source-highlighter': 'foo', idprefix: '$'] + attributes = ['source-highlighter': 'coderay', idseparator: '-'] + } + + then: + !systemOut.toString().contains('deprecated') + task.attributes['source-highlighter'] == 'coderay' + task.attributes['idseparator'] == '-' + !task.attributes.containsKey('idprefix') + } + + @SuppressWarnings('MethodName') + def "Mixing attributes with options, should produce a warning, but updates should be appended"() { + when: + Task task = project.tasks.create(name: ASCIIDOCTOR, type: AsciidoctorTask) { + options eruby: 'erubis', attributes: ['source-highlighter': 'foo', idprefix: '$'] + options doctype: 'book', attributes: [idseparator: '-'] + } + + then: + !task.attributes.containsKey('attributes') + task.attributes['source-highlighter'] == 'foo' + task.attributes['idseparator'] == '-' + task.attributes['idprefix'] == '$' + task.options['eruby'] == 'erubis' + task.options['doctype'] == 'book' + // @Ignore('Wrong sysout capture') + // systemOut.toString().contains('Attributes found in options.') + } + + @SuppressWarnings('MethodName') + def "Mixing attributes with options with assignment, should produce a warning, and attributes will be replaced"() { + when: + Map tmpStore = [eruby: 'erubis', attributes: ['source-highlighter': 'foo', idprefix: '$']] + Task task = project.tasks.create(name: ASCIIDOCTOR, type: AsciidoctorTask) { + options = tmpStore + options = [doctype: 'book', attributes: [idseparator: '-']] + } + + then: + !task.attributes.containsKey('attributes') + task.attributes['idseparator'] == '-' + !task.attributes.containsKey('source-highlighter') + !task.attributes.containsKey('idprefix') + !task.options.containsKey('eruby') + task.options['doctype'] == 'book' + // @Ignore('Wrong sysout capture') + // systemOut.toString().contains('Attributes found in options.') + } + + @SuppressWarnings('MethodName') + def "Mixing string legacy form of attributes with options with assignment, should produce a warning, and attributes will be replaced"() { + when: + Task task = project.tasks.create(name: ASCIIDOCTOR, type: AsciidoctorTask) { + options = [doctype: 'book', attributes: 'toc=right source-highlighter=coderay toc-title=Table\\ of\\ Contents'] + } + + then: + task.options['doctype'] == 'book' + !task.attributes.containsKey('attributes') + task.attributes['toc'] == 'right' + task.attributes['source-highlighter'] == 'coderay' + task.attributes['toc-title'] == 'Table of Contents' + // @Ignore('Wrong sysout capture') + // systemOut.toString().contains('Attributes found in options.') + } + + @SuppressWarnings('MethodName') + def "Mixing list legacy form of attributes with options with assignment, should produce a warning, and attributes will be replaced"() { + when: + Task task = project.tasks.create(name: ASCIIDOCTOR, type: AsciidoctorTask) { + options = [doctype: 'book', attributes: [ + 'toc=right', + 'source-highlighter=coderay', + 'toc-title=Table of Contents' + ]] + } + + then: + task.options['doctype'] == 'book' + !task.attributes.containsKey('attributes') + task.attributes['toc'] == 'right' + task.attributes['source-highlighter'] == 'coderay' + task.attributes['toc-title'] == 'Table of Contents' + // @Ignore('Wrong sysout capture') + // systemOut.toString().contains('Attributes found in options.') + } + + @SuppressWarnings('MethodName') + def "Allow setting of backends via method"() { + when: + Task task = project.tasks.create(name: ASCIIDOCTOR, type: AsciidoctorTask) { + backends 'foo', 'bar' + backends 'pdf' + } + + then: + !systemOut.toString().contains('deprecated') + task.backends.contains('pdf') + task.backends.contains('foo') + task.backends.contains('bar') + } + + @SuppressWarnings('MethodName') + def "Allow setting of backends via assignment"() { + when: + Task task = project.tasks.create(name: ASCIIDOCTOR, type: AsciidoctorTask) { + backends = ['pdf'] + backends = ['foo', 'bar'] + } + + then: + !systemOut.toString().contains('deprecated') + !task.backends.contains('pdf') + task.backends.contains('foo') + task.backends.contains('bar') + } + + @SuppressWarnings('MethodName') + def "Allow setting of requires via method"() { + when: + Task task = project.tasks.create(name: ASCIIDOCTOR, type: AsciidoctorTask) { + requires 'slim', 'tilt' + requires 'asciidoctor-pdf' + } + + then: + !systemOut.toString().contains('deprecated') + task.requires.contains('asciidoctor-pdf') + task.requires.contains('tilt') + task.requires.contains('slim') + } + + @SuppressWarnings('MethodName') + def "Allow setting of requires via assignment"() { + when: + Task task = project.tasks.create(name: ASCIIDOCTOR, type: AsciidoctorTask) { + requires = ['asciidoctor-pdf'] + requires = ['slim', 'tilt'] + } + + then: + !systemOut.toString().contains('deprecated') + !task.requires.contains('asciidoctor-pdf') + task.requires.contains('tilt') + task.requires.contains('slim') + } + + @SuppressWarnings('MethodName') + def "Allow setting of sourceDir via method"() { + when: + Task task = project.tasks.create(name: ASCIIDOCTOR, type: AsciidoctorTask) { + sourceDir project.projectDir + } + + then: + !systemOut.toString().contains('deprecated') + task.sourceDir.absolutePath == project.projectDir.absolutePath + task.sourceDir.absolutePath == project.projectDir.absolutePath + } + + + @SuppressWarnings('MethodName') + def "When setting sourceDir via assignment"() { + when: + Task task = project.tasks.create(name: ASCIIDOCTOR, type: AsciidoctorTask) { + sourceDir = project.projectDir + } + + then: + task.sourceDir.absolutePath == project.projectDir.absolutePath + task.sourceDir.absolutePath == project.projectDir.absolutePath + + } + + @SuppressWarnings('MethodName') + def "When setting sourceDir via setSourceDir"() { + when: + Task task = project.tasks.create(name: ASCIIDOCTOR, type: AsciidoctorTask) { + sourceDir = project.projectDir + } + + then: + task.sourceDir.absolutePath == project.projectDir.absolutePath + task.sourceDir.absolutePath == project.projectDir.absolutePath + !systemOut.toString().contains('deprecated') + } + + @SuppressWarnings('MethodName') + def "Allow setting of gemPath via method"() { + when: + Task task = project.tasks.create(name: ASCIIDOCTOR, type: AsciidoctorTask) { + gemPath project.projectDir + } + + then: + !systemOut.toString().contains('deprecated') + task.gemPath.files[0].absolutePath == project.projectDir.absolutePath + } + + @SuppressWarnings('MethodName') + def "When setting gemPath via assignment"() { + when: + Task task = project.tasks.create(name: ASCIIDOCTOR, type: AsciidoctorTask) { + gemPath = project.projectDir + } + + then: + task.gemPath.files[0].absolutePath == project.projectDir.absolutePath + !systemOut.toString().contains('deprecated') + } + + @SuppressWarnings('MethodName') + def "When setting gemPath via setGemPath"() { + when: + Task task = project.tasks.create(name: ASCIIDOCTOR, type: AsciidoctorTask) { + gemPath = project.projectDir + } + + then: + task.gemPath.files[0].absolutePath == project.projectDir.absolutePath + !systemOut.toString().contains('deprecated') + } + + @SuppressWarnings('MethodName') + def "sourceDocumentNames should resolve descendant files of sourceDir if supplied as relatives"() { + when: 'I specify two files relative to sourceDir,including one in a subfolder' + Task task = project.tasks.create(name: ASCIIDOCTOR, type: AsciidoctorTask) { + sourceDir srcDir + sources { + include ASCIIDOC_SAMPLE_FILE + include ASCIIDOC_SAMPLE2_FILE + } + } + def fileCollection = task.sourceFileTree + + then: 'both files should be in collection, but any other files found in folder should be excluded' + fileCollection.contains(new File(srcDir, ASCIIDOC_SAMPLE_FILE).canonicalFile) + fileCollection.contains(new File(srcDir, ASCIIDOC_SAMPLE2_FILE).canonicalFile) + !fileCollection.contains(new File(srcDir, 'sample-docinfo.xml').canonicalFile) + fileCollection.files.size() == 2 + } + + @SuppressWarnings('MethodName') + @SuppressWarnings('DuplicateNumberLiteral') + def "Add asciidoctor task with multiple backends"() { + when: + Task task = project.tasks.create(name: ASCIIDOCTOR, type: AsciidoctorTask) { + resourceCopyProxy = mockCopyProxy + sourceDir = srcDir + outputDir = outDir + backends AsciidoctorBackend.DOCBOOK.id, AsciidoctorBackend.HTML5.id + } + + task.processAsciidocSources() + then: + 2 * mockAsciidoctor.renderFile(_, { Map map -> map.backend == AsciidoctorBackend.DOCBOOK.id }) + 2 * mockAsciidoctor.renderFile(_, { Map map -> map.backend == AsciidoctorBackend.HTML5.id }) + } + + @SuppressWarnings('MethodName') + @SuppressWarnings('DuplicateNumberLiteral') + def "Adds asciidoctor task with supported backend"() { + expect: + project.tasks.findByName(ASCIIDOCTOR) == null + when: + Task task = project.tasks.create(name: ASCIIDOCTOR, type: AsciidoctorTask) { + resourceCopyProxy = mockCopyProxy + sourceDir srcDir + outputDir = outDir + } + + task.processAsciidocSources() + then: + 2 * mockAsciidoctor.renderFile(_, { Map map -> map.backend == AsciidoctorBackend.HTML5.id }) + !systemOut.toString().contains('deprecated') + } + + @SuppressWarnings('MethodName') + @SuppressWarnings('DuplicateNumberLiteral') + def "Using setBackend should output a warning"() { + expect: + project.tasks.findByName(ASCIIDOCTOR) == null + when: + Task task = project.tasks.create(name: ASCIIDOCTOR, type: AsciidoctorTask) { + resourceCopyProxy = mockCopyProxy + sourceDir = srcDir + outputDir = outDir + backends AsciidoctorBackend.DOCBOOK.id + } + + task.processAsciidocSources() + then: + 2 * mockAsciidoctor.renderFile(_, { Map map -> map.backend == AsciidoctorBackend.DOCBOOK.id }) + } + + @SuppressWarnings('MethodName') + def "Adds asciidoctor task throws exception"() { + expect: + project.tasks.findByName(ASCIIDOCTOR) == null + when: + Task task = project.tasks.create(name: ASCIIDOCTOR, type: AsciidoctorTask) { + resourceCopyProxy = mockCopyProxy + sourceDir = srcDir + outputDir = outDir + } + + task.processAsciidocSources() + then: + mockAsciidoctor.renderFile(_, _) >> { throw new IllegalArgumentException() } + thrown(GradleException) + } + + @SuppressWarnings('MethodName') + def "Processes a single document given a value for sourceDocumentName"() { + when: + Task task = project.tasks.create(name: ASCIIDOCTOR, type: AsciidoctorTask) { + resourceCopyProxy = mockCopyProxy + sourceDir = srcDir + outputDir = outDir + sources { + include ASCIIDOC_SAMPLE_FILE + } + } + + task.processAsciidocSources() + then: + 1 * mockAsciidoctor.renderFile(_, _) + } + + @SuppressWarnings('MethodName') + def "Reuses asciidoctor instance for multiple tasks"() { + when: + (1..2).collect { + project.tasks.create(name: "asciidoctor$it", type: AsciidoctorTask) { + resourceCopyProxy = mockCopyProxy + sourceDir = srcDir + outputDir = new File(outDir, "dir$it") + sources { + include ASCIIDOC_SAMPLE_FILE + } + extensions {} + } + + }.each { + it.processAsciidocSources() + } + + then: + 2 * mockAsciidoctor.unregisterAllExtensions() + 2 * mockAsciidoctor.renderFile(_, _) + 2 * mockAsciidoctor.registerExtensions(_) + } + + @SuppressWarnings('MethodName') + def "Source documents in directories end up in the corresponding output directory"() { + given: + Task task = project.tasks.create(name: ASCIIDOCTOR, type: AsciidoctorTask) { + resourceCopyProxy = mockCopyProxy + sourceDir = srcDir + outputDir = outDir + separateOutputDirs = false + } + when: + task.processAsciidocSources() + then: + 1 * mockAsciidoctor.renderFile(new File(task.sourceDir, ASCIIDOC_SAMPLE2_FILE), { + it.to_dir == new File(task.outputDir, 'subdir').absolutePath + }) + 1 * mockAsciidoctor.renderFile(new File(task.sourceDir, ASCIIDOC_SAMPLE_FILE), { + it.to_dir == task.outputDir.absolutePath + }) + and: + 0 * mockAsciidoctor.renderFile(_, _) + } + + @SuppressWarnings('MethodName') + def "Should support String value for attributes option"() { + given: + Task task = project.tasks.create(name: ASCIIDOCTOR, type: AsciidoctorTask) { + resourceCopyProxy = mockCopyProxy + sourceDir = srcDir + outputDir = outDir + sources { + include ASCIIDOC_SAMPLE_FILE + } + options = [ + attributes: 'toc=right source-highlighter=coderay' + ] + } + when: + task.processAsciidocSources() + then: + 1 * mockAsciidoctor.renderFile(_, _) + } + + @SuppressWarnings('MethodName') + @SuppressWarnings('DuplicateStringLiteral') + def "Should support GString value for attributes option"() { + given: + Task task = project.tasks.create(name: ASCIIDOCTOR, type: AsciidoctorTask) { + resourceCopyProxy = mockCopyProxy + sourceDir = srcDir + outputDir = outDir + sources { + include ASCIIDOC_SAMPLE_FILE + } + def attrs = 'toc=right source-highlighter=coderay' + options = [ + attributes: "$attrs" + ] + } + when: + task.processAsciidocSources() + then: + 1 * mockAsciidoctor.renderFile(_, _) + } + + @SuppressWarnings('MethodName') + @SuppressWarnings('DuplicateStringLiteral') + def "Should support List value for attributes option"() { + given: + Task task = project.tasks.create(name: ASCIIDOCTOR, type: AsciidoctorTask) { + asciidoctor = mockAsciidoctor + resourceCopyProxy = mockCopyProxy + sourceDir = srcDir + outputDir = outDir + sources { + include ASCIIDOC_SAMPLE_FILE + } + def highlighter = 'coderay' + options = [ + attributes: ['toc=right', "source-highlighter=$highlighter"] + ] + } + when: + task.processAsciidocSources() + then: + 1 * mockAsciidoctor.renderFile(_, _) + } + + @SuppressWarnings('MethodName') + def "Throws exception when attributes embedded in options is an unsupported type"() { + when: + Task task = project.tasks.create(name: ASCIIDOCTOR, type: AsciidoctorTask) { + resourceCopyProxy = mockCopyProxy + sourceDir = srcDir + outputDir = outDir + sources { + include ASCIIDOC_SAMPLE_FILE + } + options = [ + attributes: 23 + ] + } + task.processAsciidocSources() + then: + thrown(InvalidUserDataException) + } + + @SuppressWarnings('MethodName') + def "Setting baseDir results in the correct value being sent to Asciidoctor"() { + given: + File basedir = new File(testRootDir, 'my_base_dir') + Task task = project.tasks.create(name: ASCIIDOCTOR, type: AsciidoctorTask) { + resourceCopyProxy = mockCopyProxy + sourceDir = srcDir + outputDir = outDir + baseDir = basedir + } + when: + task.processAsciidocSources() + then: + 1 * mockAsciidoctor.renderFile(new File(task.sourceDir, ASCIIDOC_SAMPLE_FILE), { + it.base_dir == basedir.absolutePath + }) + } + + @SuppressWarnings('MethodName') + def "Omitting a value for baseDir results in sending the dir of the processed file"() { + given: + Task task = project.tasks.create(name: ASCIIDOCTOR, type: AsciidoctorTask) { + asciidoctor = mockAsciidoctor + resourceCopyProxy = mockCopyProxy + sourceDir = srcDir + outputDir = outDir + } + when: + task.processAsciidocSources() + then: + 1 * mockAsciidoctor.renderFile(new File(task.sourceDir, ASCIIDOC_SAMPLE_FILE), + { it.base_dir == new File(task.sourceDir, ASCIIDOC_SAMPLE_FILE).parentFile.absolutePath }) + } + + @SuppressWarnings('MethodName') + def "Setting baseDir to null results in no value being sent to Asciidoctor"() { + given: + Task task = project.tasks.create(name: ASCIIDOCTOR, type: AsciidoctorTask) { + resourceCopyProxy = mockCopyProxy + sourceDir = srcDir + outputDir = outDir + baseDir = null + } + when: + task.processAsciidocSources() + then: + 1 * mockAsciidoctor.renderFile(new File(task.sourceDir, ASCIIDOC_SAMPLE_FILE), { !it.base_dir }) + } + + @SuppressWarnings('MethodName') + def "Safe mode option is equal to level of SafeMode.UNSAFE by default"() { + given: + Task task = project.tasks.create(name: ASCIIDOCTOR, type: AsciidoctorTask) { + resourceCopyProxy = mockCopyProxy + sourceDir = srcDir + outputDir = outDir + } + when: + task.processAsciidocSources() + then: + 1 * mockAsciidoctor.renderFile(new File(task.sourceDir, ASCIIDOC_SAMPLE_FILE), { + it.safe == SafeMode.UNSAFE.level + }) + } + + @SuppressWarnings('MethodName') + def "Safe mode configuration option as integer is honored"() { + given: + Task task = project.tasks.create(name: ASCIIDOCTOR, type: AsciidoctorTask) { + resourceCopyProxy = mockCopyProxy + sourceDir = srcDir + outputDir = outDir + options = [ + safe: SafeMode.SERVER.level + ] + } + when: + task.processAsciidocSources() + then: + 1 * mockAsciidoctor.renderFile(new File(task.sourceDir, ASCIIDOC_SAMPLE_FILE), { + it.safe == SafeMode.SERVER.level + }) + } + + @SuppressWarnings('MethodName') + def "Safe mode configuration option as string is honored"() { + given: + Task task = project.tasks.create(name: ASCIIDOCTOR, type: AsciidoctorTask) { + resourceCopyProxy = mockCopyProxy + sourceDir = srcDir + outputDir = outDir + options = [ + safe: 'server' + ] + } + when: + task.processAsciidocSources() + then: + 1 * mockAsciidoctor.renderFile(new File(task.sourceDir, ASCIIDOC_SAMPLE_FILE), { + it.safe == SafeMode.SERVER.level + }) + } + + @SuppressWarnings('MethodName') + def "Safe mode configuration option as enum is honored"() { + given: + Task task = project.tasks.create(name: ASCIIDOCTOR, type: AsciidoctorTask) { + resourceCopyProxy = mockCopyProxy + sourceDir = srcDir + outputDir = outDir + options = [ + safe: SafeMode.SERVER + ] + } + when: + task.processAsciidocSources() + then: + 1 * mockAsciidoctor.renderFile(new File(task.sourceDir, ASCIIDOC_SAMPLE_FILE), { + it.safe == SafeMode.SERVER.level + }) + } + + @SuppressWarnings('MethodName') + def "Attributes projectdir and rootdir are always set to relative dirs of the processed file"() { + given: + Task task = project.tasks.create(name: ASCIIDOCTOR, type: AsciidoctorTask) { + resourceCopyProxy = mockCopyProxy + sourceDir = srcDir + outputDir = outDir + } + when: + task.processAsciidocSources() + then: + 1 * mockAsciidoctor.renderFile(new File(task.sourceDir, ASCIIDOC_SAMPLE_FILE), { + it.attributes.projectdir == AsciidoctorUtils.getRelativePath(project.projectDir, new File(task.sourceDir, ASCIIDOC_SAMPLE_FILE).parentFile) + it.attributes.rootdir == AsciidoctorUtils.getRelativePath(project.rootDir, new File(task.sourceDir, ASCIIDOC_SAMPLE_FILE).parentFile) + }) + } + + @SuppressWarnings('MethodName') + def "Docinfo files are not copied to target directory"() { + given: + Task task = project.tasks.create(name: ASCIIDOCTOR, type: AsciidoctorTask) { + asciidoctor = mockAsciidoctor + resourceCopyProxy = mockCopyProxy + sourceDir = srcDir + outputDir = outDir + } + when: + task.processAsciidocSources() + then: + 1 * mockAsciidoctor.renderFile(new File(task.sourceDir, ASCIIDOC_SAMPLE_FILE), _) + !outDir.listFiles({ !it.directory && !(it.name =~ DOCINFO_FILE_PATTERN) } as FileFilter) + } + + @SuppressWarnings('MethodName') + def "Project coordinates are set automatically as attributes"() { + given: + project.version = '1.0.0-SNAPSHOT' + project.group = 'com.acme' + Task task = project.tasks.create(name: ASCIIDOCTOR, type: AsciidoctorTask) { + resourceCopyProxy = mockCopyProxy + sourceDir = srcDir + outputDir = outDir + } + when: + task.processAsciidocSources() + then: + 1 * mockAsciidoctor.renderFile(new File(task.sourceDir, ASCIIDOC_SAMPLE_FILE), { + it.attributes.'project-name' == 'test' && + it.attributes.'project-group' == 'com.acme' && + it.attributes.'project-version' == '1.0.0-SNAPSHOT' + }) + } + + @SuppressWarnings('MethodName') + def "Override project coordinates with explicit attributes"() { + given: + project.version = '1.0.0-SNAPSHOT' + project.group = 'com.acme' + Task task = project.tasks.create(name: ASCIIDOCTOR, type: AsciidoctorTask) { + resourceCopyProxy = mockCopyProxy + sourceDir = srcDir + outputDir = outDir + options = [ + attributes: [ + 'project-name' : 'awesome', + 'project-group' : 'unicorns', + 'project-version': '1.0.0.Final' + ] + ] + } + when: + task.processAsciidocSources() + then: + 1 * mockAsciidoctor.renderFile(new File(task.sourceDir, ASCIIDOC_SAMPLE_FILE), { + it.attributes.'project-name' == 'awesome' && + it.attributes.'project-group' == 'unicorns' && + it.attributes.'project-version' == '1.0.0.Final' + }) + } + + @SuppressWarnings('MethodName') + def "Should support multiple source documents if names are given"() { + given: + Task task = project.tasks.create(name: ASCIIDOCTOR, type: AsciidoctorTask) { + resourceCopyProxy = mockCopyProxy + sourceDir = srcDir + outputDir = outDir + sources { + include ASCIIDOC_SAMPLE_FILE, ASCIIDOC_SAMPLE2_FILE + } + } + when: + task.processAsciidocSources() + then: + 1 * mockAsciidoctor.renderFile(new File(task.sourceDir, ASCIIDOC_SAMPLE_FILE), _) + 1 * mockAsciidoctor.renderFile(new File(task.sourceDir, ASCIIDOC_SAMPLE2_FILE), _) + } + + @SuppressWarnings('MethodName') + def "Should throw exception if the file sourceDocumentName starts with underscore"() { + given: + Task task = project.tasks.create(name: ASCIIDOCTOR, type: AsciidoctorTask) { + resourceCopyProxy = mockCopyProxy + sourceDir = srcDir + outputDir = outDir + sources { + include ASCIIDOC_INVALID_FILE + } + } + when: + task.processAsciidocSources() + then: + 0 * mockAsciidoctor.renderFile(_, _) + thrown(GradleException) + } + + def "When 'resources' not specified, then copy all images to backend"() { + given: + Task task = project.tasks.create(name: ASCIIDOCTOR, type: AsciidoctorTask) { + resourceCopyProxy = mockCopyProxy + + sourceDir srcDir + outputDir outDir + backends AsciidoctorBackend.HTML5.id + + sources { + include ASCIIDOC_SAMPLE_FILE + } + + } + when: + task.processAsciidocSources() + then: + 1 * mockCopyProxy.copy(_, _) + } + + def "When 'resources' not specified and more than one backend, then copy all images to every backend"() { + given: + Task task = project.tasks.create(name: ASCIIDOCTOR, type: AsciidoctorTask) { + resourceCopyProxy = mockCopyProxy + + sourceDir srcDir + outputDir outDir + backends AsciidoctorBackend.HTML5.id, AsciidoctorBackend.DOCBOOK.id + + sources { + include ASCIIDOC_SAMPLE_FILE + } + + } + when: + task.processAsciidocSources() + then: + 1 * mockCopyProxy.copy(new File(outDir, AsciidoctorBackend.HTML5.id), _) + 1 * mockCopyProxy.copy(new File(outDir, AsciidoctorBackend.DOCBOOK.id), _) + } + + def "When 'resources' are specified, then copy according to all patterns"() { + given: + Task task = project.tasks.create(name: ASCIIDOCTOR, type: AsciidoctorTask) { + resourceCopyProxy = mockCopyProxy + + sourceDir srcDir + outputDir outDir + backends AsciidoctorBackend.HTML5.id, AsciidoctorBackend.DOCBOOK.id + + sources { + include ASCIIDOC_SAMPLE_FILE + } + + resources { + from(sourceDir) { + include 'images/**' + } + } + } + + when: + task.processAsciidocSources() + then: + 1 * mockCopyProxy.copy(new File(outDir, AsciidoctorBackend.HTML5.id), _) + 1 * mockCopyProxy.copy(new File(outDir, AsciidoctorBackend.DOCBOOK.id), _) + } + + def "sanity test for default configuration"() { + when: + Task task = project.tasks.create(name: ASCIIDOCTOR, type: AsciidoctorTask) + + then: + task.sourceDir.absolutePath.replace('\\', '/').endsWith('src/docs/asciidoc') + task.outputDir.absolutePath.replace('\\', '/').endsWith('build/asciidoc') + } + + def "set output dir to something else than the default"() { + given: + project.version = '1.0.0-SNAPSHOT' + project.group = 'com.acme' + + Task task = project.tasks.create(name: ASCIIDOCTOR, type: AsciidoctorTask) { + asciidoctor = mockAsciidoctor + resourceCopyProxy = mockCopyProxy + sourceDir = srcDir + } + project.buildDir = '/tmp/testbuild/asciidoctor' + + when: + task.processAsciidocSources() + + then: 'the html files must be in testbuild/asciidoctor ' + task.outputDir.absolutePath.replace('\\', '/').contains('testbuild/asciidoctor') + 2 * mockAsciidoctor.renderFile(_, { it.to_dir.startsWith(project.buildDir.absolutePath) }) + } + + def "Files in the resources copyspec should be recognised as input files"() { + given: + File imagesDir = new File(outDir, 'images') + File imageFile = new File(imagesDir, 'fake.txt') + imagesDir.mkdirs() + imageFile.text = 'foo' + + Task task = project.tasks.create(name: ASCIIDOCTOR, type: AsciidoctorTask) { + resourceCopyProxy = mockCopyProxy + + sourceDir srcDir + outputDir "${outDir}/foo" + backends AsciidoctorBackend.HTML5.id + + sources { + include ASCIIDOC_SAMPLE_FILE + } + + resources { + from(outDir) { + include 'images/**' + } + } + } + + when: + project.evaluate() + + then: + task.inputs.files.contains(project.file("${srcDir}/sample.asciidoc")) + task.inputs.files.contains(project.file("${imagesDir}/fake.txt")) + } + + def "GroovyStrings in options and attributes are converted into Strings"() { + given: + String variable = 'bar' + Task task = project.tasks.create(name: ASCIIDOCTOR, type: AsciidoctorTask) { + options = [ + template_dirs: ["${project.projectDir}/templates/haml"] + ] + attributes = [ + foo: "${variable}" + ] + resourceCopyProxy = mockCopyProxy + sourceDir srcDir + outputDir = outDir + } + + when: + task.processAsciidocSources() + + then: + 1 * mockAsciidoctor.renderFile(new File(task.sourceDir, ASCIIDOC_SAMPLE_FILE), { Map props -> + props.template_dirs*.class == [String] && + props.attributes.foo.class == String + }) + } + + @SuppressWarnings('MethodName') + def "Should require libraries"() { + expect: + project.tasks.findByName(ASCIIDOCTOR) == null + when: + Task task = project.tasks.create(name: ASCIIDOCTOR, type: AsciidoctorTask) { + resourceCopyProxy = mockCopyProxy + sourceDir = srcDir + outputDir = outDir + sources { + include ASCIIDOC_SAMPLE_FILE + } + requires 'asciidoctor-pdf' + } + + task.processAsciidocSources() + then: + 1 * mockAsciidoctor.requireLibrary('asciidoctor-pdf') + } + +} diff --git a/asciidoctor-gradle-jvm/src/test/groovy/org/asciidoctor/gradle/internal/AsciidoctorUtilsSpec.groovy b/asciidoctor-gradle-jvm/src/test/groovy/org/asciidoctor/gradle/internal/AsciidoctorUtilsSpec.groovy new file mode 100755 index 000000000..e757ba385 --- /dev/null +++ b/asciidoctor-gradle-jvm/src/test/groovy/org/asciidoctor/gradle/internal/AsciidoctorUtilsSpec.groovy @@ -0,0 +1,47 @@ +/* + * Copyright 2013-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.asciidoctor.gradle.internal + +import spock.lang.Specification +import spock.lang.Unroll + +class AsciidoctorUtilsSpec extends Specification { + + static String s = File.separator + + @Unroll + @SuppressWarnings(['MethodName','DuplicateStringLiteral']) + def 'find relative paths of #target to #base'() { + + expect: + AsciidoctorUtils.getRelativePath(new File(target), new File(base)) == relative + + where: + target | base | relative + 'src/test/groovy' | 'src' | "test${s}groovy" + 'src/test/groovy/' | 'src/' | "test${s}groovy" + 'src/test/groovy' | 'src/' | "test${s}groovy" + 'src/test/groovy/' | 'src' | "test${s}groovy" + 'src' | 'src/test/groovy' | "..${s}.." + 'src/' | 'src/test/groovy/' | "..${s}.." + 'src' | 'src/test/groovy/' | "..${s}.." + 'src/' | 'src/test/groovy' | "..${s}.." + 'src/test' | 'src/test' | '' + 'src/test/' | 'src/test/' | '' + 'src/test' | 'src/test/' | '' + 'src/test/' | 'src/test' | '' + } +} diff --git a/asciidoctor-gradle-jvm/src/test/groovy/org/asciidoctor/gradle/jvm/AsciidoctorJBasePluginSpec.groovy b/asciidoctor-gradle-jvm/src/test/groovy/org/asciidoctor/gradle/jvm/AsciidoctorJBasePluginSpec.groovy new file mode 100644 index 000000000..34a3676bb --- /dev/null +++ b/asciidoctor-gradle-jvm/src/test/groovy/org/asciidoctor/gradle/jvm/AsciidoctorJBasePluginSpec.groovy @@ -0,0 +1,92 @@ +/* + * Copyright 2013-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.asciidoctor.gradle.jvm + +import org.gradle.api.Project +import org.gradle.api.Task +import org.gradle.testfixtures.ProjectBuilder +import spock.lang.Specification + +@SuppressWarnings(['MethodName', 'DuplicateStringLiteral', 'DuplicateMapLiteral']) +class AsciidoctorJBasePluginSpec extends Specification { + + Project project = ProjectBuilder.builder().build() + + void 'Apply the plugin will add an extension for AsciidoctorJ'() { + when: + project.allprojects { + apply plugin: 'org.asciidoctor.jvm.base' + } + + AsciidoctorJExtension ext = project.extensions.getByName(AsciidoctorJExtension.NAME) + + then: + verifyAll { + ext.version == AsciidoctorJExtension.DEFAULT_ASCIIDOCTORJ_VERSION + ext.groovyDslVersion == null + ext.pdfVersion == null + ext.epubVersion == null + } + } + + void 'Adding extension will set GroovyDSL'() { + when: + project.allprojects { + apply plugin: 'org.asciidoctor.jvm.base' + } + + AsciidoctorJExtension ext = project.extensions.getByName(AsciidoctorJExtension.NAME) + + ext.extensions '1' + + then: + verifyAll { + ext.version == AsciidoctorJExtension.DEFAULT_ASCIIDOCTORJ_VERSION + ext.groovyDslVersion == AsciidoctorJExtension.DEFAULT_GROOVYDSL_VERSION + } + } + + void 'When the AsciidoctorJ extension is added to a task it defaults values to the one created by the plugin'() { + when: + Task taskWithExt + AsciidoctorJExtension taskExt + project.allprojects { + apply plugin: 'org.asciidoctor.jvm.base' + } + + taskWithExt = project.tasks.create('foo') + taskExt = taskWithExt.extensions.create(AsciidoctorJExtension.NAME, AsciidoctorJExtension, taskWithExt) + AsciidoctorJExtension ext = project.extensions.getByName(AsciidoctorJExtension.NAME) + + project.allprojects { + foo { + asciidoctorj { + version '1.2.3' + groovyDslVersion '4.5.6' + } + } + } + + then: + verifyAll { + ext.version == AsciidoctorJExtension.DEFAULT_ASCIIDOCTORJ_VERSION + ext.groovyDslVersion == null + taskExt.version == '1.2.3' + taskExt.groovyDslVersion == '4.5.6' + } + + } +} \ No newline at end of file diff --git a/asciidoctor-gradle-jvm/src/test/groovy/org/asciidoctor/gradle/jvm/AsciidoctorJPluginSpec.groovy b/asciidoctor-gradle-jvm/src/test/groovy/org/asciidoctor/gradle/jvm/AsciidoctorJPluginSpec.groovy new file mode 100644 index 000000000..7d4ca5147 --- /dev/null +++ b/asciidoctor-gradle-jvm/src/test/groovy/org/asciidoctor/gradle/jvm/AsciidoctorJPluginSpec.groovy @@ -0,0 +1,41 @@ +/* + * Copyright 2013-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.asciidoctor.gradle.jvm + +import org.gradle.api.Project +import org.gradle.testfixtures.ProjectBuilder +import spock.lang.Specification + +@SuppressWarnings(['MethodName', 'DuplicateStringLiteral', 'DuplicateMapLiteral']) +class AsciidoctorJPluginSpec extends Specification { + + Project project = ProjectBuilder.builder().build() + + @SuppressWarnings('Instanceof') + void 'Apply the plugin will add a task called asciidoctor'() { + when: + project.allprojects { + apply plugin: 'org.asciidoctor.jvm.convert' + } + + then: + verifyAll { + project.tasks.getByName('asciidoctor') instanceof AsciidoctorTask + } + } + + +} \ No newline at end of file diff --git a/asciidoctor-gradle-jvm/src/test/groovy/org/asciidoctor/gradle/jvm/AsciidoctorTaskSpec.groovy b/asciidoctor-gradle-jvm/src/test/groovy/org/asciidoctor/gradle/jvm/AsciidoctorTaskSpec.groovy new file mode 100755 index 000000000..1964314d0 --- /dev/null +++ b/asciidoctor-gradle-jvm/src/test/groovy/org/asciidoctor/gradle/jvm/AsciidoctorTaskSpec.groovy @@ -0,0 +1,403 @@ +/* + * Copyright 2013-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.asciidoctor.gradle.jvm + +import org.gradle.api.GradleException +import org.gradle.api.Project +import org.gradle.testfixtures.ProjectBuilder +import spock.lang.Specification + +/** + * Asciidoctor task specification + * + * @author Benjamin Muschko + * @author Stephan Classen + * @author Marcus Fihlon + * @author Schalk W. Cronjé + */ +@SuppressWarnings(['MethodName', 'DuplicateStringLiteral', 'DuplicateMapLiteral']) +class AsciidoctorTaskSpec extends Specification { + private static final String ASCIIDOCTOR = 'asciidoctor' + private static final String ASCIIDOC_RESOURCES_DIR = 'build/resources/test/src/asciidoc' + private static final String ASCIIDOC_BUILD_DIR = 'build/asciidoc' + private static final String ASCIIDOC_SAMPLE_FILE = 'sample.asciidoc' + private static final String ASCIIDOC_SAMPLE2_FILE = 'subdir/sample2.ad' + + Project project = ProjectBuilder.builder().withName('test').build() + + File testRootDir + File srcDir + File outDir + ByteArrayOutputStream systemOut + + PrintStream originSystemOut + + def setup() { + + project.allprojects { + apply plugin: 'org.asciidoctor.jvm.base' + } + + testRootDir = new File('.') + srcDir = new File(testRootDir, ASCIIDOC_RESOURCES_DIR).absoluteFile + outDir = new File(project.projectDir, ASCIIDOC_BUILD_DIR) + systemOut = new ByteArrayOutputStream() + originSystemOut = System.out + System.out = new PrintStream(systemOut) + } + + void cleanup() { + System.out = originSystemOut + } + + @SuppressWarnings('MethodName') + void "Allow setting of options via method"() { + when: + AsciidoctorTask task = asciidoctorTask { + options eruby: 'erb' + options eruby: 'erubis' + options doctype: 'book', toc: 'right' + } + + then: + !systemOut.toString().contains('deprecated') + task.options['eruby'] == 'erubis' + task.options['doctype'] == 'book' + task.options['toc'] == 'right' + } + + @SuppressWarnings('MethodName') + void "Allow setting of options via assignment"() { + when: + AsciidoctorTask task = asciidoctorTask { + options = [eruby: 'erb', toc: 'right'] + options = [eruby: 'erubis', doctype: 'book'] + } + + then: + !systemOut.toString().contains('deprecated') + task.options['eruby'] == 'erubis' + task.options['doctype'] == 'book' + !task.options.containsKey('toc') + } + + @SuppressWarnings('MethodName') + void "Allow setting of attributes via method (Map variant)"() { + when: + AsciidoctorTask task = asciidoctorTask { + attributes 'source-highlighter': 'foo' + attributes 'source-highlighter': 'coderay' + attributes idprefix: '$', idseparator: '-' + } + + then: + !systemOut.toString().contains('deprecated') + task.attributes['source-highlighter'] == 'coderay' + task.attributes['idprefix'] == '$' + task.attributes['idseparator'] == '-' + } + + @SuppressWarnings('MethodName') + void "Do not allow setting of attributes via legacy key=value list"() { + when: + asciidoctorTask { + attributes(['source-highlighter=foo', 'source-highlighter=coderay', 'idprefix=$', 'idseparator=-']) + } + + then: + thrown(MissingMethodException) + } + + @SuppressWarnings('MethodName') + void "Do not allow setting of attributes via legacy key-value string"() { + when: + asciidoctorTask { + attributes 'source-highlighter=foo source-highlighter=coderay idprefix=$ idseparator=-' + } + + then: + thrown(MissingMethodException) + } + + @SuppressWarnings('MethodName') + void "Allow setting of attributes via assignment"() { + when: + AsciidoctorTask task = asciidoctorTask { + attributes = ['source-highlighter': 'foo', idprefix: '$'] + attributes = ['source-highlighter': 'coderay', idseparator: '-'] + } + + then: + !systemOut.toString().contains('deprecated') + task.attributes['source-highlighter'] == 'coderay' + task.attributes['idseparator'] == '-' + !task.attributes.containsKey('idprefix') + } + + @SuppressWarnings('MethodName') + void "Mixing attributes with options, produces an exception"() { + when: + asciidoctorTask { + options eruby: 'erubis', attributes: ['source-highlighter': 'foo', idprefix: '$'] + options doctype: 'book', attributes: [idseparator: '-'] + } + + then: + thrown(GradleException) + } + + @SuppressWarnings('MethodName') + void "Mixing attributes with options (with assignment), produces an exception"() { + when: + Map tmpStore = [eruby: 'erubis', attributes: ['source-highlighter': 'foo', idprefix: '$']] + asciidoctorTask { + options = tmpStore + options = [doctype: 'book', attributes: [idseparator: '-']] + } + + then: + thrown(GradleException) + } + + @SuppressWarnings('MethodName') + void "Mixing string legacy form of attributes with options with assignment, produces an exception"() { + when: + asciidoctorTask { + options = [doctype: 'book', attributes: 'toc=right source-highlighter=coderay toc-title=Table\\ of\\ Contents'] + } + + then: + thrown(GradleException) + } + + @SuppressWarnings('MethodName') + void "Mixing list legacy form of attributes with options with assignment, produces an exception"() { + when: + asciidoctorTask { + options = [doctype: 'book', attributes: [ + 'toc=right', + 'source-highlighter=coderay', + 'toc-title=Table of Contents' + ]] + } + + then: + thrown(GradleException) + } + + @SuppressWarnings('MethodName') + void "Allow setting of backends via method"() { + given: + Set testBackends + + when: + asciidoctorTask { + outputOptions { + backends 'foo', 'bar' + backends 'pdf' + } + + outputOptions { + testBackends = backends + } + } + + then: + !systemOut.toString().contains('deprecated') + verifyAll { + testBackends.contains('pdf') + testBackends.contains('foo') + testBackends.contains('bar') + } + } + + @SuppressWarnings('MethodName') + void "Allow setting of backends via assignment"() { + given: + Set testBackends + + when: + asciidoctorTask { + outputOptions { + backends = ['pdf'] + backends = ['foo', 'bar'] + } + + outputOptions { + testBackends = backends + } + } + + then: + !systemOut.toString().contains('deprecated') + + verifyAll { + !testBackends.contains('pdf') + testBackends.contains('foo') + testBackends.contains('bar') + } + } + + @SuppressWarnings('MethodName') + void "Allow setting of requires via method"() { + when: + project.allprojects { + asciidoctorj { + requires 'asciidoctor-pdf' + } + } + AsciidoctorTask task = asciidoctorTask { + asciidoctorj { + requires 'slim', 'tilt' + } + } + + then: + !systemOut.toString().contains('deprecated') + task.asciidoctorj.requires.contains('asciidoctor-pdf') + task.asciidoctorj.requires.contains('tilt') + task.asciidoctorj.requires.contains('slim') + } + + @SuppressWarnings('MethodName') + void "Allow setting of requires via assignment"() { + when: + project.allprojects { + asciidoctorj { + requires 'asciidoctor-pdf' + } + } + AsciidoctorTask task = asciidoctorTask { + asciidoctorj { + requires = ['slim', 'tilt'] + } + } + + then: + !systemOut.toString().contains('deprecated') + !task.asciidoctorj.requires.contains('asciidoctor-pdf') + task.asciidoctorj.requires.contains('tilt') + task.asciidoctorj.requires.contains('slim') + } + + @SuppressWarnings('MethodName') + void "Allow setting of sourceDir via method"() { + when: + AsciidoctorTask task = asciidoctorTask { + sourceDir project.projectDir + } + + then: + !systemOut.toString().contains('deprecated') + task.sourceDir.absolutePath == project.projectDir.absolutePath + task.sourceDir.absolutePath == project.projectDir.absolutePath + } + + + @SuppressWarnings('MethodName') + void "When setting sourceDir via assignment"() { + when: + AsciidoctorTask task = asciidoctorTask { + sourceDir = project.projectDir + } + + then: + task.sourceDir.absolutePath == project.projectDir.absolutePath + task.sourceDir.absolutePath == project.projectDir.absolutePath + + } + + @SuppressWarnings('MethodName') + void "When setting sourceDir via setSourceDir"() { + when: + AsciidoctorTask task = asciidoctorTask { + sourceDir = project.projectDir + } + + then: + task.sourceDir.absolutePath == project.projectDir.absolutePath + task.sourceDir.absolutePath == project.projectDir.absolutePath + !systemOut.toString().contains('deprecated') + } + + @SuppressWarnings('MethodName') + void "Allow setting of gemPath via method"() { + when: + AsciidoctorTask task = asciidoctorTask { + asciidoctorj { + gemPaths project.projectDir + } + } + + then: + !systemOut.toString().contains('deprecated') + task.asciidoctorj.asGemPath() == project.projectDir.absolutePath + } + + @SuppressWarnings('MethodName') + void "When setting gemPath via assignment"() { + when: + AsciidoctorTask task = asciidoctorTask { + asciidoctorj { + gemPaths = [project.projectDir] + } + } + + then: + task.asciidoctorj.asGemPath() == project.projectDir.absolutePath + !systemOut.toString().contains('deprecated') + } + + @SuppressWarnings('MethodName') + void "When setting gemPath via setGemPaths"() { + when: + project.allprojects { + asciidoctorj { + gemPaths = [project.projectDir] + } + } + AsciidoctorTask task = asciidoctorTask { + } + + then: + task.asciidoctorj.asGemPath() == project.projectDir.absolutePath + !systemOut.toString().contains('deprecated') + } + + @SuppressWarnings('MethodName') + void "sourceDocumentNames should resolve descendant files of sourceDir if supplied as relatives"() { + when: 'I specify two files relative to sourceDir,including one in a subfolder' + AsciidoctorTask task = asciidoctorTask { + sourceDir srcDir + sources { + include ASCIIDOC_SAMPLE_FILE + include ASCIIDOC_SAMPLE2_FILE + } + } + def fileCollection = task.sourceFileTree + + then: 'both files should be in collection, but any other files found in folder should be excluded' + fileCollection.contains(new File(srcDir, ASCIIDOC_SAMPLE_FILE).canonicalFile) + fileCollection.contains(new File(srcDir, ASCIIDOC_SAMPLE2_FILE).canonicalFile) + !fileCollection.contains(new File(srcDir, 'sample-docinfo.xml').canonicalFile) + fileCollection.files.size() == 2 + } + + + AsciidoctorTask asciidoctorTask(Closure cfg) { + project.tasks.create(name: ASCIIDOCTOR, type: AsciidoctorTask).configure cfg + } +} diff --git a/build.gradle b/build.gradle index 243e2042b..2d58e192d 100644 --- a/build.gradle +++ b/build.gradle @@ -24,13 +24,17 @@ plugins { id 'com.github.kt3k.coveralls' version '2.8.2' apply false id 'net.ossindex.audit' version '0.1.1' apply false id 'org.kordamp.jdeps' version '0.2.0' apply false + id 'fi.linuxbox.download.worker' version '0.3' apply false + id 'org.ysb33r.ivypot' version '0.8' apply false + id 'org.ysb33r.os' version '0.9' apply false + id 'org.ysb33r.gradletest' version '2.0-alpha-6' apply false } subprojects { - apply plugin : 'groovy' apply plugin : 'idea' apply plugin : 'maven-publish' + apply plugin : 'org.ysb33r.ivypot' apply plugin : 'jacoco' apply plugin : 'codenarc' apply plugin : 'java-gradle-plugin' @@ -50,6 +54,9 @@ subprojects { repositories { jcenter() + if(version.endsWith('-SNAPSHOT')) { + mavenLocal() + } } dependencyUpdates.resolutionStrategy = { @@ -70,12 +77,9 @@ subprojects { } dependencies { + compile localGroovy() compile gradleApi() - testCompile "org.asciidoctor:asciidoctorj:$asciidoctorjVersion" - testCompile("org.asciidoctor:asciidoctorj-groovy-dsl:$asciidoctorjDslVersion") { - exclude group: 'org.codehaus.groovy', module: 'groovy-all' - exclude group: 'org.asciidoctor', module: 'asciidoctorj' - } + compile "org.ysb33r.gradle:grolifant:${grolifantVersion}" testCompile("org.spockframework:spock-core:$spockVersion") { exclude group:'org.codehaus.groovy' } @@ -95,6 +99,14 @@ subprojects { exclude group: 'org.hamcrest', module: 'hamcrest-core' } intTestRuntime files(createClasspathManifest) + + if( project.name != 'kindlegen-gradle' ) { +// testCompile "org.asciidoctor:asciidoctorj:$compileOnlyAsciidoctorJVersion" +// testCompile("org.asciidoctor:asciidoctorj-groovy-dsl:$asciidoctorjDslVersion") { +// exclude group: 'org.codehaus.groovy', module: 'groovy-all' +// exclude group: 'org.asciidoctor', module: 'asciidoctorj' +// } + } } compileGroovy { @@ -111,8 +123,8 @@ subprojects { header = "$project.name $project.version API" footer = "Copyright © ${copyrightYear} the original author or authors. All rights reserved." includePrivate = false - link 'https://docs.gradle.org/3.5/javadoc/', 'org.gradle.' - link 'http://docs.oracle.com/javase/7/docs/api/', 'java.', 'org.xml.', 'javax.', 'org.w3c.' + link "https://docs.gradle.org/${gradle.gradleVersion}/javadoc/", 'org.gradle.' + link 'http://docs.oracle.com/javase/8/docs/api/', 'java.', 'org.xml.', 'javax.', 'org.w3c.' } task sourcesJar(type: Jar) { @@ -155,4 +167,9 @@ subprojects { html.enabled = true } } + + if(project.name != 'asciidoctor-gradle-base') { + apply from : "${rootProject.projectDir}/gradle/compatibility-tests.gradle" + } } + diff --git a/gradle.properties b/gradle.properties index 2b813f265..6109329ad 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ -version = 1.6.0-SNAPSHOT +version = 1.6.0-alpha-1-SNAPSHOT group = org.asciidoctor -sourceCompatibility = 1.7 -targetCompatibility = 1.7 +sourceCompatibility = 1.8 +targetCompatibility = 1.8 copyrightYear = 2013-2018 project_pluginId = org.asciidoctor.convert @@ -14,9 +14,7 @@ bintray_org = asciidoctor bintray_repo = maven bintray_dryRun = false -asciidoctorjVersion = 1.5.6 -asciidoctorjDslVersion = 1.0.0.Alpha2 -#asciidoctorjDslVersion = 1.6.0-alpha.1 cglibVersion = 3.2.6 jsoupVersion = 1.11.2 spockVersion = 1.1-groovy-2.4 +grolifantVersion = 0.6 diff --git a/gradle/asciidoctorj-versions.gradle b/gradle/asciidoctorj-versions.gradle new file mode 100644 index 000000000..fa5409ab8 --- /dev/null +++ b/gradle/asciidoctorj-versions.gradle @@ -0,0 +1,24 @@ +ext { + pluginExtraText = (version.contains('-alpha') || version.contains('-beta')) ? + " (If you need a production-ready version of the AsciidoctorJ plugin for Gradle use a 1.5.x release of 'org.asciidoctor.convert' instead)" + : '' + + extensionFileContent = file("${project(':asciidoctor-gradle-jvm').projectDir}/src/main/groovy/org/asciidoctor/gradle/jvm/AsciidoctorJExtension.groovy").readLines() + + compileOnlyAsciidoctorJVersion = (extensionFileContent.grep { + it =~ /final\s+static\s+String\s+DEFAULT_ASCIIDOCTORJ_VERSION\s+=/ + })[0].replaceFirst(~/^(.+?["'])/, '').replaceFirst(~/(["'].*)$/, '') + + compileOnlyGroovyDslVersion = (extensionFileContent.grep { + it =~ /final\s+static\s+String\s+DEFAULT_GROOVYDSL_VERSION\s+=/ + })[0].replaceFirst(~/^(.+?["'])/, '').replaceFirst(~/(["'].*)$/, '') + + downloadOnlyPdfVersion = (extensionFileContent.grep { + it =~ /final\s+static\s+String\s+DEFAULT_PDF_VERSION\s+=/ + })[0].replaceFirst(~/^(.+?["'])/, '').replaceFirst(~/(["'].*)$/, '') + + downloadOnlyEpubVersion = (extensionFileContent.grep { + it =~ /final\s+static\s+String\s+DEFAULT_EPUB_VERSION\s+=/ + })[0].replaceFirst(~/^(.+?["'])/, '').replaceFirst(~/(["'].*)$/, '') + +} diff --git a/gradle/code-quality.gradle b/gradle/code-quality.gradle index bf3d455cb..55d98570c 100644 --- a/gradle/code-quality.gradle +++ b/gradle/code-quality.gradle @@ -27,7 +27,7 @@ license { } codenarc { - configFile = file('config/codenarc/codenarc.groovy') + configFile = file("${rootProject.projectDir}/config/codenarc/codenarc.groovy") // run codenarc on production sources only sourceSets = [project.sourceSets.main] } diff --git a/gradle/compatibility-tests.gradle b/gradle/compatibility-tests.gradle new file mode 100644 index 000000000..bae483c3e --- /dev/null +++ b/gradle/compatibility-tests.gradle @@ -0,0 +1,13 @@ +apply plugin : 'org.ysb33r.gradletest' + +gradleTest { + versions '4.0', '4.3', '4.6' + + mustRunAfter intTest + + enabled = !gradle.startParameter.offline +} + +configurations { + gradleTestRuntime.extendsFrom testRuntime +} \ No newline at end of file diff --git a/gradle/integration-tests.gradle b/gradle/integration-tests.gradle index e96c32119..51c5593c0 100644 --- a/gradle/integration-tests.gradle +++ b/gradle/integration-tests.gradle @@ -21,20 +21,67 @@ sourceSets { intTest } +configurations { + intTestOfflineRepo +} + +syncRemoteRepositories { + + repoRoot "${buildDir}/intTestOfflineRepo" + + ext { + repDescriptionFile = file("${repoRoot}/repositories.gradle") + } + + outputs.files repDescriptionFile + inputs.file "${rootProject.projectDir}/gradle/integration-tests.gradle" + + repositories { + jcenter() + + if (version.endsWith('-SNAPSHOT')) { + mavenLocal() + } + } + + configurations 'intTestOfflineRepo' + + doLast { + repDescriptionFile.withWriter { w -> + w.println 'repositories {' + w.print ' ' * 4 + w.println 'ivy {' + w.print ' ' * 8 + w.println "name 'OfflineRepo'" + w.print ' ' * 8 + w.println "layout 'gradle'" + w.print ' ' * 8 + w.println "url '${file(repoRoot).toURI()}'" + w.print ' ' * 4 + w.println '}' + w.println '}' + } + } + +} + // For a single test, you can run "gradle -DintTest.single= intTest" task intTest(type: Test) { description = "Runs the plugin's integration tests" group = "verification" mustRunAfter "test" - inputs.dir sourceSets.main.output.classesDir - inputs.dir sourceSets.main.output.resourcesDir + inputs.files sourceSets.main.output.classesDirs + inputs.files sourceSets.main.output.resourcesDir - testClassesDir = sourceSets.intTest.output.classesDir + testClassesDirs = sourceSets.intTest.output.classesDirs classpath = sourceSets.intTest.runtimeClasspath include "**/*Spec*" - exclude "**/Abstract*Spec*" +// exclude "**/Abstract*Spec*" + + dependsOn syncRemoteRepositories + systemProperties OFFLINE_REPO: syncRemoteRepositories.repDescriptionFile.parentFile.absolutePath } check.dependsOn intTest diff --git a/kindlegen-gradle/build.gradle b/kindlegen-gradle/build.gradle new file mode 100644 index 000000000..b0a7cec8c --- /dev/null +++ b/kindlegen-gradle/build.gradle @@ -0,0 +1,55 @@ +import fi.linuxbox.gradle.download.worker.DownloadWorkerTask + + +apply plugin: 'fi.linuxbox.download.worker' +apply plugin : 'org.ysb33r.os' + +ext { + kindleGenCacheDir = file("${buildDir}/kindlegen-binaries") + + kindleGenPackageName = { -> + fileContent = file("src/main/groovy/org/asciidoctor/gradle/kindlegen/KindleGenExtension.groovy").readLines() + + kindleGenVer = (fileContent.grep { + it =~ /final\s+static\s+String\s+DEFAULT_KINDLEGEN_VERSION\s+=/ + })[0].replaceFirst(~/^(.+?["'])/, '').replaceFirst(~/(["'].*)$/, '') + + String fileName = OS.macOsX ? 'KindleGen' : 'kindlegen' + String fileExt = OS.linux ? 'tar.gz' : 'zip' + String fileOs = OS.linux ? 'linux_2.6_i386' : (OS.windows ? 'win32' : 'Mac_i386') + + "${fileName}_${fileOs}_v${kindleGenVer}.${fileExt}" + }.call() + + kindleGenBinaryUrl = "http://kindlegen.s3.amazonaws.com/${kindleGenPackageName}" +} + +configurations { + kindlegen +} + +task cacheKindleGenBinary( type : DownloadWorkerTask ) { + from kindleGenBinaryUrl + to "${kindleGenCacheDir}/${kindleGenPackageName}" + + doFirst { + logger.info("Caching ${kindleGenBinaryUrl}") + } + + enabled = OS.windows || OS.linux || OS.macOsX + onlyIf { ! gradle.startParameter.offline } +} + +dependencies { + testRuntimeClasspath files(createClasspathManifest) +} + +test { + systemProperties 'org.asciidoctor.gradle.kindlegen.uri' : kindleGenCacheDir.toURI() + + dependsOn cacheKindleGenBinary, createClasspathManifest +} + +artifacts { + kindlegen file("${kindleGenCacheDir}/${kindleGenPackageName}") +} \ No newline at end of file diff --git a/kindlegen-gradle/src/main/groovy/org/asciidoctor/gradle/kindlegen/KindleGenBasePlugin.groovy b/kindlegen-gradle/src/main/groovy/org/asciidoctor/gradle/kindlegen/KindleGenBasePlugin.groovy new file mode 100644 index 000000000..5edfcea3e --- /dev/null +++ b/kindlegen-gradle/src/main/groovy/org/asciidoctor/gradle/kindlegen/KindleGenBasePlugin.groovy @@ -0,0 +1,19 @@ +package org.asciidoctor.gradle.kindlegen + +import groovy.transform.CompileStatic +import org.gradle.api.Plugin +import org.gradle.api.Project + +/** Create the {@code kindlegen} extension. + * + * @since 1.6.0 + * @author Schalk W. Cronjé + */ +@CompileStatic +class KindleGenBasePlugin implements Plugin { + + void apply(Project project) { + project.extensions.create(KindleGenExtension.NAME, KindleGenExtension, project) + } +} + diff --git a/kindlegen-gradle/src/main/groovy/org/asciidoctor/gradle/kindlegen/KindleGenDownloader.groovy b/kindlegen-gradle/src/main/groovy/org/asciidoctor/gradle/kindlegen/KindleGenDownloader.groovy new file mode 100644 index 000000000..a9cf9c8f3 --- /dev/null +++ b/kindlegen-gradle/src/main/groovy/org/asciidoctor/gradle/kindlegen/KindleGenDownloader.groovy @@ -0,0 +1,77 @@ +package org.asciidoctor.gradle.kindlegen + +import groovy.transform.CompileStatic +import org.gradle.api.GradleException +import org.gradle.api.Project +import org.ysb33r.grolifant.api.AbstractDistributionInstaller +import org.ysb33r.grolifant.api.OperatingSystem +import org.ysb33r.grolifant.api.errors.DistributionFailedException +import org.ysb33r.grolifant.api.os.Linux +import org.ysb33r.grolifant.api.os.MacOsX +import org.ysb33r.grolifant.api.os.Windows + +/** + * @since 1.6.0 + */ +@CompileStatic +class KindleGenDownloader extends AbstractDistributionInstaller { + + static final OperatingSystem OS = OperatingSystem.current() + static final String BASE_URI = System.getProperty('org.asciidoctor.gradle.kindlegen.uri') ?: 'http://kindlegen.s3.amazonaws.com' + static final String KINDLEGEN_BASE = 'kindlegen' + + KindleGenDownloader( String distributionVersion, Project project) { + super(KINDLEGEN_BASE, distributionVersion, 'native-binaries', project) + } + + /** Creates a download URI from a given distribution version + * + * @param version Version of the distribution to download + * @return + */ + @Override + URI uriFromVersion(String version) { + switch(OS) { + case Windows: + return "${BASE_URI}/kindlegen_win32_v${version}.zip".toURI() + case Linux: + return "${BASE_URI}/kindlegen_linux_2.6_i386_v${version}.tar.gz".toURI() + case MacOsX: + return "${BASE_URI}/KindleGen_Mac_i386_v${version}.zip".toURI() + default: + throw new GradleException('Kindlegen downloads are only supported on Windows, MacOS & Linux') + } + } + + /** Validates that the unpacked distribution is good. + * + *

The default implementation simply checks that only one directory should exist and then uses that. You should override this + * method if your distribution in question does not follow the common practice of one top-level directory. + * + * @param distDir Directory where distribution was unpacked to. + * @param distributionDescription A descriptive name of the distribution + * @return The directory where the real distribution now exists. In the default implementation it will be + * the single directory that exists below {@code distDir}. + * + * @throw {@link org.ysb33r.grolifant.api.errors.DistributionFailedException} if distribution failed to meet criteria. + */ + @Override + protected File getAndVerifyDistributionRoot(File distDir, String distributionDescription) { + if(!new File( distDir, kindleGenFileName)) { + throw new DistributionFailedException("${kindleGenFileName} not found in ${distDir}") + } + distDir + } + + private String getKindleGenFileName() { + switch(OS) { + case Windows: + return "${KINDLEGEN_BASE}.exe" + case Linux: + return KINDLEGEN_BASE + case MacOsX: + return KINDLEGEN_BASE + } + } + +} diff --git a/kindlegen-gradle/src/main/groovy/org/asciidoctor/gradle/kindlegen/KindleGenExtension.groovy b/kindlegen-gradle/src/main/groovy/org/asciidoctor/gradle/kindlegen/KindleGenExtension.groovy new file mode 100644 index 000000000..38e43ceff --- /dev/null +++ b/kindlegen-gradle/src/main/groovy/org/asciidoctor/gradle/kindlegen/KindleGenExtension.groovy @@ -0,0 +1,76 @@ +package org.asciidoctor.gradle.kindlegen + +import groovy.transform.CompileStatic +import org.gradle.api.GradleException +import org.gradle.api.Project +import org.gradle.api.Task +import org.ysb33r.grolifant.api.exec.AbstractToolExtension +import org.ysb33r.grolifant.api.exec.NamedResolvedExecutableFactory +import org.ysb33r.grolifant.api.exec.ResolvableExecutable +import org.ysb33r.grolifant.api.exec.ResolveExecutableByVersion +import org.ysb33r.grolifant.api.os.Linux +import org.ysb33r.grolifant.api.os.MacOsX +import org.ysb33r.grolifant.api.os.Windows + +import static org.asciidoctor.gradle.kindlegen.KindleGenDownloader.KINDLEGEN_BASE + +/** Extension for configuring the path to {@code kindlegen} or a version which will then + * be bootstrapped by Gradle. + * + * @since 1.6.0 + * @author Schalk W. Cronjé + */ +@CompileStatic +class KindleGenExtension extends AbstractToolExtension { + + final static String NAME = 'kindlegen' + final static String DEFAULT_KINDLEGEN_VERSION = '2_9' + + KindleGenExtension(Project project) { + super(project) + resolverFactoryRegistry.registerExecutableKeyActions(resolverFactory(project)) + executable version: DEFAULT_KINDLEGEN_VERSION + } + + KindleGenExtension(Task task) { + super(task, NAME) + resolverFactoryRegistry.registerExecutableKeyActions(resolverFactory(task.project)) + } + + /** Explicitly configure to agree to Amazon Terms of Usage + * + * @sa {@link https://www.amazon.com/gp/feature.html?docId=1000599251} + * + */ + boolean agreeToTermsOfUse = false + + /** Obtain a lazy-evaluated object to resolve a path to an executable. + * + * @return An object for which will resolve a path on-demand. + */ + @Override + ResolvableExecutable getResolvableExecutable() { + if (!agreeToTermsOfUse) { + throw new GradleException('You need to agree to Amazon\'s terms of usage for KindleGen. Set kindlegen.agreeToTermsOfUse=true. For more details on the ToU see https://www.amazon.com/gp/feature.html?docId=1000599251') + } + super.resolvableExecutable + } + + private static NamedResolvedExecutableFactory resolverFactory(Project project) { + new ResolveExecutableByVersion( + project, { Map options, String version, Project p -> + new KindleGenDownloader(version, project) + } as ResolveExecutableByVersion.DownloaderFactory, + { KindleGenDownloader downloader -> + switch (KindleGenDownloader.OS) { + case Windows: + return new File(downloader.distributionRoot, "${KINDLEGEN_BASE}.exe") + case Linux: + case MacOsX: + return new File(downloader.distributionRoot, KINDLEGEN_BASE) + default: + null + } + } as ResolveExecutableByVersion.DownloadedExecutable) + } +} diff --git a/kindlegen-gradle/src/main/resources/META-INF/gradle-plugins/org.asciidoctor.kindlegen.base.properties b/kindlegen-gradle/src/main/resources/META-INF/gradle-plugins/org.asciidoctor.kindlegen.base.properties new file mode 100644 index 000000000..421c4d90e --- /dev/null +++ b/kindlegen-gradle/src/main/resources/META-INF/gradle-plugins/org.asciidoctor.kindlegen.base.properties @@ -0,0 +1 @@ +implementation-class=org.asciidoctor.gradle.kindlegen.KindleGenBasePlugin \ No newline at end of file diff --git a/kindlegen-gradle/src/test/groovy/org/asciidoctor/gradle/kindlegen/KindleGenFunctionalSpec.groovy b/kindlegen-gradle/src/test/groovy/org/asciidoctor/gradle/kindlegen/KindleGenFunctionalSpec.groovy new file mode 100644 index 000000000..f5e52e720 --- /dev/null +++ b/kindlegen-gradle/src/test/groovy/org/asciidoctor/gradle/kindlegen/KindleGenFunctionalSpec.groovy @@ -0,0 +1,55 @@ +package org.asciidoctor.gradle.kindlegen + +import org.asciidoctor.gradle.kindlegen.internal.FunctionalSpecification +import org.gradle.api.Project +import org.gradle.testfixtures.ProjectBuilder +import org.gradle.testkit.runner.BuildResult +import org.ysb33r.grolifant.api.OperatingSystem + +@SuppressWarnings('MethodName') +class KindleGenFunctionalSpec extends FunctionalSpecification { + + void 'Downloader can download and unpack kindlegen'() { + given: + Project project = ProjectBuilder.builder().build() + OperatingSystem os = OperatingSystem.current() + String version = KindleGenExtension.DEFAULT_KINDLEGEN_VERSION + String baseName = os.macOsX ? 'KindleGen' : 'kindlegen' + String fileExt = os.linux ? 'tar.gz' : 'zip' + String fileOs = os.linux ? 'linux_2.6_i386' : (os.windows ? 'win32' : 'Mac_i386') + String fileName = "${baseName}_${fileOs}_v${version}.${fileExt}" + KindleGenDownloader downloader = new KindleGenDownloader(version, project) + + when: + File root = downloader.distributionRoot + + then: + new File(root, fileName).exists() + } + + void 'Download and cache kindlegen'() { + + given: + new File(testProjectDir.root, 'build.gradle').text = """ +plugins { + id 'org.asciidoctor.kindlegen.base' +} + +kindlegen { + agreeToTermsOfUse = true +} + +task getKindleGen { + doLast { + println kindlegen.resolvableExecutable.executable + } +} +""" + + when: + BuildResult result = getGradleRunner(['getKindleGen', '-i']).build() + + then: + result.output.contains('/kindlegen') + } +} \ No newline at end of file diff --git a/kindlegen-gradle/src/test/groovy/org/asciidoctor/gradle/kindlegen/internal/FunctionalSpecification.groovy b/kindlegen-gradle/src/test/groovy/org/asciidoctor/gradle/kindlegen/internal/FunctionalSpecification.groovy new file mode 100644 index 000000000..d71f4f6cf --- /dev/null +++ b/kindlegen-gradle/src/test/groovy/org/asciidoctor/gradle/kindlegen/internal/FunctionalSpecification.groovy @@ -0,0 +1,51 @@ +/* + * Copyright 2013-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.asciidoctor.gradle.kindlegen.internal + +import org.gradle.testkit.runner.GradleRunner +import org.junit.Rule +import org.junit.rules.TemporaryFolder +import spock.lang.Shared +import spock.lang.Specification + +class FunctionalSpecification extends Specification { + @Rule + final TemporaryFolder testProjectDir = new TemporaryFolder() + + @Shared + List pluginClasspath + + def setupSpec() { + def pluginClasspathResource = getClass().classLoader.getResource('plugin-classpath.txt') + if (pluginClasspathResource == null) { + pluginClasspathResource = new File('./build/createClasspathManifest/plugin-classpath.txt') + } + if (pluginClasspathResource == null) { + throw new IllegalStateException('Did not find plugin classpath resource, run `intTestClasses` build task.') + } + + pluginClasspath = pluginClasspathResource.readLines().collect { new File(it) } + } + + GradleRunner getGradleRunner(List taskNames) { + GradleRunner.create() + .withProjectDir(testProjectDir.root) + .withArguments(taskNames) + .withPluginClasspath(pluginClasspath) + .forwardOutput() + .withDebug(true) + } +} \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index db1b6a3d3..3cd4b9e6b 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,2 +1,5 @@ include 'asciidoctor-gradle-base' include 'asciidoctor-gradle-jvm' +include 'kindlegen-gradle' +include 'asciidoctor-gradle-jvm-epub' +// include 'asciidoctor-gradle-js' \ No newline at end of file