How to Implement Custom Template Processors in Quarkdown's Project Creator
Implement custom template processors by creating a class that implements ProjectCreatorTemplateProcessorFactory, injecting your custom placeholders into JTE templates, and registering the factory in CreateProjectCommand to extend the quarkdown create wizard.
The quarkdown create command in the iamgio/quarkdown repository bootstraps new projects by rendering JTE templates through the Project Creator wizard. When you need to scaffold projects with custom layouts, additional configuration files, or organization-specific defaults, implementing custom template processors lets you inject arbitrary metadata into .qd.jte templates without modifying the core engine.
Understanding the Template Processing Architecture
The Project Creator delegates all template rendering to a thin abstraction layer over com.quarkdown.core.template.TemplateProcessor. To build a custom processor, you extend specific factory interfaces and wire them into the CLI entry point.
Core Components You Will Extend
ProjectCreatorTemplateProcessorFactory– The interface that definescreate(), returning a configuredTemplateProcessorinstance. Located inquarkdown-cli/src/main/kotlin/com/quarkdown/cli/creator/template/ProjectCreatorTemplateProcessorFactory.kt.DefaultProjectCreatorTemplateProcessorFactory– The reference implementation that populates standard placeholders likename,description, andauthors. Found inquarkdown-cli/src/main/kotlin/com/quarkdown/cli/creator/template/DefaultProjectCreatorTemplateProcessorFactory.kt.ProjectCreatorTemplatePlaceholders– An object containing string constants for placeholder names (e.g.,NAME,DESCRIPTION). Located inquarkdown-cli/src/main/kotlin/com/quarkdown/cli/creator/template/ProjectCreatorTemplatePlaceholders.kt.TemplateProcessor– The low-level builder class that feeds values into JTE viaoptionalValue(),conditional(), anditerable(). Defined inquarkdown-core/src/main/kotlin/com/quarkdown/core/template/TemplateProcessor.kt.CreateProjectCommand– The CLI handler that selects which factory to instantiate based on user flags. Found inquarkdown-cli/src/main/kotlin/com/quarkdown/cli/creator/command/CreateProjectCommand.kt.
Creating a Custom JTE Template
Place your template in the CLI module's resources to ensure it is packaged with the executable.
- Create the file at
quarkdown-cli/src/main/resources/creator/custom.qd.jte. - Use JTE syntax compatible with
TemplateProcessor:- Values:
${PLACEHOLDER} - Conditionals:
@if(PLACEHOLDER) … @endif - Iterables:
@for(item in PLACEHOLDER)${item}@endfor
- Values:
.doctype {plain}
.docname {${name}}
@if(description)
.docdesc {${description}}
@endif
@for(author in AUTHORS)
.author {${author}}
@endfor
@if(license)
.license {${license}}
@endif
- Add any static assets (images, CSS) adjacent to the template; the resource supplier copies them to the generated project during scaffolding.
Implementing a Custom Factory
Create a Kotlin class that implements ProjectCreatorTemplateProcessorFactory. You can extend the default factory to inherit standard placeholder wiring, or implement the interface directly for full control.
Minimal Example: Swapping the Template Path
This implementation delegates placeholder injection to DefaultProjectCreatorTemplateProcessorFactory while only changing the template resource:
package com.quarkdown.cli.creator.template
import com.quarkdown.core.document.DocumentInfo
import com.quarkdown.core.template.TemplateProcessor
private const val CUSTOM_TEMPLATE = "/creator/custom.qd.jte"
class CustomProjectCreatorTemplateProcessorFactory(
private val info: DocumentInfo,
) : ProjectCreatorTemplateProcessorFactory {
override fun create(): TemplateProcessor =
DefaultProjectCreatorTemplateProcessorFactory(info, CUSTOM_TEMPLATE).create()
}
Adding New Placeholders
To inject custom data (e.g., a license field), extend the placeholder constants and manually configure the TemplateProcessor:
- Add the constant to
ProjectCreatorTemplatePlaceholders.kt:
const val LICENSE = "license"
- Implement the factory with explicit value injection:
class CustomProjectCreatorTemplateProcessorFactory(
private val info: DocumentInfo,
private val license: String? = null,
) : ProjectCreatorTemplateProcessorFactory {
override fun create(): TemplateProcessor =
with(ProjectCreatorTemplatePlaceholders) {
TemplateProcessor.fromResourceName("/creator/custom.qd.jte").apply {
optionalValue(NAME, info.name)
optionalValue(DESCRIPTION, info.description)
conditional(KEYWORDS, info.keywords.isNotEmpty())
iterable(KEYWORDS, info.keywords)
conditional(AUTHORS, info.authors.isNotEmpty())
iterable(AUTHORS, info.authors.map { it.name })
optionalValue(TYPE, info.type.quarkdownName)
conditional(IS_DOCS, info.type == DocumentType.DOCS)
// Custom placeholder injection
optionalValue(LICENSE, license)
}
}
}
Wiring the Factory into the CLI
Modify CreateProjectCommand.kt to instantiate your factory when specific flags are present. The command currently branches between default and docs factories:
val processorFactory = if (isDocs) {
DocsProjectCreatorTemplateProcessorFactory(documentInfo)
} else {
DefaultProjectCreatorTemplateProcessorFactory(documentInfo)
}
Add a new branch for your custom implementation:
val isCustom = parsedOptions.hasFlag("custom")
val customLicense = parsedOptions.getOptionValue("license")
val processorFactory = when {
isDocs -> DocsProjectCreatorTemplateProcessorFactory(documentInfo)
isCustom -> CustomProjectCreatorTemplateProcessorFactory(documentInfo, customLicense)
else -> DefaultProjectCreatorTemplateProcessorFactory(documentInfo)
}
After registering the flag in your CLI options definition, running quarkdown create --custom --license MIT generates a project using your custom template with the ${license} placeholder resolved to "MIT".
Complete Working Example
File Structure
quarkdown-cli/
└─ src/main/
├─ resources/creator/
│ └─ custom.qd.jte
└─ kotlin/com/quarkdown/cli/creator/template/
├─ CustomProjectCreatorTemplateProcessorFactory.kt
└─ ProjectCreatorTemplatePlaceholders.kt
Custom Factory Implementation
package com.quarkdown.cli.creator.template
import com.quarkdown.core.document.DocumentInfo
import com.quarkdown.core.document.DocumentType
import com.quarkdown.core.template.TemplateProcessor
class CustomProjectCreatorTemplateProcessorFactory(
private val info: DocumentInfo,
private val license: String? = null,
) : ProjectCreatorTemplateProcessorFactory {
override fun create(): TemplateProcessor =
with(ProjectCreatorTemplatePlaceholders) {
TemplateProcessor.fromResourceName("/creator/custom.qd.jte").apply {
optionalValue(NAME, info.name)
optionalValue(DESCRIPTION, info.description)
conditional(KEYWORDS, info.keywords.isNotEmpty())
iterable(KEYWORDS, info.keywords)
conditional(AUTHORS, info.authors.isNotEmpty())
iterable(AUTHORS, info.authors.map { it.name })
optionalValue(TYPE, info.type.quarkdownName)
conditional(IS_DOCS, info.type == DocumentType.DOCS)
optionalValue(LANGUAGE, info.locale?.displayName)
conditional(HAS_THEME, info.theme?.hasComponent == true)
optionalValue(COLOR_THEME, info.theme?.color)
optionalValue(LAYOUT_THEME, info.theme?.layout)
conditional(USE_PAGE_COUNTER, info.type == DocumentType.PAGED)
// Custom injection
optionalValue(LICENSE, license)
}
}
}
CLI Integration
// In CreateProjectCommand.kt
val isCustom = parsedOptions.hasFlag("custom")
val licenseValue = parsedOptions.getOptionValue("license")
val processorFactory = when {
isDocs -> DocsProjectCreatorTemplateProcessorFactory(documentInfo)
isCustom -> CustomProjectCreatorTemplateProcessorFactory(documentInfo, licenseValue)
else -> DefaultProjectCreatorTemplateProcessorFactory(documentInfo)
}
Summary
- Custom template processors in Quarkdown wrap
TemplateProcessorto inject metadata into JTE templates during project creation. - Implement
ProjectCreatorTemplateProcessorFactoryand overridecreate()to configure placeholders viaoptionalValue(),conditional(), anditerable(). - Delegate to
DefaultProjectCreatorTemplateProcessorFactoryto reuse standard placeholder logic while swapping template paths. - Register your factory in
CreateProjectCommand.ktby adding CLI flags and conditional instantiation logic. - Store templates in
quarkdown-cli/src/main/resources/creator/and reference them viaTemplateProcessor.fromResourceName().
Frequently Asked Questions
Can I generate multiple files with a single custom processor?
Yes. Override createFilenameMappings() instead of create() to return a Map<String, TemplateProcessor> where each key is a target filename and each value is a configured processor. This lets you scaffold auxiliary files like READMEs or configuration files alongside the main .qd document according to the iamgio/quarkdown source architecture.
How do I access command-line options inside my custom factory?
Pass parsed option values as constructor parameters when instantiating the factory in CreateProjectCommand.kt. The factory receives a DocumentInfo object by default; for custom flags (like --license), extract the values in the command handler and inject them into your factory's constructor before calling create().
What JTE syntax features does TemplateProcessor support?
TemplateProcessor supports standard JTE value interpolation with ${PLACEHOLDER}, conditional blocks with @if(PLACEHOLDER)…@endif, and iteration with @for(item in PLACEHOLDER)${item}@endfor. These map directly to the optionalValue(), conditional(), and iterable() methods in TemplateProcessor.kt.
Is it possible to extend the default factory without copying all placeholder logic?
Yes. Compose your custom factory by accepting a DefaultProjectCreatorTemplateProcessorFactory as a delegate or by calling its create() method with a custom template path parameter. This preserves all standard injections (name, description, authors, themes) while allowing you to override only the template location or append additional placeholders afterward.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →