diff --git a/TODO.md b/TODO.md index 45d4d52..ecfb45e 100644 --- a/TODO.md +++ b/TODO.md @@ -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 diff --git a/api/build.gradle b/api/build.gradle new file mode 100644 index 0000000..09fdb15 --- /dev/null +++ b/api/build.gradle @@ -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' +} \ No newline at end of file diff --git a/api/src/main/groovy/com/jessebrault/ssg/BuildTasksConverter.groovy b/api/src/main/groovy/com/jessebrault/ssg/BuildTasksConverter.groovy new file mode 100644 index 0000000..e555288 --- /dev/null +++ b/api/src/main/groovy/com/jessebrault/ssg/BuildTasksConverter.groovy @@ -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> convert(Build buildScriptResult) +} diff --git a/api/src/main/groovy/com/jessebrault/ssg/SimpleBuildTasksConverter.groovy b/api/src/main/groovy/com/jessebrault/ssg/SimpleBuildTasksConverter.groovy new file mode 100644 index 0000000..39c8ca5 --- /dev/null +++ b/api/src/main/groovy/com/jessebrault/ssg/SimpleBuildTasksConverter.groovy @@ -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> convert(Build buildScriptResult) { + def taskSpec = new TaskSpec( + buildScriptResult.name, + buildScriptResult.outputDirFunction.apply(buildScriptResult).file, + buildScriptResult.siteSpec, + buildScriptResult.globals + ) + Collection tasks = [] + Collection 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) + } + +} diff --git a/api/src/main/groovy/com/jessebrault/ssg/SiteSpec.groovy b/api/src/main/groovy/com/jessebrault/ssg/SiteSpec.groovy new file mode 100644 index 0000000..10c248d --- /dev/null +++ b/api/src/main/groovy/com/jessebrault/ssg/SiteSpec.groovy @@ -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 })" + } + +} diff --git a/api/src/main/groovy/com/jessebrault/ssg/buildscript/Build.groovy b/api/src/main/groovy/com/jessebrault/ssg/buildscript/Build.groovy new file mode 100644 index 0000000..99ed3bb --- /dev/null +++ b/api/src/main/groovy/com/jessebrault/ssg/buildscript/Build.groovy @@ -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 globals + final Collection taskFactorySpecs + + AllBuilds plus(AllBuilds other) { + concat(this, other) + } + + } + + static Build getEmpty() { + new Build( + '', + OutputDirFunctions.DEFAULT, + SiteSpec.getBlank(), + [:], + [] + ) + } + + static Build get(Map args) { + new Build( + args?.name as String ?: '', + args?.outputDirFunction as Function ?: OutputDirFunctions.DEFAULT, + args?.siteSpec as SiteSpec ?: SiteSpec.getBlank(), + args?.globals as Map ?: [:], + args?.taskFactorySpecs as Collection ?: [] + ) + } + + 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 outputDirFunction + final SiteSpec siteSpec + final Map globals + final Collection taskFactorySpecs + + Build plus(Build other) { + concat(this, other) + } + +} diff --git a/api/src/main/groovy/com/jessebrault/ssg/buildscript/BuildScriptBase.groovy b/api/src/main/groovy/com/jessebrault/ssg/buildscript/BuildScriptBase.groovy new file mode 100644 index 0000000..e716760 --- /dev/null +++ b/api/src/main/groovy/com/jessebrault/ssg/buildscript/BuildScriptBase.groovy @@ -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 allBuildsDelegates = [] + private final Collection buildDelegates = [] + + private int currentBuildNumber = 0 + + final AllBuilds defaultAllBuilds = AllBuilds.getEmpty() + + void build( + @DelegatesTo(value = BuildDelegate, strategy = Closure.DELEGATE_FIRST) + Closure buildClosure + ) { + this.build('build' + this.currentBuildNumber, buildClosure) + } + + void build( + String name, + @DelegatesTo(value = BuildDelegate, strategy = Closure.DELEGATE_FIRST) + Closure 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 allBuildsClosure + ) { + def d = new AllBuildsDelegate() + allBuildsClosure.setDelegate(d) + allBuildsClosure.setResolveStrategy(Closure.DELEGATE_FIRST) + allBuildsClosure() + this.allBuildsDelegates << d + } + + Collection getBuilds() { + def allBuilds = this.defaultAllBuilds + this.allBuildsDelegates.each { + allBuilds += it.getResult() + } + + def baseBuild = Build.from(allBuilds) + this.buildDelegates.collect { + baseBuild + it.getResult() + } + } + +} diff --git a/api/src/main/groovy/com/jessebrault/ssg/buildscript/BuildScriptConfiguratorFactory.groovy b/api/src/main/groovy/com/jessebrault/ssg/buildscript/BuildScriptConfiguratorFactory.groovy new file mode 100644 index 0000000..72029d4 --- /dev/null +++ b/api/src/main/groovy/com/jessebrault/ssg/buildscript/BuildScriptConfiguratorFactory.groovy @@ -0,0 +1,7 @@ +package com.jessebrault.ssg.buildscript + +import java.util.function.Consumer + +interface BuildScriptConfiguratorFactory { + Consumer get() +} \ No newline at end of file diff --git a/api/src/main/groovy/com/jessebrault/ssg/buildscript/BuildScriptUtil.groovy b/api/src/main/groovy/com/jessebrault/ssg/buildscript/BuildScriptUtil.groovy new file mode 100644 index 0000000..5ac6058 --- /dev/null +++ b/api/src/main/groovy/com/jessebrault/ssg/buildscript/BuildScriptUtil.groovy @@ -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 runBuildScript(String relativePath, Consumer 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() {} + +} diff --git a/api/src/main/groovy/com/jessebrault/ssg/buildscript/DefaultBuildScriptConfiguratorFactory.groovy b/api/src/main/groovy/com/jessebrault/ssg/buildscript/DefaultBuildScriptConfiguratorFactory.groovy new file mode 100644 index 0000000..1ad5567 --- /dev/null +++ b/api/src/main/groovy/com/jessebrault/ssg/buildscript/DefaultBuildScriptConfiguratorFactory.groovy @@ -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 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 + } + } + } + } + } + +} diff --git a/api/src/main/groovy/com/jessebrault/ssg/buildscript/OutputDir.groovy b/api/src/main/groovy/com/jessebrault/ssg/buildscript/OutputDir.groovy new file mode 100644 index 0000000..2f6c99a --- /dev/null +++ b/api/src/main/groovy/com/jessebrault/ssg/buildscript/OutputDir.groovy @@ -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) + } + +} diff --git a/api/src/main/groovy/com/jessebrault/ssg/buildscript/OutputDirFunctions.groovy b/api/src/main/groovy/com/jessebrault/ssg/buildscript/OutputDirFunctions.groovy new file mode 100644 index 0000000..ace7ef5 --- /dev/null +++ b/api/src/main/groovy/com/jessebrault/ssg/buildscript/OutputDirFunctions.groovy @@ -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 DEFAULT = of { new OutputDir(it.name) } + + static Function concat( + Function f0, + Function f1 + ) { + f1 == OutputDirFunctions.DEFAULT ? f0 : f1 + } + + static Function of( + @ClosureParams(value = SimpleType, options = 'com.jessebrault.ssg.buildscript.Build') + Closure closure + ) { + closure as Function + } + + static Function of(File dir) { + of { new OutputDir(dir) } + } + + static Function of(String path) { + of { new OutputDir(path) } + } + + private OutputDirFunctions() {} + +} diff --git a/api/src/main/groovy/com/jessebrault/ssg/buildscript/SourceProviders.groovy b/api/src/main/groovy/com/jessebrault/ssg/buildscript/SourceProviders.groovy new file mode 100644 index 0000000..45ce4a2 --- /dev/null +++ b/api/src/main/groovy/com/jessebrault/ssg/buildscript/SourceProviders.groovy @@ -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 args) { + new SourceProviders( + args?.textsProvider as CollectionProvider + ?: CollectionProviders.getEmpty() as CollectionProvider, + args?.modelsProvider as CollectionProvider> + ?: CollectionProviders.getEmpty() as CollectionProvider>, + args?.pagesProvider as CollectionProvider + ?: CollectionProviders.getEmpty() as CollectionProvider, + args?.templatesProvider as CollectionProvider