collectIncludes

Johannes Thorn PacoVK rmi2hi Jody Winter Jody Winter Ralf D. Müller Schrotti Tim Riemer Alexander Schwartz Julian Elve Ralf D. Mueller

2 minutes to read

About This Task

This task crawls through your entire project looking for AsciiDoc files with a specific name pattern, then creates a single AsciiDoc file which includes only those files.

When you create modular documentation, most includes are static. For example, the arc42-template has 12 chapters and a master template that includes those 12 chapters.

Normally when you work with dynamic modules like ADRs (Architecture Decision Records) you create those files on the fly. Maybe not within your /src/docs folder, but alongside the code file for which you wrote the ADR. In order to include these files in your documentation, you have to add the file with its whole relative path to one of your AsciiDoc files.

This task will handle it for you!

Just stick to this file-naming pattern ^[A-Za-z]{3,}[-_].* (begin with at least three letters and a dash/underscore) and this task will collect the file and write it to your build folder. You only have to include this generated file from within your documentation. If you provide templates for the documents, those templates are skipped if the name matches the pattern ^.\*[-\_][tT]emplate [-\_].*.

The Optional Parameter Configurations

You can configure which files are found by the script be setting the paramters in the Config.groovy file.

collectIncludes = [:]

collectIncludes.with {

    fileFilter = "adoc" // define which files are considered. default: "ad|adoc|asciidoc"

    minPrefixLength = "3" // define what minimum length the prefix. default: "3"

    maxPrefixLength = "3" // define what maximum length the prefix. default: ""

    separatorChar = "_" // define the allowed separators after prefix. default: "-_"

    cleanOutputFolder = true // should the output folder be emptied before generation? default: false

    excludeDirectories = [] // define additional directories that should not be traversed.

}

Example

You have a file called:

/src/java/yourCompany/domain/books/ADR-1-whyWeUseTheAISINInsteadOFISBN.adoc

The task will collect this file and write another file called:

/build/docs/_includes/ADR_includes.adoc

…​which will look like this:

include::../../../src/java/yourCompany/domain/books/ADR-1-whyWeUseTheAISINInsteadOFISBN.adoc[]

Obviously, you’ll reap the most benefits if the task has several ADR files to collect. 😎

You can then include these files in your main documentation by using a single include:

include::{targetDir}/docs/_includes/ADR_includes.adoc[]

Source

scripts/collectIncludes.gradle
import static groovy.io.FileType.*
import static groovy.io.FileVisitResult.*
import java.security.MessageDigest

