Major work for v0.2.0; moved from lib to api; tests all working.

This commit is contained in:
JesseBrault0709 2023-04-25 18:32:10 +02:00
parent 0ef4a3aafd
commit db5d00bdca
143 changed files with 4503 additions and 196 deletions

View File

@ -15,5 +15,10 @@ Here will be kept all of the various todos for this project, organized by releas
// as well as some other information, perhaps such as the Type, extension, *etc.*
```
- [ ] Add `extensionUtil` object to dsl.
- [ ] Investigate imports, including static, in scripts
- [ ] Get rid of `taskTypes` DSL, replace with static import of task types to scripts
- [ ] Plan out plugin system such that we can create custom providers of texts, data, etc.
- [ ] Plan out `data` models DSL
- [ ] Provide a way to override `ssgBuilds` variables from the cli.
### Fix

22
api/build.gradle Normal file
View File

@ -0,0 +1,22 @@
plugins {
id 'ssg.common'
}
repositories {
mavenCentral()
}
dependencies {
// https://mvnrepository.com/artifact/org.apache.groovy/groovy-templates
implementation 'org.apache.groovy:groovy-templates:4.0.9'
// https://mvnrepository.com/artifact/org.commonmark/commonmark
implementation 'org.commonmark:commonmark:0.21.0'
// https://mvnrepository.com/artifact/org.commonmark/commonmark-ext-yaml-front-matter
implementation 'org.commonmark:commonmark-ext-yaml-front-matter:0.21.0'
}
jar {
archivesBaseName = 'ssg-api'
}

View File

@ -0,0 +1,9 @@
package com.jessebrault.ssg
import com.jessebrault.ssg.buildscript.Build
import com.jessebrault.ssg.task.Task
import com.jessebrault.ssg.util.Result
interface BuildTasksConverter {
Result<Collection<Task>> convert(Build buildScriptResult)
}

View File

@ -0,0 +1,33 @@
package com.jessebrault.ssg
import com.jessebrault.ssg.buildscript.Build
import com.jessebrault.ssg.task.Task
import com.jessebrault.ssg.task.TaskSpec
import com.jessebrault.ssg.util.Diagnostic
import com.jessebrault.ssg.util.Result
final class SimpleBuildTasksConverter implements BuildTasksConverter {
@Override
Result<Collection<Task>> convert(Build buildScriptResult) {
def taskSpec = new TaskSpec(
buildScriptResult.name,
buildScriptResult.outputDirFunction.apply(buildScriptResult).file,
buildScriptResult.siteSpec,
buildScriptResult.globals
)
Collection<Task> tasks = []
Collection<Diagnostic> diagnostics = []
buildScriptResult.taskFactorySpecs.each {
def factory = it.supplier.get()
it.configureClosures.each { it(factory) }
def result = factory.getTasks(taskSpec)
diagnostics.addAll(result.diagnostics)
tasks.addAll(result.get())
}
Result.of(diagnostics, tasks)
}
}

View File

@ -0,0 +1,35 @@
package com.jessebrault.ssg
import groovy.transform.EqualsAndHashCode
import groovy.transform.NullCheck
import groovy.transform.TupleConstructor
@TupleConstructor(defaults = false)
@NullCheck(includeGenerated = true)
@EqualsAndHashCode
final class SiteSpec {
static SiteSpec getBlank() {
new SiteSpec('', '')
}
static SiteSpec concat(SiteSpec s0, SiteSpec s1) {
new SiteSpec(
s1.name.blank ? s0.name : s1.name,
s1.baseUrl.blank ? s0.baseUrl : s1.baseUrl
)
}
final String name
final String baseUrl
SiteSpec plus(SiteSpec other) {
concat(this, other)
}
@Override
String toString() {
"SiteSpec(${ this.name }, ${ this.baseUrl })"
}
}

View File

@ -0,0 +1,93 @@
package com.jessebrault.ssg.buildscript
import com.jessebrault.ssg.SiteSpec
import com.jessebrault.ssg.task.TaskFactorySpec
import groovy.transform.EqualsAndHashCode
import groovy.transform.NullCheck
import groovy.transform.TupleConstructor
import java.util.function.Function
@TupleConstructor(defaults = false)
@NullCheck(includeGenerated = true)
@EqualsAndHashCode
final class Build {
@TupleConstructor(defaults = false)
@NullCheck(includeGenerated = true)
@EqualsAndHashCode
static final class AllBuilds {
static AllBuilds concat(AllBuilds ab0, AllBuilds ab1) {
new AllBuilds(
ab0.siteSpec + ab1.siteSpec,
ab0.globals + ab1.globals,
ab0.taskFactorySpecs + ab1.taskFactorySpecs
)
}
static AllBuilds getEmpty() {
new AllBuilds(SiteSpec.getBlank(), [:], [])
}
final SiteSpec siteSpec
final Map<String, Object> globals
final Collection<TaskFactorySpec> taskFactorySpecs
AllBuilds plus(AllBuilds other) {
concat(this, other)
}
}
static Build getEmpty() {
new Build(
'',
OutputDirFunctions.DEFAULT,
SiteSpec.getBlank(),
[:],
[]
)
}
static Build get(Map<String, Object> args) {
new Build(
args?.name as String ?: '',
args?.outputDirFunction as Function<Build, OutputDir> ?: OutputDirFunctions.DEFAULT,
args?.siteSpec as SiteSpec ?: SiteSpec.getBlank(),
args?.globals as Map<String, Object> ?: [:],
args?.taskFactorySpecs as Collection<TaskFactorySpec> ?: []
)
}
static Build concat(Build b0, Build b1) {
new Build(
b1.name.blank ? b0.name : b1.name,
OutputDirFunctions.concat(b0.outputDirFunction, b1.outputDirFunction),
SiteSpec.concat(b0.siteSpec, b1.siteSpec),
b0.globals + b1.globals,
b0.taskFactorySpecs + b1.taskFactorySpecs
)
}
static Build from(AllBuilds allBuilds) {
new Build(
'',
OutputDirFunctions.DEFAULT,
allBuilds.siteSpec,
allBuilds.globals,
allBuilds.taskFactorySpecs
)
}
final String name
final Function<Build, OutputDir> outputDirFunction
final SiteSpec siteSpec
final Map<String, Object> globals
final Collection<TaskFactorySpec> taskFactorySpecs
Build plus(Build other) {
concat(this, other)
}
}

View File

@ -0,0 +1,62 @@
package com.jessebrault.ssg.buildscript
import com.jessebrault.ssg.buildscript.Build.AllBuilds
import com.jessebrault.ssg.buildscript.dsl.AllBuildsDelegate
import com.jessebrault.ssg.buildscript.dsl.BuildDelegate
abstract class BuildScriptBase extends Script {
private final Collection<AllBuildsDelegate> allBuildsDelegates = []
private final Collection<BuildDelegate> buildDelegates = []
private int currentBuildNumber = 0
final AllBuilds defaultAllBuilds = AllBuilds.getEmpty()
void build(
@DelegatesTo(value = BuildDelegate, strategy = Closure.DELEGATE_FIRST)
Closure<Void> buildClosure
) {
this.build('build' + this.currentBuildNumber, buildClosure)
}
void build(
String name,
@DelegatesTo(value = BuildDelegate, strategy = Closure.DELEGATE_FIRST)
Closure<Void> buildClosure
) {
def d = new BuildDelegate().tap {
it.name = name
}
buildClosure.setDelegate(d)
buildClosure.setResolveStrategy(Closure.DELEGATE_FIRST)
buildClosure()
this.buildDelegates << d
this.currentBuildNumber++
}
void allBuilds(
@DelegatesTo(value = AllBuildsDelegate, strategy = Closure.DELEGATE_FIRST)
Closure<Void> allBuildsClosure
) {
def d = new AllBuildsDelegate()
allBuildsClosure.setDelegate(d)
allBuildsClosure.setResolveStrategy(Closure.DELEGATE_FIRST)
allBuildsClosure()
this.allBuildsDelegates << d
}
Collection<Build> getBuilds() {
def allBuilds = this.defaultAllBuilds
this.allBuildsDelegates.each {
allBuilds += it.getResult()
}
def baseBuild = Build.from(allBuilds)
this.buildDelegates.collect {
baseBuild + it.getResult()
}
}
}

View File

@ -0,0 +1,7 @@
package com.jessebrault.ssg.buildscript
import java.util.function.Consumer
interface BuildScriptConfiguratorFactory {
Consumer<BuildScriptBase> get()
}

View File

@ -0,0 +1,39 @@
package com.jessebrault.ssg.buildscript
import groovy.transform.NullCheck
import org.codehaus.groovy.control.CompilerConfiguration
import org.codehaus.groovy.control.customizers.ImportCustomizer
import java.util.function.Consumer
@NullCheck
final class BuildScriptUtil {
// TODO: check exactly what we are importing to the script automatically
// TODO: check the roots arg, do we include 'ssgBuilds'/'buildSrc' dir eventually?
static Collection<Build> runBuildScript(String relativePath, Consumer<BuildScriptBase> configureBuildScript) {
def engine = new GroovyScriptEngine([new File('.').toURI().toURL()] as URL[])
engine.config = new CompilerConfiguration().tap {
addCompilationCustomizers(new ImportCustomizer().tap {
addStarImports(
'com.jessebrault.ssg',
'com.jessebrault.ssg.part',
'com.jessebrault.ssg.page',
'com.jessebrault.ssg.template',
'com.jessebrault.ssg.text',
'com.jessebrault.ssg.util'
)
})
scriptBaseClass = 'com.jessebrault.ssg.buildscript.BuildScriptBase'
}
def buildScript = engine.createScript(relativePath, new Binding())
assert buildScript instanceof BuildScriptBase
configureBuildScript.accept(buildScript)
buildScript()
buildScript.getBuilds()
}
private BuildScriptUtil() {}
}

View File

@ -0,0 +1,97 @@
package com.jessebrault.ssg.buildscript
import com.jessebrault.ssg.html.PageToHtmlTaskFactory
import com.jessebrault.ssg.html.TextToHtmlSpec
import com.jessebrault.ssg.html.TextToHtmlTaskFactory
import com.jessebrault.ssg.page.PageTypes
import com.jessebrault.ssg.page.PagesProviders
import com.jessebrault.ssg.part.Part
import com.jessebrault.ssg.part.PartTypes
import com.jessebrault.ssg.provider.CollectionProviders
import com.jessebrault.ssg.template.TemplatesProviders
import com.jessebrault.ssg.text.TextsProviders
import com.jessebrault.ssg.template.Template
import com.jessebrault.ssg.template.TemplateTypes
import com.jessebrault.ssg.text.TextTypes
import com.jessebrault.ssg.util.ExtensionUtil
import com.jessebrault.ssg.util.PathUtil
import com.jessebrault.ssg.util.Result
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import java.util.function.Consumer
final class DefaultBuildScriptConfiguratorFactory implements BuildScriptConfiguratorFactory {
private static final Logger logger = LoggerFactory.getLogger(DefaultBuildScriptConfiguratorFactory)
@Override
Consumer<BuildScriptBase> get() {
return {
it.allBuilds {
types {
textTypes << TextTypes.MARKDOWN
pageTypes << PageTypes.GSP
templateTypes << TemplateTypes.GSP
partTypes << PartTypes.GSP
//noinspection GroovyUnnecessaryReturn
return
}
providers { types ->
texts(TextsProviders.from(new File('texts'), types.textTypes))
pages(PagesProviders.from(new File('pages'), types.pageTypes))
templates(TemplatesProviders.from(new File('templates'), types.templateTypes))
parts(CollectionProviders.from(new File('parts')) { File file ->
def extension = ExtensionUtil.getExtension(file.path)
def partType = types.partTypes.find { it.ids.contains(extension) }
if (!partType) {
logger.warn('there is no PartType for file {}; skipping', file)
null
} else {
new Part(PathUtil.relative('parts', file.path), partType, file.getText())
}
})
}
taskFactories { sp ->
register('textToHtml', TextToHtmlTaskFactory::new) {
it.specProvider += CollectionProviders.from {
def templates = sp.templatesProvider.provide()
sp.textsProvider.provide().collect {
def frontMatterResult = it.type.frontMatterGetter.get(it)
if (frontMatterResult.hasDiagnostics()) {
return Result.ofDiagnostics(frontMatterResult.diagnostics)
}
def templateValue = frontMatterResult.get().get('template')
if (templateValue) {
def template = templates.find { it.path == templateValue }
return Result.of(new TextToHtmlSpec(it, template, it.path))
} else {
return null
}
}
}
it.allTextsProvider += sp.textsProvider
it.allPartsProvider += sp.partsProvider
//noinspection GroovyUnnecessaryReturn
return
}
register('pageToHtml', PageToHtmlTaskFactory::new) {
it.pagesProvider += sp.pagesProvider
it.allTextsProvider += sp.textsProvider
it.allPartsProvider += sp.partsProvider
//noinspection GroovyUnnecessaryReturn
return
}
}
}
}
}
}

View File

@ -0,0 +1,32 @@
package com.jessebrault.ssg.buildscript
import groovy.transform.EqualsAndHashCode
import org.jetbrains.annotations.Nullable
@EqualsAndHashCode
final class OutputDir {
static OutputDir concat(OutputDir od0, OutputDir od1) {
new OutputDir(od1.path ? od1.path : od0.path)
}
@Nullable
final String path
OutputDir(@Nullable String path) {
this.path = path
}
OutputDir(File file) {
this.path = file.path
}
File getFile() {
this.path ? new File(this.path) : new File('')
}
OutputDir plus(OutputDir other) {
concat(this, other)
}
}

View File

@ -0,0 +1,36 @@
package com.jessebrault.ssg.buildscript
import groovy.transform.stc.ClosureParams
import groovy.transform.stc.SimpleType
import java.util.function.Function
final class OutputDirFunctions {
static final Function<Build, OutputDir> DEFAULT = of { new OutputDir(it.name) }
static Function<Build, OutputDir> concat(
Function<Build, OutputDir> f0,
Function<Build, OutputDir> f1
) {
f1 == OutputDirFunctions.DEFAULT ? f0 : f1
}
static Function<Build, OutputDir> of(
@ClosureParams(value = SimpleType, options = 'com.jessebrault.ssg.buildscript.Build')
Closure<OutputDir> closure
) {
closure as Function<Build, OutputDir>
}
static Function<Build, OutputDir> of(File dir) {
of { new OutputDir(dir) }
}
static Function<Build, OutputDir> of(String path) {
of { new OutputDir(path) }
}
private OutputDirFunctions() {}
}

View File

@ -0,0 +1,64 @@
package com.jessebrault.ssg.buildscript
import com.jessebrault.ssg.model.Model
import com.jessebrault.ssg.page.Page
import com.jessebrault.ssg.part.Part
import com.jessebrault.ssg.provider.CollectionProvider
import com.jessebrault.ssg.provider.CollectionProviders
import com.jessebrault.ssg.template.Template
import com.jessebrault.ssg.text.Text
import groovy.transform.EqualsAndHashCode
import groovy.transform.NullCheck
import groovy.transform.TupleConstructor
@TupleConstructor(defaults = false)
@NullCheck(includeGenerated = true)
@EqualsAndHashCode
final class SourceProviders {
static SourceProviders concat(SourceProviders sp0, SourceProviders sp1) {
new SourceProviders(
sp0.textsProvider + sp1.textsProvider,
sp0.modelsProvider + sp1.modelsProvider,
sp0.pagesProvider + sp1.pagesProvider,
sp0.templatesProvider + sp1.templatesProvider,
sp0.partsProvider + sp1.partsProvider
)
}
static SourceProviders get(Map<String, Object> args) {
new SourceProviders(
args?.textsProvider as CollectionProvider<Text>
?: CollectionProviders.getEmpty() as CollectionProvider<Text>,
args?.modelsProvider as CollectionProvider<Model<Object>>
?: CollectionProviders.getEmpty() as CollectionProvider<Model<Object>>,
args?.pagesProvider as CollectionProvider<Page>
?: CollectionProviders.getEmpty() as CollectionProvider<Page>,
args?.templatesProvider as CollectionProvider<Template>
?: CollectionProviders.getEmpty() as CollectionProvider<Template>,
args?.partsProvider as CollectionProvider<Part>
?: CollectionProviders.getEmpty() as CollectionProvider<Part>
)
}
static SourceProviders getEmpty() {
new SourceProviders(
CollectionProviders.getEmpty(),
CollectionProviders.getEmpty(),
CollectionProviders.getEmpty(),
CollectionProviders.getEmpty(),
CollectionProviders.getEmpty()
)
}
final CollectionProvider<Text> textsProvider
final CollectionProvider<Model<Object>> modelsProvider
final CollectionProvider<Page> pagesProvider
final CollectionProvider<Template> templatesProvider
final CollectionProvider<Part> partsProvider
SourceProviders plus(SourceProviders other) {
concat(this, other)
}
}

View File

@ -0,0 +1,38 @@
package com.jessebrault.ssg.buildscript
import com.jessebrault.ssg.page.PageType
import com.jessebrault.ssg.part.PartType
import com.jessebrault.ssg.template.TemplateType
import com.jessebrault.ssg.text.TextType
import groovy.transform.EqualsAndHashCode
import groovy.transform.NullCheck
import groovy.transform.TupleConstructor
@TupleConstructor(defaults = false)
@NullCheck(includeGenerated = true)
@EqualsAndHashCode
final class TypesContainer {
static TypesContainer getEmpty() {
new TypesContainer([], [], [], [])
}
static TypesContainer concat(TypesContainer tc0, TypesContainer tc1) {
new TypesContainer(
tc0.textTypes + tc1.textTypes,
tc0.pageTypes + tc1.pageTypes,
tc0.templateTypes + tc1.templateTypes,
tc0.partTypes + tc1.partTypes
)
}
Collection<TextType> textTypes
Collection<PageType> pageTypes
Collection<TemplateType> templateTypes
Collection<PartType> partTypes
TypesContainer plus(TypesContainer other) {
concat(this, other)
}
}

View File

@ -0,0 +1,111 @@
package com.jessebrault.ssg.buildscript.dsl
import com.jessebrault.ssg.SiteSpec
import com.jessebrault.ssg.buildscript.SourceProviders
import com.jessebrault.ssg.buildscript.TypesContainer
import com.jessebrault.ssg.task.TaskFactorySpec
import groovy.transform.stc.ClosureParams
import groovy.transform.stc.SimpleType
abstract class AbstractBuildDelegate<T> {
private final Collection<Closure<Void>> siteSpecClosures = []
private final Collection<Closure<Void>> globalsClosures = []
private final Collection<Closure<Void>> typesClosures = []
private final Collection<Closure<Void>> sourcesClosures = []
private final Collection<Closure<Void>> taskFactoriesClosures = []
abstract T getResult()
protected final SiteSpec getSiteSpecResult() {
this.siteSpecClosures.inject(SiteSpec.getBlank()) { acc, closure ->
def d = new SiteSpecDelegate()
closure.delegate = d
closure.resolveStrategy = DELEGATE_FIRST
closure()
acc + d.getResult()
}
}
protected final Map<String, Object> getGlobalsResult() {
this.globalsClosures.inject([:] as Map<String, Object>) { acc, closure ->
def d = new GlobalsDelegate()
closure.delegate = d
closure.resolveStrategy = DELEGATE_FIRST
closure()
acc + d.getResult()
}
}
protected final TypesContainer getTypesResult() {
this.typesClosures.inject(TypesContainer.getEmpty()) { acc, closure ->
def d = new TypesDelegate()
closure.delegate = d
closure.resolveStrategy = DELEGATE_FIRST
closure()
acc + d.getResult()
}
}
protected final SourceProviders getSourcesResult(TypesContainer typesContainer) {
this.sourcesClosures.inject(SourceProviders.getEmpty()) { acc, closure ->
def d = new SourceProvidersDelegate()
closure.delegate = d
closure.resolveStrategy = DELEGATE_FIRST
closure(typesContainer)
acc + d.getResult()
}
}
protected final Collection<TaskFactorySpec> getTaskFactoriesResult(SourceProviders sourceProviders) {
this.taskFactoriesClosures.inject([:] as Map<String, TaskFactorySpec>) { acc, closure ->
def d = new TaskFactoriesDelegate()
closure.delegate = d
closure.resolveStrategy = DELEGATE_FIRST
closure(sourceProviders)
def specs = d.getResult()
specs.forEach { name, spec ->
acc.merge(name, spec) { spec0, spec1 -> spec0 + spec1 }
}
acc
}.values()
}
void siteSpec(
@DelegatesTo(value = SiteSpecDelegate, strategy = Closure.DELEGATE_FIRST)
Closure<Void> siteSpecClosure
) {
this.siteSpecClosures << siteSpecClosure
}
void globals(
@DelegatesTo(value = GlobalsDelegate, strategy = Closure.DELEGATE_FIRST)
Closure<Void> globalsClosure
) {
this.globalsClosures << globalsClosure
}
void types(
@DelegatesTo(value = TypesDelegate, strategy = Closure.DELEGATE_FIRST)
Closure<Void> typesClosure
) {
this.typesClosures << typesClosure
}
void providers(
@DelegatesTo(value = SourceProvidersDelegate, strategy = Closure.DELEGATE_FIRST)
@ClosureParams(value = SimpleType, options = 'com.jessebrault.ssg.buildscript.TypesContainer')
Closure<Void> providersClosure
) {
this.sourcesClosures << providersClosure
}
void taskFactories(
@DelegatesTo(value = TaskFactoriesDelegate, strategy = Closure.DELEGATE_FIRST)
@ClosureParams(value = SimpleType, options = 'com.jessebrault.ssg.buildscript.SourceProviders')
Closure<Void> taskFactoriesClosure
) {
this.taskFactoriesClosures << taskFactoriesClosure
}
}

View File

@ -0,0 +1,16 @@
package com.jessebrault.ssg.buildscript.dsl
import com.jessebrault.ssg.buildscript.Build
final class AllBuildsDelegate extends AbstractBuildDelegate<Build.AllBuilds> {
@Override
Build.AllBuilds getResult() {
new Build.AllBuilds(
this.getSiteSpecResult(),
this.getGlobalsResult(),
this.getTaskFactoriesResult(this.getSourcesResult(this.getTypesResult()))
)
}
}

View File

@ -0,0 +1,38 @@
package com.jessebrault.ssg.buildscript.dsl
import com.jessebrault.ssg.buildscript.Build
import com.jessebrault.ssg.buildscript.OutputDir
import com.jessebrault.ssg.buildscript.OutputDirFunctions
import org.jetbrains.annotations.Nullable
import java.util.function.Function
final class BuildDelegate extends AbstractBuildDelegate<Build> {
String name = ''
private Function<Build, OutputDir> outputDirFunction = OutputDirFunctions.DEFAULT
@Override
Build getResult() {
new Build(
this.name,
this.outputDirFunction,
this.getSiteSpecResult(),
this.getGlobalsResult(),
this.getTaskFactoriesResult(this.getSourcesResult(this.getTypesResult()))
)
}
void setOutputDirFunction(Function<Build, OutputDir> outputDirFunction) {
this.outputDirFunction = outputDirFunction
}
void setOutputDir(File file) {
this.outputDirFunction = { new OutputDir(file) }
}
void setOutputDir(@Nullable String path) {
this.outputDirFunction = { new OutputDir(path) }
}
}

View File

@ -0,0 +1,22 @@
package com.jessebrault.ssg.buildscript.dsl
final class GlobalsDelegate {
@Delegate
final Map<String, Object> globals = [:]
@Override
Object getProperty(String propertyName) {
this.globals[propertyName]
}
@Override
void setProperty(String propertyName, Object newValue) {
this.globals[propertyName] = newValue
}
Map<String, Object> getResult() {
this.globals
}
}

View File

@ -0,0 +1,20 @@
package com.jessebrault.ssg.buildscript.dsl
import com.jessebrault.ssg.SiteSpec
final class SiteSpecDelegate {
String name
String baseUrl
SiteSpecDelegate() {
def blank = SiteSpec.getBlank()
this.name = blank.name
this.baseUrl = blank.baseUrl
}
SiteSpec getResult() {
new SiteSpec(this.name, this.baseUrl)
}
}

View File

@ -0,0 +1,50 @@
package com.jessebrault.ssg.buildscript.dsl
import com.jessebrault.ssg.buildscript.SourceProviders
import com.jessebrault.ssg.model.Model
import com.jessebrault.ssg.page.Page
import com.jessebrault.ssg.part.Part
import com.jessebrault.ssg.provider.CollectionProvider
import com.jessebrault.ssg.provider.CollectionProviders
import com.jessebrault.ssg.template.Template
import com.jessebrault.ssg.text.Text
final class SourceProvidersDelegate {
private CollectionProvider<Text> textsProvider = CollectionProviders.getEmpty()
private CollectionProvider<Model<Object>> modelsProvider = CollectionProviders.getEmpty()
private CollectionProvider<Page> pagesProvider = CollectionProviders.getEmpty()
private CollectionProvider<Template> templatesProvider = CollectionProviders.getEmpty()
private CollectionProvider<Part> partsProvider = CollectionProviders.getEmpty()
void texts(CollectionProvider<Text> textsProvider) {
this.textsProvider += textsProvider
}
void models(CollectionProvider<Model<?>> modelsProvider) {
this.modelsProvider += modelsProvider
}
void pages(CollectionProvider<Page> pagesProvider) {
this.pagesProvider += pagesProvider
}
void templates(CollectionProvider<Template> templatesProvider) {
this.templatesProvider += templatesProvider
}
void parts(CollectionProvider<Part> partsProvider) {
this.partsProvider += partsProvider
}
SourceProviders getResult() {
new SourceProviders(
this.textsProvider,
this.modelsProvider,
this.pagesProvider,
this.templatesProvider,
this.partsProvider
)
}
}

View File

@ -0,0 +1,46 @@
package com.jessebrault.ssg.buildscript.dsl
import com.jessebrault.ssg.task.TaskFactory
import com.jessebrault.ssg.task.TaskFactorySpec
import groovy.transform.stc.ClosureParams
import groovy.transform.stc.SecondParam
import java.util.function.Supplier
final class TaskFactoriesDelegate {
private final Map<String, TaskFactorySpec> specs = [:]
private void checkNotRegistered(String name) {
if (this.specs.containsKey(name)) {
throw new IllegalArgumentException("a TaskFactory is already registered by the name ${ name }")
}
}
void register(String name, Supplier<? extends TaskFactory> factorySupplier) {
this.checkNotRegistered(name)
this.specs[name] = new TaskFactorySpec(factorySupplier, [])
}
def <T extends TaskFactory> void register(
String name,
Supplier<T> factorySupplier,
@ClosureParams(value = SecondParam.FirstGenericType)
Closure<Void> factoryConfigureClosure
) {
this.checkNotRegistered(name)
this.specs[name] = new TaskFactorySpec(factorySupplier, [factoryConfigureClosure])
}
void configure(String name, Closure<Void> factoryConfigureClosure) {
if (!this.specs.containsKey(name)) {
throw new IllegalArgumentException("there is no TaskFactory registered by name ${ name }")
}
this.specs[name].configureClosures << factoryConfigureClosure
}
Map<String, TaskFactorySpec> getResult() {
this.specs
}
}

View File

@ -0,0 +1,24 @@
package com.jessebrault.ssg.buildscript.dsl
import com.jessebrault.ssg.buildscript.TypesContainer
import com.jessebrault.ssg.page.PageType
import com.jessebrault.ssg.part.PartType
import com.jessebrault.ssg.template.TemplateType
import com.jessebrault.ssg.text.TextType
import groovy.transform.EqualsAndHashCode
import groovy.transform.NullCheck
@NullCheck
@EqualsAndHashCode
final class TypesDelegate {
final Collection<TextType> textTypes = []
final Collection<PageType> pageTypes = []
final Collection<TemplateType> templateTypes = []
final Collection<PartType> partTypes = []
TypesContainer getResult() {
new TypesContainer(this.textTypes, this.pageTypes, this.templateTypes, this.partTypes)
}
}

View File

@ -0,0 +1,53 @@
package com.jessebrault.ssg.dsl
import com.jessebrault.ssg.part.Part
import com.jessebrault.ssg.render.RenderContext
import com.jessebrault.ssg.text.Text
import groovy.transform.EqualsAndHashCode
import org.jetbrains.annotations.Nullable
import static java.util.Objects.requireNonNull
@EqualsAndHashCode(includeFields = true)
final class EmbeddablePart {
private final Part part
private final RenderContext context
private final Closure<Void> onDiagnostics
@Nullable
private final Text text
EmbeddablePart(
Part part,
RenderContext context,
Closure<Void> onDiagnostics,
@Nullable Text text
) {
this.part = requireNonNull(part)
this.context = requireNonNull(context)
this.onDiagnostics = requireNonNull(onDiagnostics)
this.text = text
}
String render(Map binding = [:]) {
def result = part.type.renderer.render(
this.part,
binding,
this.context,
this.text
)
if (result.hasDiagnostics()) {
this.onDiagnostics.call(result.diagnostics)
''
} else {
result.get()
}
}
@Override
String toString() {
"EmbeddablePart(part: ${ this.part })"
}
}

View File

@ -0,0 +1,33 @@
package com.jessebrault.ssg.dsl
import com.jessebrault.ssg.render.RenderContext
import com.jessebrault.ssg.text.Text
import groovy.transform.EqualsAndHashCode
import org.jetbrains.annotations.Nullable
import static java.util.Objects.requireNonNull
@EqualsAndHashCode(includeFields = true)
final class EmbeddablePartsMap {
@Delegate
private final Map<String, EmbeddablePart> partsMap = [:]
EmbeddablePartsMap(
RenderContext context,
Closure<Void> onDiagnostics,
@Nullable Text text = null
) {
requireNonNull(context)
requireNonNull(onDiagnostics)
context.parts.each {
this.put(it.path, new EmbeddablePart(it, context, onDiagnostics, text))
}
}
@Override
String toString() {
"EmbeddablePartsMap(partsMap: ${ this.partsMap })"
}
}

View File

@ -0,0 +1,60 @@
package com.jessebrault.ssg.dsl
import com.jessebrault.ssg.text.FrontMatter
import com.jessebrault.ssg.text.Text
import groovy.transform.EqualsAndHashCode
import groovy.transform.Memoized
import groovy.transform.NullCheck
import groovy.transform.TupleConstructor
@TupleConstructor(includeFields = true, defaults = false)
@NullCheck(includeGenerated = true)
@EqualsAndHashCode(includeFields = true)
final class EmbeddableText {
private final Text text
private final Closure<Void> onDiagnostics
@Memoized
String render() {
def result = this.text.type.renderer.render(this.text)
if (result.diagnostics.size() > 0) {
this.onDiagnostics.call(result.diagnostics)
''
} else {
result.get()
}
}
@Memoized
FrontMatter getFrontMatter() {
def result = this.text.type.frontMatterGetter.get(this.text)
if (result.hasDiagnostics()) {
this.onDiagnostics.call(result.diagnostics)
new FrontMatter(this.text, [:])
} else {
result.get()
}
}
@Memoized
String getExcerpt(int limit) {
def result = this.text.type.excerptGetter.getExcerpt(this.text, limit)
if (result.hasDiagnostics()) {
this.onDiagnostics.call(result.diagnostics)
''
} else {
result.get()
}
}
String getPath() {
this.text.path
}
@Override
String toString() {
"EmbeddableText(text: ${ this.text })"
}
}

View File

@ -0,0 +1,25 @@
package com.jessebrault.ssg.dsl
import com.jessebrault.ssg.text.Text
import groovy.transform.EqualsAndHashCode
import groovy.transform.NullCheck
@NullCheck
@EqualsAndHashCode(includeFields = true)
final class EmbeddableTextsCollection {
@Delegate
private final Collection<EmbeddableText> embeddableTexts = []
EmbeddableTextsCollection(Collection<Text> texts, Closure<Void> onDiagnostics) {
texts.each {
this << new EmbeddableText(it, onDiagnostics)
}
}
@Override
String toString() {
"EmbeddableTextsCollection(embeddableTexts: ${ this.embeddableTexts })"
}
}

View File

@ -0,0 +1,44 @@
package com.jessebrault.ssg.dsl
import com.jessebrault.ssg.model.Model
import groovy.transform.EqualsAndHashCode
import groovy.transform.NullCheck
import org.jetbrains.annotations.Nullable
@NullCheck
@EqualsAndHashCode(includeFields = true)
final class ModelCollection<T> {
@Delegate
private final Collection<Model<T>> ts = []
ModelCollection(Collection<Model<T>> ts) {
this.ts.addAll(ts)
}
@Nullable
Model<T> getByName(String name) {
this.ts.find { it.name == name }
}
@Nullable
<E extends T> Model<E> getByNameAndType(String name, Class<E> type) {
this.ts.find { it.name == name && type.isAssignableFrom(it.get().class) } as Model<E>
}
Optional<Model<T>> findByName(String name) {
Optional.ofNullable(this.getByName(name))
}
def <E extends T> Optional<Model<E>> findByNameAndType(String name, Class<E> type) {
Optional.ofNullable(this.getByNameAndType(name, type))
}
def <E extends T> ModelCollection<E> findAllByType(Class<E> type) {
def es = this.ts.findResults {
type.isAssignableFrom(it.get().class) ? it as Model<E> : null
}
new ModelCollection<>(es)
}
}

View File

@ -0,0 +1,87 @@
package com.jessebrault.ssg.dsl
import com.jessebrault.ssg.dsl.tagbuilder.DynamicTagBuilder
import com.jessebrault.ssg.dsl.urlbuilder.PathBasedUrlBuilder
import com.jessebrault.ssg.render.RenderContext
import com.jessebrault.ssg.text.Text
import groovy.transform.NullCheck
import groovy.transform.stc.ClosureParams
import groovy.transform.stc.SimpleType
import org.slf4j.LoggerFactory
final class StandardDslMap {
@NullCheck(includeGenerated = true)
static final class Builder {
private final Map<String, Object> custom = [:]
String loggerName = ''
Closure<Void> onDiagnostics = { }
Text text = null
void putCustom(String key, Object value) {
this.custom.put(key, value)
}
void putAllCustom(Map<String, Object> m) {
this.custom.putAll(m)
}
}
static Map<String, Object> get(
RenderContext context,
@DelegatesTo(value = Builder, strategy = Closure.DELEGATE_FIRST)
@ClosureParams(
value = SimpleType,
options = ['com.jessebrault.ssg.dsl.StandardDslMap.Builder']
)
Closure<Void> builderClosure
) {
def b = new Builder()
builderClosure.resolveStrategy = Closure.DELEGATE_FIRST
builderClosure.delegate = b
builderClosure(b)
[:].tap {
// standard variables
it.globals = context.globals
it.logger = LoggerFactory.getLogger(b.loggerName)
it.models = new ModelCollection<Object>(context.models)
it.parts = new EmbeddablePartsMap(
context,
b.onDiagnostics,
b.text
)
it.siteSpec = context.siteSpec
it.sourcePath = context.sourcePath
it.tagBuilder = new DynamicTagBuilder()
it.targetPath = context.targetPath
it.tasks = new TaskCollection(context.tasks)
it.text = b.text ? new EmbeddableText(
b.text,
b.onDiagnostics
) : null
it.texts = new EmbeddableTextsCollection(
context.texts,
b.onDiagnostics
)
it.urlBuilder = new PathBasedUrlBuilder(
context.targetPath,
context.siteSpec.baseUrl
)
// task types
it.Task = com.jessebrault.ssg.task.Task
it.HtmlTask = com.jessebrault.ssg.html.HtmlTask
it.ModelToHtmlTask = com.jessebrault.ssg.html.ModelToHtmlTask
it.PageToHtmlTask = com.jessebrault.ssg.html.PageToHtmlTask
it.TextToHtmlTask = com.jessebrault.ssg.html.TextToHtmlTask
// custom
it.putAll(b.custom)
}
}
}

View File

@ -0,0 +1,21 @@
package com.jessebrault.ssg.dsl
import com.jessebrault.ssg.task.Task
import groovy.transform.EqualsAndHashCode
@EqualsAndHashCode(includeFields = true)
final class TaskCollection {
@Delegate
private final Collection<Task> tasks
TaskCollection(Collection<? extends Task> src = []) {
this.tasks = []
this.tasks.addAll(src)
}
def <T extends Task> Collection<T> byType(Class<T> taskClass) {
this.tasks.findAll { taskClass.isAssignableFrom(it.class) } as Collection<T>
}
}

View File

@ -0,0 +1,77 @@
package com.jessebrault.ssg.dsl.tagbuilder
import org.codehaus.groovy.runtime.InvokerHelper
final class DynamicTagBuilder implements TagBuilder {
@Override
String create(String name) {
"<$name />"
}
@Override
String create(String name, Map<String, ?> attributes) {
def formattedAttributes = attributes.collect {
if (it.value instanceof String) {
it.key + '="' + it.value + '"'
} else if (it.value instanceof Integer) {
it.key + '=' + it.value
} else if (it.value instanceof Boolean && it.value == true) {
it.key
} else {
it.key + '="' + it.value.toString() + '"'
}
}.join(' ')
"<$name $formattedAttributes />"
}
@Override
String create(String name, String body) {
"<$name>$body</$name>"
}
@Override
String create(String name, Map<String, ?> attributes, String body) {
def formattedAttributes = attributes.collect {
if (it.value instanceof String) {
it.key + '="' + it.value + '"'
} else if (it.value instanceof Integer) {
it.key + '=' + it.value
} else if (it.value instanceof Boolean && it.value == true) {
it.key
} else {
it.key + '="' + it.value.toString() + '"'
}
}.join(' ')
"<$name $formattedAttributes>$body</$name>"
}
@Override
Object invokeMethod(String name, Object args) {
def argsList = InvokerHelper.asList(args)
return switch (argsList.size()) {
case 0 -> this.create(name)
case 1 -> {
def arg0 = argsList[0]
if (arg0 instanceof Map) {
this.create(name, arg0)
} else if (arg0 instanceof String) {
this.create(name, arg0)
} else {
throw new MissingMethodException(name, this.class, args, false)
}
}
case 2 -> {
def arg0 = argsList[0]
def arg1 = argsList[1]
if (arg0 instanceof Map && arg1 instanceof String) {
this.create(name, arg0, arg1)
} else {
throw new MissingMethodException(name, this.class, args, false)
}
}
default -> throw new MissingMethodException(name, this.class, args, false)
}
}
}

View File

@ -0,0 +1,8 @@
package com.jessebrault.ssg.dsl.tagbuilder
interface TagBuilder {
String create(String name)
String create(String name, Map<String, ?> attributes)
String create(String name, String body)
String create(String name, Map<String, ?> attributes, String body)
}

View File

@ -0,0 +1,37 @@
package com.jessebrault.ssg.dsl.urlbuilder
import java.nio.file.Path
final class PathBasedUrlBuilder implements UrlBuilder {
private final String absolute
private final String baseUrl
private final Path fromDirectory
PathBasedUrlBuilder(String targetPath, String baseUrl) {
this.absolute = baseUrl + '/' + targetPath
this.baseUrl = baseUrl
def fromFilePath = Path.of(targetPath)
if (fromFilePath.parent) {
this.fromDirectory = fromFilePath.parent
} else {
this.fromDirectory = Path.of('')
}
}
@Override
String getAbsolute() {
this.absolute
}
@Override
String absolute(String to) {
this.baseUrl + '/' + to
}
@Override
String relative(String to) {
this.fromDirectory.relativize(Path.of(to)).toString()
}
}

View File

@ -0,0 +1,7 @@
package com.jessebrault.ssg.dsl.urlbuilder
interface UrlBuilder {
String getAbsolute()
String absolute(String to)
String relative(String to)
}

View File

@ -0,0 +1,44 @@
package com.jessebrault.ssg.html
import com.jessebrault.ssg.task.AbstractTask
import com.jessebrault.ssg.task.Task
import com.jessebrault.ssg.util.Diagnostic
import com.jessebrault.ssg.util.Result
import groovy.transform.EqualsAndHashCode
import groovy.transform.NullCheck
@NullCheck
@EqualsAndHashCode
abstract class AbstractHtmlTask extends AbstractTask implements HtmlTask {
final String path
private final File buildDir
AbstractHtmlTask(String name, String path, File buildDir) {
super(name)
this.path = path
this.buildDir = buildDir
}
protected abstract Result<String> transform(Collection<Task> allTasks)
@Override
final Collection<Diagnostic> execute(Collection<Task> allTasks) {
def transformResult = this.transform(allTasks)
if (transformResult.hasDiagnostics()) {
transformResult.diagnostics
} else {
def content = transformResult.get()
def target = new File(this.buildDir, this.path)
target.createParentDirectories()
target.write(content)
[]
}
}
@Override
String toString() {
"AbstractHtmlTask(path: ${ this.path }, super: ${ super.toString() })"
}
}

View File

@ -0,0 +1,7 @@
package com.jessebrault.ssg.html
import com.jessebrault.ssg.task.Task
interface HtmlTask extends Task {
String getPath()
}

View File

@ -0,0 +1,16 @@
package com.jessebrault.ssg.html
import com.jessebrault.ssg.model.Model
import com.jessebrault.ssg.template.Template
import groovy.transform.EqualsAndHashCode
import groovy.transform.NullCheck
import groovy.transform.TupleConstructor
@TupleConstructor(defaults = false)
@NullCheck(includeGenerated = true)
@EqualsAndHashCode
final class ModelToHtmlSpec<T> {
final Model<T> model
final Template template
final String path
}

View File

@ -0,0 +1,65 @@
package com.jessebrault.ssg.html
import com.jessebrault.ssg.SiteSpec
import com.jessebrault.ssg.model.Model
import com.jessebrault.ssg.part.Part
import com.jessebrault.ssg.render.RenderContext
import com.jessebrault.ssg.task.Task
import com.jessebrault.ssg.task.TaskSpec
import com.jessebrault.ssg.template.Template
import com.jessebrault.ssg.text.Text
import com.jessebrault.ssg.util.Result
import groovy.transform.EqualsAndHashCode
import groovy.transform.NullCheck
@NullCheck
@EqualsAndHashCode
final class ModelToHtmlTask<T> extends AbstractHtmlTask {
private final SiteSpec siteSpec
private final Map<String, Object> globals
private final Model<T> model
private final Template template
private final Collection<Text> allTexts
private final Collection<Model<Object>> allModels
private final Collection<Part> allParts
ModelToHtmlTask(
String path,
TaskSpec taskSpec,
Model<T> model,
Template template,
Collection<Text> allTexts,
Collection<Model<Object>> allModels,
Collection<Part> allParts
) {
super("modelToHtml:${ path }", path, taskSpec.outputDir)
this.siteSpec = taskSpec.siteSpec
this.globals = taskSpec.globals
this.model = model
this.template = template
this.allTexts = allTexts
this.allModels = allModels
this.allParts = allParts
}
@Override
protected Result<String> transform(Collection<Task> allTasks) {
this.template.type.renderer.render(this.template, null, new RenderContext(
sourcePath: this.model.name,
targetPath: this.path,
tasks: allTasks,
texts: this.allTexts,
models: this.allModels,
parts: this.allParts,
siteSpec: this.siteSpec,
globals: this.globals
))
}
@Override
String toString() {
"ModelToHtml(${ this.model }, ${ this.template }, ${ this.allTexts }, ${ this.allParts }, ${ super.toString() })"
}
}

View File

@ -0,0 +1,33 @@
package com.jessebrault.ssg.html
import com.jessebrault.ssg.provider.CollectionProvider
import com.jessebrault.ssg.provider.CollectionProviders
import com.jessebrault.ssg.task.AbstractRenderTaskFactory
import com.jessebrault.ssg.task.Task
import com.jessebrault.ssg.task.TaskSpec
import com.jessebrault.ssg.util.Result
final class ModelToHtmlTaskFactory<T> extends AbstractRenderTaskFactory {
CollectionProvider<ModelToHtmlSpec<T>> specsProvider = CollectionProviders.getEmpty()
@Override
Result<Collection<Task>> getTasks(TaskSpec taskSpec) {
def allTexts = this.allTextsProvider.provide()
def allModels = this.allModelsProvider.provide()
def allParts = this.allPartsProvider.provide()
Result.of(specsProvider.provide().collect {
new ModelToHtmlTask<>(
it.path,
taskSpec,
it.model,
it.template,
allTexts,
allModels,
allParts
)
})
}
}

View File

@ -0,0 +1,62 @@
package com.jessebrault.ssg.html
import com.jessebrault.ssg.SiteSpec
import com.jessebrault.ssg.model.Model
import com.jessebrault.ssg.page.Page
import com.jessebrault.ssg.part.Part
import com.jessebrault.ssg.render.RenderContext
import com.jessebrault.ssg.task.Task
import com.jessebrault.ssg.task.TaskSpec
import com.jessebrault.ssg.text.Text
import com.jessebrault.ssg.util.Result
import groovy.transform.EqualsAndHashCode
import groovy.transform.NullCheck
@NullCheck
@EqualsAndHashCode(includeFields = true, callSuper = true)
final class PageToHtmlTask extends AbstractHtmlTask {
private final SiteSpec siteSpec
private final Map<String, Object> globals
private final Page page
private final Collection<Text> allTexts
private final Collection<Model<Object>> allModels
private final Collection<Part> allParts
PageToHtmlTask(
String path,
TaskSpec taskSpec,
Page page,
Collection<Text> allTexts,
Collection<Model<Object>> allModels,
Collection<Part> allParts
) {
super("pageToHtml:${ path }", path, taskSpec.outputDir)
this.siteSpec = taskSpec.siteSpec
this.globals = taskSpec.globals
this.page = page
this.allTexts = allTexts
this.allModels = allModels
this.allParts = allParts
}
@Override
protected Result<String> transform(Collection<Task> allTasks) {
this.page.type.renderer.render(this.page, new RenderContext(
this.page.path,
this.path,
allTasks,
this.allTexts,
this.allModels,
this.allParts,
this.siteSpec,
this.globals
))
}
@Override
String toString() {
"PageToHtml(${ this.page }, ${ this.allTexts }, ${ this.allParts }, ${ super.toString() })"
}
}

View File

@ -0,0 +1,40 @@
package com.jessebrault.ssg.html
import com.jessebrault.ssg.page.Page
import com.jessebrault.ssg.provider.CollectionProvider
import com.jessebrault.ssg.provider.CollectionProviders
import com.jessebrault.ssg.task.AbstractRenderTaskFactory
import com.jessebrault.ssg.task.Task
import com.jessebrault.ssg.task.TaskSpec
import com.jessebrault.ssg.util.Result
import static com.jessebrault.ssg.util.ExtensionUtil.stripExtension
import static java.util.Objects.requireNonNull
final class PageToHtmlTaskFactory extends AbstractRenderTaskFactory {
CollectionProvider<Page> pagesProvider = CollectionProviders.getEmpty()
@Override
Result<Collection<Task>> getTasks(TaskSpec taskSpec) {
super.checkProviders()
requireNonNull(this.pagesProvider)
def allTexts = this.allTextsProvider.provide()
def allModels = this.allModelsProvider.provide()
def allParts = this.allPartsProvider.provide()
final Collection<Task> tasks = this.pagesProvider.provide()
.collect {
new PageToHtmlTask(
stripExtension(it.path) + '.html',
taskSpec,
it,
allTexts,
allModels,
allParts
)
}
Result.of(tasks)
}
}

View File

@ -0,0 +1,16 @@
package com.jessebrault.ssg.html
import com.jessebrault.ssg.template.Template
import com.jessebrault.ssg.text.Text
import groovy.transform.EqualsAndHashCode
import groovy.transform.NullCheck
import groovy.transform.TupleConstructor
@TupleConstructor(defaults = false)
@NullCheck(includeGenerated = true)
@EqualsAndHashCode
final class TextToHtmlSpec {
final Text text
final Template template
final String path
}

View File

@ -0,0 +1,65 @@
package com.jessebrault.ssg.html
import com.jessebrault.ssg.SiteSpec
import com.jessebrault.ssg.model.Model
import com.jessebrault.ssg.part.Part
import com.jessebrault.ssg.render.RenderContext
import com.jessebrault.ssg.task.Task
import com.jessebrault.ssg.task.TaskSpec
import com.jessebrault.ssg.template.Template
import com.jessebrault.ssg.text.Text
import com.jessebrault.ssg.util.Result
import groovy.transform.EqualsAndHashCode
import groovy.transform.NullCheck
@NullCheck
@EqualsAndHashCode(includeFields = true, callSuper = true)
final class TextToHtmlTask extends AbstractHtmlTask {
private final SiteSpec siteSpec
private final Map<String, Object> globals
private final Text text
private final Template template
private final Collection<Text> allTexts
private final Collection<Model<Object>> allModels
private final Collection<Part> allParts
TextToHtmlTask(
String path,
TaskSpec taskSpec,
Text text,
Template template,
Collection<Text> allTexts,
Collection<Model<Object>> allModels,
Collection<Part> allParts
) {
super("textToHtml:${ path }", path, taskSpec.outputDir)
this.siteSpec = taskSpec.siteSpec
this.globals = taskSpec.globals
this.text = text
this.template = template
this.allTexts = allTexts
this.allModels = allModels
this.allParts = allParts
}
@Override
protected Result<String> transform(Collection<Task> allTasks) {
this.template.type.renderer.render(this.template, this.text, new RenderContext(
this.text.path,
this.path,
allTasks,
this.allTexts,
this.allModels,
this.allParts,
this.siteSpec,
this.globals
))
}
@Override
String toString() {
"TextToHtml(${ this.text }, ${ this.template }, ${ this.allTexts }, ${ this.allParts }, ${ super.toString() })"
}
}

View File

@ -0,0 +1,48 @@
package com.jessebrault.ssg.html
import com.jessebrault.ssg.provider.CollectionProvider
import com.jessebrault.ssg.provider.CollectionProviders
import com.jessebrault.ssg.task.AbstractRenderTaskFactory
import com.jessebrault.ssg.task.Task
import com.jessebrault.ssg.task.TaskSpec
import com.jessebrault.ssg.util.Diagnostic
import com.jessebrault.ssg.util.Result
import static java.util.Objects.requireNonNull
final class TextToHtmlTaskFactory extends AbstractRenderTaskFactory {
CollectionProvider<Result<TextToHtmlSpec>> specProvider = CollectionProviders.getEmpty()
@Override
Result<Collection<Task>> getTasks(TaskSpec taskSpec) {
super.checkProviders()
requireNonNull(this.specProvider)
def allTexts = this.allTextsProvider.provide()
def allModels = this.allModelsProvider.provide()
def allParts = this.allPartsProvider.provide()
Collection<Diagnostic> diagnostics = []
final Collection<Task> tasks = this.specProvider.provide()
.findResults {
if (it.hasDiagnostics()) {
diagnostics.addAll(it.diagnostics)
} else {
def spec = it.get()
new TextToHtmlTask(
spec.path,
taskSpec,
spec.text,
spec.template,
allTexts,
allModels,
allParts
)
}
}
Result.of(diagnostics, tasks)
}
}

View File

@ -0,0 +1,22 @@
package com.jessebrault.ssg.model
import groovy.transform.EqualsAndHashCode
import groovy.transform.NullCheck
import groovy.transform.PackageScope
import groovy.transform.TupleConstructor
@PackageScope
@TupleConstructor(includeFields = true, defaults = false)
@NullCheck(includeGenerated = true)
@EqualsAndHashCode(includeFields = true)
final class ClosureBasedModel<T> implements Model<T> {
final String name
private final Closure<T> tClosure
@Override
T get() {
this.tClosure()
}
}

View File

@ -0,0 +1,6 @@
package com.jessebrault.ssg.model
interface Model<T> {
String getName()
T get()
}

View File

@ -0,0 +1,46 @@
package com.jessebrault.ssg.model
import groovy.io.FileType
import groovy.transform.stc.ClosureParams
import groovy.transform.stc.FromString
import java.nio.file.Path
final class Models {
static <T> Model<T> of(String name, T t) {
new SimpleModel<>(name, t)
}
static <T> Model<T> from(String name, Closure<T> tClosure) {
new ClosureBasedModel<>(name, tClosure)
}
/**
* Takes a directory and iterates recursively through all files present in the directory and sub-directories,
* supplying each File along with a String representing that File's path relative to the base Directory to the
* given Closure, which then returns a Model containing T, all of which are collected and then returned together.
*
* @param directory The base directory in which to search for Files to process.
* @param fileToModelClosure A Closure which receives two params: the File being processed,
* and a String representing the path of that File relative to the base directory. Must return
* a Model containing T.
* @return A Collection of Models containing Ts.
*/
static <T> Collection<Model<T>> fromDirectory(
File directory,
@ClosureParams(value = FromString, options = ['java.io.File, java.lang.String'])
Closure<Model<T>> fileToModelClosure
) {
final Collection<Model<T>> models = []
def directoryPath = Path.of(directory.path)
directory.eachFileRecurse(FileType.FILES) {
def relativePath = directoryPath.relativize(Path.of(it.path)).toString()
models << fileToModelClosure(it, relativePath)
}
models
}
private Models() {}
}

View File

@ -0,0 +1,27 @@
package com.jessebrault.ssg.model
import groovy.transform.EqualsAndHashCode
import groovy.transform.NullCheck
import groovy.transform.PackageScope
import groovy.transform.TupleConstructor
@PackageScope
@TupleConstructor(includeFields = true, defaults = false)
@NullCheck(includeGenerated = true)
@EqualsAndHashCode(includeFields = true)
final class SimpleModel<T> implements Model<T> {
final String name
private final T t
@Override
T get() {
this.t
}
@Override
String toString() {
"SimpleModel(${ this.t })"
}
}

View File

@ -0,0 +1,45 @@
package com.jessebrault.ssg.page
import com.jessebrault.ssg.render.RenderContext
import com.jessebrault.ssg.render.StandardGspRenderer
import com.jessebrault.ssg.util.Diagnostic
import com.jessebrault.ssg.util.Result
import groovy.transform.EqualsAndHashCode
import groovy.transform.NullCheck
@NullCheck
@EqualsAndHashCode
final class GspPageRenderer implements PageRenderer {
private final StandardGspRenderer gspRenderer = new StandardGspRenderer(this.class.classLoader)
@Override
Result<String> render(
Page specialPage,
RenderContext context
) {
def diagnostics = []
try {
def result = this.gspRenderer.render(specialPage.text, context) {
it.loggerName = "GspSpecialPage(${ specialPage.path })"
it.onDiagnostics = diagnostics.&addAll
return
}
Result.of(diagnostics, result.toString())
} catch (Exception e) {
Result.of(
[*diagnostics, new Diagnostic(
"An exception occurred while rendering specialPage ${ specialPage.path }:\n${ e }",
e
)],
''
)
}
}
@Override
String toString() {
"GspSpecialPageRenderer()"
}
}

View File

@ -0,0 +1,21 @@
package com.jessebrault.ssg.page
import groovy.transform.EqualsAndHashCode
import groovy.transform.NullCheck
import groovy.transform.TupleConstructor
@TupleConstructor(defaults = false)
@NullCheck(includeGenerated = true)
@EqualsAndHashCode
final class Page {
final String path
final PageType type
final String text
@Override
String toString() {
"Page(path: ${ this.path }, type: ${ this.type })"
}
}

View File

@ -0,0 +1,13 @@
package com.jessebrault.ssg.page
import com.jessebrault.ssg.render.RenderContext
import com.jessebrault.ssg.util.Result
interface PageRenderer {
Result<String> render(
Page specialPage,
RenderContext context
)
}

View File

@ -0,0 +1,20 @@
package com.jessebrault.ssg.page
import groovy.transform.EqualsAndHashCode
import groovy.transform.NullCheck
import groovy.transform.TupleConstructor
@TupleConstructor(defaults = false)
@NullCheck(includeGenerated = true)
@EqualsAndHashCode
final class PageType {
Collection<String> ids
PageRenderer renderer
@Override
String toString() {
"PageType(ids: ${ this.ids }, renderer: ${ this.renderer })"
}
}

View File

@ -0,0 +1,9 @@
package com.jessebrault.ssg.page
final class PageTypes {
static final PageType GSP = new PageType(['.gsp'], new GspPageRenderer())
private PageTypes() {}
}

View File

@ -0,0 +1,29 @@
package com.jessebrault.ssg.page
import com.jessebrault.ssg.provider.CollectionProvider
import com.jessebrault.ssg.provider.CollectionProviders
import com.jessebrault.ssg.util.ExtensionUtil
import com.jessebrault.ssg.util.PathUtil
import org.slf4j.Logger
import org.slf4j.LoggerFactory
final class PagesProviders {
private static final Logger logger = LoggerFactory.getLogger(PagesProviders)
static CollectionProvider<Page> from(File pagesDirectory, Collection<PageType> pageTypes) {
CollectionProviders.from(pagesDirectory) {
def extension = ExtensionUtil.getExtension(it.path)
def pageType = pageTypes.find { it.ids.contains(extension) }
if (!pageType) {
logger.warn('there is no PageType for file {}; skipping', it)
null
} else {
new Page(PathUtil.relative(pagesDirectory.path, it.path), pageType, it.getText())
}
}
}
private PagesProviders() {}
}

View File

@ -0,0 +1,56 @@
package com.jessebrault.ssg.part
import com.jessebrault.ssg.render.StandardGspRenderer
import com.jessebrault.ssg.util.Diagnostic
import com.jessebrault.ssg.render.RenderContext
import com.jessebrault.ssg.util.Result
import com.jessebrault.ssg.text.Text
import groovy.transform.EqualsAndHashCode
import org.jetbrains.annotations.Nullable
import static java.util.Objects.requireNonNull
@EqualsAndHashCode
final class GspPartRenderer implements PartRenderer {
private final StandardGspRenderer gspRenderer = new StandardGspRenderer(this.class.classLoader)
@Override
Result<String> render(
Part part,
Map<String, Object> binding,
RenderContext context,
@Nullable Text text
) {
requireNonNull(part)
requireNonNull(binding)
requireNonNull(context)
def diagnostics = []
try {
def result = this.gspRenderer.render(part.text, context) {
it.putCustom('binding', binding)
it.loggerName = "GspPart(${ part.path })"
it.onDiagnostics = diagnostics.&addAll
if (text) {
it.text = text
}
return
}
Result.of(diagnostics, result.toString())
} catch (Exception e) {
Result.of(
[*diagnostics, new Diagnostic(
"An exception occurred while rendering part ${ part.path }:\n${ e }",
e
)],
''
)
}
}
@Override
String toString() {
"GspPartRenderer()"
}
}

View File

@ -0,0 +1,21 @@
package com.jessebrault.ssg.part
import groovy.transform.EqualsAndHashCode
import groovy.transform.NullCheck
import groovy.transform.TupleConstructor
@TupleConstructor(defaults = false)
@NullCheck(includeGenerated = true)
@EqualsAndHashCode
final class Part {
String path
PartType type
String text
@Override
String toString() {
"Part(path: ${ this.path }, type: ${ this.type })"
}
}

View File

@ -0,0 +1,17 @@
package com.jessebrault.ssg.part
import com.jessebrault.ssg.render.RenderContext
import com.jessebrault.ssg.util.Result
import com.jessebrault.ssg.text.Text
import org.jetbrains.annotations.Nullable
interface PartRenderer {
Result<String> render(
Part part,
Map<String, Object> binding,
RenderContext context,
@Nullable Text text
)
}

View File

@ -0,0 +1,20 @@
package com.jessebrault.ssg.part
import groovy.transform.EqualsAndHashCode
import groovy.transform.NullCheck
import groovy.transform.TupleConstructor
@TupleConstructor(defaults = false)
@NullCheck(includeGenerated = true)
@EqualsAndHashCode
final class PartType {
Collection<String> ids
PartRenderer renderer
@Override
String toString() {
"PartType(ids: ${ this.ids }, renderer: ${ this.renderer })"
}
}

View File

@ -0,0 +1,9 @@
package com.jessebrault.ssg.part
final class PartTypes {
static final PartType GSP = new PartType(['.gsp'], new GspPartRenderer())
private PartTypes() {}
}

View File

@ -0,0 +1,29 @@
package com.jessebrault.ssg.part
import com.jessebrault.ssg.provider.CollectionProvider
import com.jessebrault.ssg.provider.CollectionProviders
import com.jessebrault.ssg.util.ExtensionUtil
import com.jessebrault.ssg.util.PathUtil
import org.slf4j.Logger
import org.slf4j.LoggerFactory
final class PartsProviders {
private static final Logger logger = LoggerFactory.getLogger(PartsProviders)
static CollectionProvider<Part> of(File partsDir, Collection<PartType> partTypes) {
CollectionProviders.from(partsDir) {
def extension = ExtensionUtil.getExtension(it.path)
def partType = partTypes.find { it.ids.contains(extension) }
if (!partType) {
logger.warn('there is no PartType for file {}; skipping', it)
null
} else {
new Part(PathUtil.relative(partsDir.path, it.path), partType, it.getText())
}
}
}
private PartsProviders() {}
}

View File

@ -0,0 +1,24 @@
package com.jessebrault.ssg.provider
abstract class AbstractCollectionProvider<T> implements CollectionProvider<T> {
static <T> CollectionProvider<T> concat(
CollectionProvider<T> cp0,
CollectionProvider<T> cp1
) {
ClosureBasedCollectionProvider.get {
cp0.provide() + cp1.provide()
}
}
@Override
CollectionProvider<T> plus(Provider<T> other) {
concat(this, other as CollectionProvider<T>)
}
@Override
CollectionProvider<T> plus(CollectionProvider<T> other) {
concat(this, other)
}
}

View File

@ -0,0 +1,26 @@
package com.jessebrault.ssg.provider
abstract class AbstractProvider<T> implements Provider<T> {
static <T> CollectionProvider<T> concat(
Provider<T> p0,
Provider<T> p1
) {
ClosureBasedCollectionProvider.get {
[p0.provide(), p1.provide()]
}
}
@Override
CollectionProvider<T> plus(Provider<T> other) {
concat(this, other)
}
@Override
CollectionProvider<T> asType(Class<CollectionProvider> collectionProviderClass) {
ClosureBasedCollectionProvider.get {
[this.provide() as T]
}
}
}

View File

@ -0,0 +1,25 @@
package com.jessebrault.ssg.provider
import groovy.transform.EqualsAndHashCode
import groovy.transform.NullCheck
import groovy.transform.PackageScope
import groovy.transform.TupleConstructor
@PackageScope
@TupleConstructor(defaults = false, includeFields = true)
@NullCheck(includeGenerated = true)
@EqualsAndHashCode(includeFields = true)
final class ClosureBasedCollectionProvider<T> extends AbstractCollectionProvider<T> {
static <T> CollectionProvider<T> get(Closure<Collection<T>> closure) {
new ClosureBasedCollectionProvider<>(closure)
}
private final Closure<Collection<T>> closure
@Override
Collection<T> provide() {
this.closure()
}
}

View File

@ -0,0 +1,25 @@
package com.jessebrault.ssg.provider
import groovy.transform.EqualsAndHashCode
import groovy.transform.NullCheck
import groovy.transform.PackageScope
import groovy.transform.TupleConstructor
@PackageScope
@TupleConstructor(includeFields = true, defaults = false)
@NullCheck(includeGenerated = true)
@EqualsAndHashCode(includeFields = true)
final class ClosureBasedProvider<T> extends AbstractProvider<T> {
static <T> Provider<T> of(Closure<T> closure) {
new ClosureBasedProvider<>(closure)
}
private final Closure<T> closure
@Override
T provide() {
this.closure()
}
}

View File

@ -0,0 +1,7 @@
package com.jessebrault.ssg.provider
interface CollectionProvider<T> {
Collection<T> provide()
CollectionProvider<T> plus(Provider<T> other)
CollectionProvider<T> plus(CollectionProvider<T> other)
}

View File

@ -0,0 +1,31 @@
package com.jessebrault.ssg.provider
import groovy.transform.stc.ClosureParams
import groovy.transform.stc.FromString
import org.jetbrains.annotations.Nullable
final class CollectionProviders {
static <T> CollectionProvider<T> getEmpty() {
new SimpleCollectionProvider<>([])
}
static <T> CollectionProvider<T> of(Collection<T> ts) {
new SimpleCollectionProvider<T>(ts)
}
static <T> CollectionProvider<T> from(Closure<Collection<T>> closure) {
ClosureBasedCollectionProvider.get(closure)
}
static <T> CollectionProvider<T> from(
File dir,
@ClosureParams(value = FromString, options = 'java.io.File')
Closure<@Nullable T> fileToElementClosure
) {
new FileBasedCollectionProvider<>(dir, fileToElementClosure)
}
private CollectionProviders() {}
}

View File

@ -0,0 +1,49 @@
package com.jessebrault.ssg.provider
import groovy.io.FileType
import groovy.transform.EqualsAndHashCode
import groovy.transform.NullCheck
import groovy.transform.PackageScope
import groovy.transform.stc.ClosureParams
import groovy.transform.stc.FromString
import org.jetbrains.annotations.Nullable
import org.slf4j.Logger
import org.slf4j.LoggerFactory
@PackageScope
@NullCheck
@EqualsAndHashCode(includeFields = true)
final class FileBasedCollectionProvider<T> extends AbstractCollectionProvider<T> {
private static final Logger logger = LoggerFactory.getLogger(FileBasedCollectionProvider)
private final File dir
private final Closure<@Nullable T> fileToElementClosure
FileBasedCollectionProvider(
File dir,
@ClosureParams(value = FromString, options = 'java.io.File')
Closure<@Nullable T> fileToElementClosure
) {
this.dir = dir
this.fileToElementClosure = fileToElementClosure
}
@Override
Collection<T> provide() {
if (!this.dir.isDirectory()) {
logger.error('{} does not exist or is not a directory; returning empty collection', this.dir)
[]
} else {
def ts = []
this.dir.eachFileRecurse(FileType.FILES) {
def t = this.fileToElementClosure(it)
if (t) {
ts << t
}
}
ts
}
}
}

View File

@ -0,0 +1,7 @@
package com.jessebrault.ssg.provider
interface Provider<T> {
T provide()
CollectionProvider<T> plus(Provider<T> other)
CollectionProvider<T> asType(Class<CollectionProvider> collectionProviderClass)
}

View File

@ -0,0 +1,27 @@
package com.jessebrault.ssg.provider
import org.codehaus.groovy.runtime.InvokerHelper
final class Providers {
static <T> Provider<T> of(T t) {
new SimpleProvider<>(t)
}
static <T> Provider<T> from(Closure<T> closure) {
ClosureBasedProvider.of(closure)
}
static <T> CollectionProvider<T> concat(Provider<T> ...providers) {
concat(List.of(providers))
}
static <T> CollectionProvider<T> concat(Collection<Provider<T>> providers) {
providers.inject(CollectionProviders.<T>getEmpty()) { acc, val ->
acc + val
}
}
private Providers() {}
}

View File

@ -0,0 +1,21 @@
package com.jessebrault.ssg.provider
import groovy.transform.EqualsAndHashCode
import groovy.transform.NullCheck
import groovy.transform.PackageScope
import groovy.transform.TupleConstructor
@PackageScope
@TupleConstructor(defaults = false, includeFields = true)
@NullCheck(includeGenerated = true)
@EqualsAndHashCode(includeFields = true)
final class SimpleCollectionProvider<T> extends AbstractCollectionProvider<T> {
private final Collection<T> ts
@Override
Collection<T> provide() {
this.ts
}
}

View File

@ -0,0 +1,21 @@
package com.jessebrault.ssg.provider
import groovy.transform.EqualsAndHashCode
import groovy.transform.NullCheck
import groovy.transform.PackageScope
import groovy.transform.TupleConstructor
@PackageScope
@TupleConstructor(defaults = false, includeFields = true)
@NullCheck(includeGenerated = true)
@EqualsAndHashCode(includeFields = true)
final class SimpleProvider<T> extends AbstractProvider<T> {
private final T t
@Override
T provide() {
this.t
}
}

View File

@ -0,0 +1,39 @@
package com.jessebrault.ssg.render
import com.jessebrault.ssg.SiteSpec
import com.jessebrault.ssg.model.Model
import com.jessebrault.ssg.part.Part
import com.jessebrault.ssg.task.Task
import com.jessebrault.ssg.text.Text
import groovy.transform.EqualsAndHashCode
import groovy.transform.NullCheck
import groovy.transform.TupleConstructor
@TupleConstructor(defaults = false, force = true)
@NullCheck(includeGenerated = true)
@EqualsAndHashCode
final class RenderContext {
RenderContext(Map<String, Object> args = [:]) {
this(
args.sourcePath as String ?: '',
args.targetPath as String ?: '',
args.tasks as Collection<Task> ?: [],
args.texts as Collection<Text> ?: [],
args.models as Collection<Model<Object>> ?: [],
args.parts as Collection<Part> ?: [],
args.siteSpec as SiteSpec ?: SiteSpec.getBlank(),
args.globals as Map<String, Object> ?: [:]
)
}
final String sourcePath
final String targetPath
final Collection<Task> tasks
final Collection<Text> texts
final Collection<Model<Object>> models
final Collection<Part> parts
final SiteSpec siteSpec
final Map<String, Object> globals
}

View File

@ -0,0 +1,27 @@
package com.jessebrault.ssg.render
import com.jessebrault.ssg.dsl.StandardDslMap
import groovy.text.GStringTemplateEngine
import groovy.text.TemplateEngine
import org.codehaus.groovy.control.CompilerConfiguration
final class StandardGspRenderer {
private final TemplateEngine engine
StandardGspRenderer(ClassLoader parentClassLoader) {
def cc = new CompilerConfiguration() // TODO: investigate if this makes any difference on the ultimate template
def gcl = new GroovyClassLoader(parentClassLoader, cc)
this.engine = new GStringTemplateEngine(gcl)
}
String render(
String template,
RenderContext context,
@DelegatesTo(value = StandardDslMap.Builder, strategy = Closure.DELEGATE_FIRST)
Closure<Void> dslMapBuilderClosure
) {
this.engine.createTemplate(template).make(StandardDslMap.get(context, dslMapBuilderClosure)).toString()
}
}

View File

@ -0,0 +1,23 @@
package com.jessebrault.ssg.task
import com.jessebrault.ssg.model.Model
import com.jessebrault.ssg.part.Part
import com.jessebrault.ssg.provider.CollectionProvider
import com.jessebrault.ssg.provider.CollectionProviders
import com.jessebrault.ssg.text.Text
import static java.util.Objects.requireNonNull
abstract class AbstractRenderTaskFactory implements TaskFactory {
CollectionProvider<Text> allTextsProvider = CollectionProviders.getEmpty()
CollectionProvider<Model<Object>> allModelsProvider = CollectionProviders.getEmpty()
CollectionProvider<Part> allPartsProvider = CollectionProviders.getEmpty()
protected final void checkProviders() {
requireNonNull(this.allTextsProvider)
requireNonNull(this.allModelsProvider)
requireNonNull(this.allPartsProvider)
}
}

View File

@ -0,0 +1,19 @@
package com.jessebrault.ssg.task
import groovy.transform.EqualsAndHashCode
import groovy.transform.NullCheck
import groovy.transform.TupleConstructor
@TupleConstructor(defaults = false)
@NullCheck(includeGenerated = true)
@EqualsAndHashCode
abstract class AbstractTask implements Task {
final String name
@Override
String toString() {
"AbstractTask(name: ${ this.name })"
}
}

View File

@ -0,0 +1,29 @@
package com.jessebrault.ssg.task
import com.jessebrault.ssg.util.Result
import groovy.transform.EqualsAndHashCode
import groovy.transform.NullCheck
import groovy.transform.PackageScope
import groovy.transform.stc.ClosureParams
import groovy.transform.stc.SimpleType
@PackageScope
@NullCheck
@EqualsAndHashCode(includeFields = true)
final class ClosureBasedTaskFactory implements TaskFactory {
private final Closure<Collection<Task>> closure
ClosureBasedTaskFactory(
@ClosureParams(value = SimpleType, options = 'com.jessebrault.ssg.task.TaskSpec')
Closure<Collection<Task>> closure
) {
this.closure = closure
}
@Override
Result<Collection<Task>> getTasks(TaskSpec taskSpec) {
this.closure(taskSpec)
}
}

View File

@ -0,0 +1,5 @@
package com.jessebrault.ssg.task
trait RenderingTaskFactory {
}

View File

@ -0,0 +1,8 @@
package com.jessebrault.ssg.task
import com.jessebrault.ssg.util.Diagnostic
interface Task {
String getName()
Collection<Diagnostic> execute(Collection<Task> allTasks)
}

View File

@ -0,0 +1,17 @@
package com.jessebrault.ssg.task
import groovy.transform.stc.ClosureParams
import groovy.transform.stc.SimpleType
final class TaskFactories {
static TaskFactory of(
@ClosureParams(value = SimpleType, options = 'com.jessebrault.ssg.task.TaskSpec')
Closure<Collection<Void>> closure
) {
new ClosureBasedTaskFactory(closure)
}
private TaskFactories() {}
}

View File

@ -0,0 +1,7 @@
package com.jessebrault.ssg.task
import com.jessebrault.ssg.util.Result
interface TaskFactory {
Result<Collection<Task>> getTasks(TaskSpec taskSpec)
}

View File

@ -0,0 +1,28 @@
package com.jessebrault.ssg.task
import groovy.transform.EqualsAndHashCode
import groovy.transform.NullCheck
import groovy.transform.TupleConstructor
import java.util.function.Supplier
@TupleConstructor(defaults = false)
@NullCheck(includeGenerated = true)
@EqualsAndHashCode
final class TaskFactorySpec {
static TaskFactorySpec concat(TaskFactorySpec spec0, TaskFactorySpec spec1) {
if (spec0.supplier != spec1.supplier) {
throw new IllegalArgumentException("suppliers must be equal!")
}
new TaskFactorySpec(spec0.supplier, spec0.configureClosures + spec1.configureClosures)
}
final Supplier<TaskFactory> supplier
final Collection<Closure<Void>> configureClosures
TaskFactorySpec plus(TaskFactorySpec other) {
concat(this, other)
}
}

View File

@ -0,0 +1,22 @@
package com.jessebrault.ssg.task
import com.jessebrault.ssg.SiteSpec
import groovy.transform.EqualsAndHashCode
import groovy.transform.NullCheck
import groovy.transform.TupleConstructor
@TupleConstructor(defaults = false)
@NullCheck(includeGenerated = true)
@EqualsAndHashCode
final class TaskSpec {
static TaskSpec getEmpty() {
new TaskSpec('', new File(''), SiteSpec.getBlank(), [:])
}
final String buildName
final File outputDir
final SiteSpec siteSpec
final Map<String, Object> globals
}

View File

@ -0,0 +1,35 @@
package com.jessebrault.ssg.task.collector
import com.jessebrault.ssg.provider.Provider
import com.jessebrault.ssg.task.TaskFactory
import groovy.io.FileType
import groovy.transform.NullCheck
import groovy.transform.TupleConstructor
@TupleConstructor(defaults = false, includeFields = true)
@NullCheck(includeGenerated = true)
final class GroovyFileTaskFactoryCollector implements TaskFactoryCollector {
private final GroovyClassLoader groovyClassLoader
private final Collection<Provider<File>> factoryDirectoryProviders
@Override
Collection<TaskFactory> getAllFactories() {
Collection<TaskFactory> factories = []
def pluginDirectories = this.factoryDirectoryProviders.collect { it.provide() }
pluginDirectories.each {
it.eachFileRecurse(FileType.FILES) {
def cl = this.groovyClassLoader.parseClass(it)
if (TaskFactory.isAssignableFrom(cl)) {
def constructor = cl.getDeclaredConstructor()
def factory = constructor.newInstance() as TaskFactory
factories << factory
}
}
}
factories
}
}

View File

@ -0,0 +1,18 @@
package com.jessebrault.ssg.task.collector
import com.jessebrault.ssg.task.TaskFactory
import groovy.transform.NullCheck
import groovy.transform.TupleConstructor
@TupleConstructor(includeFields = true, defaults = false)
@NullCheck(includeGenerated = true)
final class ServiceTaskFactoryCollector implements TaskFactoryCollector {
private final ClassLoader classLoader
@Override
Collection<TaskFactory> getAllFactories() {
ServiceLoader.load(TaskFactory, this.classLoader).asList()
}
}

View File

@ -0,0 +1,7 @@
package com.jessebrault.ssg.task.collector
import com.jessebrault.ssg.task.TaskFactory
interface TaskFactoryCollector {
Collection<TaskFactory> getAllFactories()
}

View File

@ -0,0 +1,48 @@
package com.jessebrault.ssg.template
import com.jessebrault.ssg.render.StandardGspRenderer
import com.jessebrault.ssg.util.Diagnostic
import com.jessebrault.ssg.render.RenderContext
import com.jessebrault.ssg.util.Result
import com.jessebrault.ssg.text.Text
import groovy.transform.EqualsAndHashCode
import groovy.transform.NullCheck
@NullCheck
@EqualsAndHashCode
final class GspTemplateRenderer implements TemplateRenderer {
private final StandardGspRenderer gspRenderer = new StandardGspRenderer(this.class.classLoader)
@Override
Result<String> render(
Template template,
Text text,
RenderContext context
) {
def diagnostics = []
try {
def result = this.gspRenderer.render(template.text, context) {
it.loggerName = "GspTemplate(${ template.path })"
it.onDiagnostics = diagnostics.&addAll
it.text = text
return
}
Result.of(diagnostics, result)
} catch (Exception e) {
Result.of(
[*diagnostics, new Diagnostic(
"An exception occurred while rendering Template ${ template.path }:\n${ e }",
e
)],
''
)
}
}
@Override
String toString() {
"GspTemplateRenderer()"
}
}

View File

@ -0,0 +1,21 @@
package com.jessebrault.ssg.template
import groovy.transform.EqualsAndHashCode
import groovy.transform.NullCheck
import groovy.transform.TupleConstructor
@TupleConstructor(defaults = false)
@NullCheck(includeGenerated = true)
@EqualsAndHashCode
final class Template {
String path
TemplateType type
String text
@Override
String toString() {
"Template(path: ${ this.path }, type: ${ this.type })"
}
}

View File

@ -0,0 +1,15 @@
package com.jessebrault.ssg.template
import com.jessebrault.ssg.render.RenderContext
import com.jessebrault.ssg.util.Result
import com.jessebrault.ssg.text.Text
interface TemplateRenderer {
Result<String> render(
Template template,
Text text,
RenderContext context
)
}

View File

@ -0,0 +1,20 @@
package com.jessebrault.ssg.template
import groovy.transform.EqualsAndHashCode
import groovy.transform.NullCheck
import groovy.transform.TupleConstructor
@TupleConstructor(defaults = false)
@NullCheck(includeGenerated = true)
@EqualsAndHashCode
final class TemplateType {
Collection<String> ids
TemplateRenderer renderer
@Override
String toString() {
"TemplateType(ids: ${ this.ids }, renderer: ${ this.renderer })"
}
}

View File

@ -0,0 +1,9 @@
package com.jessebrault.ssg.template
final class TemplateTypes {
static final TemplateType GSP = new TemplateType(['.gsp'], new GspTemplateRenderer())
private TemplateTypes() {}
}

View File

@ -0,0 +1,29 @@
package com.jessebrault.ssg.template
import com.jessebrault.ssg.provider.CollectionProvider
import com.jessebrault.ssg.provider.CollectionProviders
import com.jessebrault.ssg.util.ExtensionUtil
import com.jessebrault.ssg.util.PathUtil
import org.slf4j.Logger
import org.slf4j.LoggerFactory
final class TemplatesProviders {
private static final Logger logger = LoggerFactory.getLogger(TemplatesProviders)
static CollectionProvider<Template> from(File templatesDir, Collection<TemplateType> templateTypes) {
CollectionProviders.from(templatesDir) {
def extension = ExtensionUtil.getExtension(it.path)
def templateType = templateTypes.find { it.ids.contains(extension) }
if (!templateType) {
logger.warn('there is no TemplateType for file {}; skipping', it)
null
} else {
new Template(PathUtil.relative(templatesDir.path, it.path), templateType, it.getText())
}
}
}
private TemplatesProviders() {}
}

View File

@ -0,0 +1,7 @@
package com.jessebrault.ssg.text
import com.jessebrault.ssg.util.Result
interface ExcerptGetter {
Result<String> getExcerpt(Text text, int limit)
}

View File

@ -0,0 +1,54 @@
package com.jessebrault.ssg.text
import groovy.transform.EqualsAndHashCode
import groovy.transform.NullCheck
import groovy.transform.TupleConstructor
import org.slf4j.Logger
import org.slf4j.LoggerFactory
@TupleConstructor(includeFields = true, defaults = false)
@NullCheck(includeGenerated = true)
@EqualsAndHashCode(includeFields = true)
final class FrontMatter {
private static final Logger logger = LoggerFactory.getLogger(FrontMatter)
private final Text text
private final Map<String, List<String>> data
String get(String key) {
if (this.data[key] != null) {
this.data[key][0]
} else {
logger.warn('in {} no entry for key {} in frontMatter, returning empty string', this.text, key)
''
}
}
String getAt(String key) {
this.get(key)
}
List<String> getList(String key) {
if (data[key] != null) {
data[key]
} else {
logger.warn('in {} no entry for key {} in frontMatter, returning empty list: {}', this.text, key)
[]
}
}
Optional<String> find(String key) {
Optional.ofNullable(this.data[key]?[0])
}
Optional<List<String>> findList(String key) {
Optional.ofNullable(this.data[key])
}
@Override
String toString() {
"FrontMatter(text: ${ this.text }, data: ${ this.data })"
}
}

View File

@ -0,0 +1,7 @@
package com.jessebrault.ssg.text
import com.jessebrault.ssg.util.Result
interface FrontMatterGetter {
Result<FrontMatter> get(Text text)
}

View File

@ -0,0 +1,55 @@
package com.jessebrault.ssg.text
import com.jessebrault.ssg.util.Diagnostic
import com.jessebrault.ssg.util.Result
import org.commonmark.ext.front.matter.YamlFrontMatterExtension
import org.commonmark.node.AbstractVisitor
import org.commonmark.parser.Parser
final class MarkdownExcerptGetter implements ExcerptGetter {
private static class ExcerptVisitor extends AbstractVisitor {
final int limit
List<String> words = []
ExcerptVisitor(int limit) {
this.limit = limit
}
@Override
void visit(org.commonmark.node.Text text) {
if (this.words.size() <= limit) {
def textWords = text.literal.split('\\s+').toList()
def taken = textWords.take(this.limit - this.words.size())
this.words.addAll(taken)
}
}
String getResult() {
this.words.take(this.limit).join(' ')
}
}
private static final Parser parser = Parser.builder()
.extensions([YamlFrontMatterExtension.create()])
.build()
@Override
Result<String> getExcerpt(Text text, int limit) {
try {
def node = parser.parse(text.text)
def visitor = new ExcerptVisitor(limit)
node.accept(visitor)
Result.of(visitor.result)
} catch (Exception e) {
def diagnostic = new Diagnostic(
"There was an exception while getting the excerpt for ${ text }:\n${ e }",
e
)
Result.of([diagnostic], '')
}
}
}

View File

@ -0,0 +1,42 @@
package com.jessebrault.ssg.text
import com.jessebrault.ssg.util.Diagnostic
import com.jessebrault.ssg.util.Result
import groovy.transform.EqualsAndHashCode
import groovy.transform.NullCheck
import org.commonmark.ext.front.matter.YamlFrontMatterExtension
import org.commonmark.ext.front.matter.YamlFrontMatterVisitor
import org.commonmark.parser.Parser
@NullCheck
@EqualsAndHashCode
final class MarkdownFrontMatterGetter implements FrontMatterGetter {
private static final Parser parser = Parser.builder()
.extensions([YamlFrontMatterExtension.create()])
.build()
@Override
Result<FrontMatter> get(Text text) {
try {
def node = parser.parse(text.text)
def v = new YamlFrontMatterVisitor()
node.accept(v)
Result.of(new FrontMatter(text, v.data))
} catch (Exception e) {
Result.of(
[new Diagnostic(
"An exception occured while parsing frontMatter for ${ text.path }:\n${ e }",
e
)],
new FrontMatter(text, [:])
)
}
}
@Override
String toString() {
"MarkdownFrontMatterGetter()"
}
}

View File

@ -0,0 +1,37 @@
package com.jessebrault.ssg.text
import com.jessebrault.ssg.util.Diagnostic
import com.jessebrault.ssg.util.Result
import groovy.transform.EqualsAndHashCode
import groovy.transform.NullCheck
import org.commonmark.ext.front.matter.YamlFrontMatterExtension
import org.commonmark.parser.Parser
import org.commonmark.renderer.html.HtmlRenderer
@NullCheck
@EqualsAndHashCode
final class MarkdownTextRenderer implements TextRenderer {
private static final Parser parser = Parser.builder()
.extensions([YamlFrontMatterExtension.create()])
.build()
private static final HtmlRenderer htmlRenderer = HtmlRenderer.builder().build()
@Override
Result<String> render(Text text) {
try {
Result.of(htmlRenderer.render(parser.parse(text.text)))
} catch (Exception e) {
Result.of(
[new Diagnostic("There was an exception while rendering ${ text.path }:\n${ e }", e)],
''
)
}
}
@Override
String toString() {
"MarkdownTextRenderer()"
}
}

View File

@ -0,0 +1,21 @@
package com.jessebrault.ssg.text
import groovy.transform.EqualsAndHashCode
import groovy.transform.NullCheck
import groovy.transform.TupleConstructor
@TupleConstructor(defaults = false)
@NullCheck(includeGenerated = true)
@EqualsAndHashCode
final class Text {
final String path
final TextType type
final String text
@Override
String toString() {
"Text(path: ${ this.path }, type: ${ this.type })"
}
}

View File

@ -0,0 +1,7 @@
package com.jessebrault.ssg.text
import com.jessebrault.ssg.util.Result
interface TextRenderer {
Result<String> render(Text text)
}

View File

@ -0,0 +1,22 @@
package com.jessebrault.ssg.text
import groovy.transform.EqualsAndHashCode
import groovy.transform.NullCheck
import groovy.transform.TupleConstructor
@TupleConstructor(defaults = false)
@NullCheck(includeGenerated = true)
@EqualsAndHashCode
final class TextType {
final Collection<String> ids
final TextRenderer renderer
final FrontMatterGetter frontMatterGetter
final ExcerptGetter excerptGetter
@Override
String toString() {
"TextType(ids: ${ this.ids })"
}
}

View File

@ -0,0 +1,14 @@
package com.jessebrault.ssg.text
final class TextTypes {
static final MARKDOWN = new TextType(
['.md'],
new MarkdownTextRenderer(),
new MarkdownFrontMatterGetter(),
new MarkdownExcerptGetter()
)
private TextTypes() {}
}

Some files were not shown because too many files have changed in this diff Show More