task collectIncludes(
        description: 'collect all ADRs as includes in one file',
        group: 'docToolchain'
) {
    doFirst {
        boolean cleanOutputFolder = config.collectIncludes.cleanOutputFolder?:false
        String outputFolder = targetDir + '/_includes'
        if (cleanOutputFolder){
            delete fileTree(outputFolder)
        }

        new File(outputFolder).mkdirs()
    }
    doLast {
        //let's search the whole project for files, not only the docs folder
        //exclude typical system folders
        final defaultExcludedDirectories = [
            '.svn', '.git', '.idea', 'node_modules', '.gradle', 'build', '.github'
        ]

        //running as subproject? set scandir to main project
        String scanDir_save = scanDir
        if (project.name!=rootProject.name && scanDir=='.') {
            scanDir = project(':').projectDir.path
        }
        if (docDir.startsWith('.')) {
            docDir = file(new File(projectDir, docDir).canonicalPath)
        }
        logger.info "docToolchain> docDir: ${docDir}"
        logger.info "docToolchain> scanDir: ${scanDir}"
        if (scanDir.startsWith('.')) {
            scanDir = file(new File(docDir, scanDir).canonicalPath)
        } else {
            scanDir = file(new File(scanDir, "").canonicalPath)
        }
        logger.info "docToolchain> scanDir: ${scanDir}"

        logger.info "docToolchain> includeRoot: ${includeRoot}"

        if (includeRoot.startsWith('.')) {
            includeRoot = file(new File(docDir, includeRoot).canonicalPath)
        }
        logger.info "docToolchain> includeRoot: ${includeRoot}"

        File sourceFolder = scanDir
        println "sourceFolder: " + sourceFolder.canonicalPath
        def collections = [:]

        String fileFilter = config.collectIncludes.fileFilter?:"ad|adoc|asciidoc"
        String minPrefixLength = config.collectIncludes.minPrefixLength?:"3"
        String maxPrefixLength = config.collectIncludes.maxPrefixLength?:""
        String separatorChar = config.collectIncludes.separatorChar?:"-_"
        def extraExcludeDirectories = config.collectIncludes.excludeDirectories?:[]

        def excludedDirectories = defaultExcludedDirectories + extraExcludeDirectories

        String prefixRegEx = "[A-Za-z]{" + minPrefixLength + "," + maxPrefixLength + "}"
        String separatorCharRegEx = "[" + separatorChar + "]"
        String fileFilterRegEx = "^" + prefixRegEx + separatorCharRegEx + ".*[.](" + fileFilter + ")\$"

        logger.info "considering files with this pattern: " + fileFilterRegEx

        sourceFolder.traverse(
            type: FILES,
            preDir : { if (it.name in excludedDirectories) return SKIP_SUBTREE },
            excludeNameFilter: excludedDirectories
        ) { file ->
            if (file.name ==~ fileFilterRegEx) {
                String typeRegEx = "^(" + prefixRegEx + ")" + separatorCharRegEx + ".*\$"
                def type = file.name.replaceAll(typeRegEx,'\$1').toUpperCase()
                if (!collections[type]) {
                    collections[type] = []
                }
                logger.info "file: " + file.canonicalPath
                def fileName = (file.canonicalPath - scanDir.canonicalPath)[1..-1]
                if (file.name ==~ '^.*[Tt]emplate.*$') {
                    logger.info "ignore template file: " + fileName
                } else {
                    String includeFileRegEx = "^.*" + prefixRegEx + "_includes.adoc\$"
                    if (file.name ==~ includeFileRegEx) {
                        logger.info "ignore generated _includes files: " + fileName
                    } else {
                        if ( fileName.startsWith('docToolchain') || fileName.replace("\\", "/").matches('^.*/docToolchain/.*$')) {
                            //ignore docToolchain as submodule
                        } else {
                            logger.info "include corrected file: " + fileName
                            collections[type] << fileName
                        }
                    }
                }
            }
        }
        println "targetFolder: " + (targetDir - docDir)
        logger.info "targetDir - includeRoot: " + (targetDir - includeRoot)
        def pathDiff = '../' * ((targetDir - docDir)
                                    .replaceAll('^/','')
                                    .replaceAll('/$','')
                                    .replaceAll("[^/]",'').size()+1)

        logger.info "pathDiff: " + pathDiff
        collections.each { type, fileNames ->
            if (fileNames) {
                def outFile = new File(targetDir + '/_includes', type + '_includes.adoc')
                logger.info outFile.canonicalPath-sourceFolder.canonicalPath
                outFile.write("// this is autogenerated\n")
                logger.info "docToolchain> Use Antora integration: ${useAntoraIntegration}"
                fileNames.sort().each { fileName ->
                    if (useAntoraIntegration) {
                        outFile.append("ifndef::optimize-content[]\n")
                        outFile.append ("include::../" + pathDiff + scanDir_save + "/" + fileName.replace("\\", "/")+"[]\n")
                        outFile.append("endif::optimize-content[]\n\n")
                        outFile.append("ifdef::optimize-content[]\n")
                        outFile.append ("include::example\$" + fileName.replace("\\", "/").replace("${inputPath}/modules/ROOT/examples/", "")+"[]\n")
                        outFile.append("endif::optimize-content[]\n\n")
                    } else {
                        outFile.append ("include::../" + pathDiff + scanDir_save + "/" + fileName.replace("\\", "/")+"[]\n\n")
                    }
                }
            }
        }
    }
}