Compare commits

..

No commits in common. "master" and "v0.2.0-SNAPSHOT" have entirely different histories.

258 changed files with 6805 additions and 3074 deletions

View File

@ -1,28 +0,0 @@
name: Ssg Check, Publish, and Release
on:
push:
tags:
- v*
jobs:
ci:
runs-on: ubuntu-latest
steps:
- name: Checkout the code
uses: actions/checkout@v4
- name: Setup Java.
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: 21
- name: Check libraries
run: ./gradlew check
- name: Publish to git.jessebrault.com
run: ./gradlew publishAllPublicationsToGiteaRepository
- name: Create install distribution
run: ./gradlew installDist
- name: Release cli to git.jessebrault.com
uses: akkuman/gitea-release-action@v1
with:
files: |-
cli/build/distributions/*.tar
cli/build/distributions/*.zip

29
.github/workflows/release.yml vendored Normal file
View File

@ -0,0 +1,29 @@
name: StaticSiteGenerator Release
on:
push:
tags:
- 'v*'
jobs:
release:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: 17
distribution: adopt
cache: gradle
- name: Gradle Test
run: ./gradlew test
- name: Gradle Install
run: ./gradlew :cli:assembleDist
- name: Release
uses: ncipollo/release-action@v1
with:
artifacts: 'cli/build/distributions/*.tar,cli/build/distributions/*.zip'
name: ${{ env.GITHUB_REF_NAME }}
tag: ${{ env.GITHUB_REF_NAME }}
token: ${{ secrets.GITHUB_TOKEN }}

View File

@ -1,16 +0,0 @@
# Static Site Generator (SSG)
## Updating Gradle
Update the Gradle wrapper via `gradle/wrapper/gradle-wrapper.properties`. Make sure that the tooling-api dependency is
updated to the same version in `cli/build.gradle`.
## Version-bumping
Update the version of the project in `buildSrc/src/main/groovy/ssg-common.gradle`. Then update the references to the
`cli` and `api` projects in `ssg-gradle-plugin/src/main/java/com/jessebrault/ssg/gradle/SsgGradlePlugin.java`.
## Publishing
Gradle command `publishAllPublicationsToJbArchiva<Internal|Snapshots>Repository`. Which one of `internal` or `snapshots`
appears depends on the current version.

60
TODO.md
View File

@ -1,40 +1,40 @@
# TODO
Here will be kept all of the various todos for this project, organized by release.
N.b. that v0.3.0 was skipped because of such a fundamental change in the usage of the
program with the incorporation of Groowt and Web View Components.
## 0.6.0
- [ ] Plugin system for build scripts
## Future
## 0.5.0
- [ ] watch/dev mode and server
- [ ] Reorganize gradle project layout so there is less hunting around for files
### Add
- [ ] Plan out plugin system such that we can create custom providers of texts, data, etc.
- [ ] Add `Watchable` interface/trait back; an abstraction over FS watching and other sources (such as a database, etc.).
- [ ] Explore `apply(Plugin)` in buildScripts.
## 0.4.* Ongoing
- [ ] Automate test project
- [ ] Move as much gradle integration from `cli` project to `api` project
- [ ] Think about abstracting the build tool logic, because all we need
really is the URLs/Paths for the classes/jars of components and resources
- [ ] Document new api and usage.
- [ ] Re-incorporate dist plugin in gradle build of cli/api
- [ ] Think about how these might be used without a gradle project backing
### Fix
## 0.4.3
- [ ] `Text` component for simply rendering Text objects. Can be used as such:
```
<Text path='/SomeText.md' />
<Text name='SomeText.md' />
<Text text={text} />
```
- [ ] `TextContainer` for accessing all found texts
- [ ] `ModelFactory` for creating models, and `TextModelFactory` for creating models from texts.
- [ ] `Model` component for rendering a model with either a supplied renderer, or a registered `ModelRenderer`
- [ ] `Global` component for rendering globals.
- [ ] Automatically inject self PageSpec and path to Pages.
## v0.2.0
## 0.4.1
- [x] Update groowt to 0.1.2.
### Add
- [ ] Write manual.
- [x] Remove `lib` module.
- [x] Add a way for CLI to choose a build to do, or multiple builds, defaulting to 'default' if it exists.
- [ ] Still must work on 'default'-ing.
- [x] Write lots of tests for buildscript dsl, etc.
- [x] Explore `base` in buildScript dsl.
- Get rid of `allBuilds` concept, and replace it with composable/concat-able builds. In the dsl we could have a notion of `abstractBuild` which can be 'extended' (i.e., on the left side of a concat operation) but not actually run (since it doesn't have a name).
- `OutputDir` should be concat-able, such that the left is the *base* for the right.
- `OutputDirFunctions.concat` should be concat-able as well, such that both are `BiFunction<OutputDir, Build, OutputDir>`, and the output of the left is the input of the right.
- Make the delegates as dumb as possible; no more `getResult` methods; make different classes/object handle concat'ing and getting results.
- [x] Provide a way to override `ssgBuilds` variables from the cli.
### Fix
- [ ] Update CHANGELOG to reflect the gsp-dsl changes.
- [ ] `taskTypes` gone, use class name instead
- [ ] introduction of `models`
- [x] Change most instances of `Closure<Void>` to `Closure<?>` to help with IDE expectations.
- [ ] Fix auto-imports in build script so we don't need to import things.
- [ ] Re-introduce input/output concept to tasks, if possible
## Finished
### v0.2.0
- [x] Investigate imports, including static, in scripts
@ -53,4 +53,4 @@ program with the incorporation of Groowt and Web View Components.
assert post.path == 'blog/post.md'
assert post.targetPath = 'blog/post.html'
// as well as some other information, perhaps such as the Type, extension, *etc.*
```
```

View File

@ -1,58 +1,28 @@
plugins {
id 'ssg-common'
id 'groovy'
id 'java-library'
id 'java-test-fixtures'
id 'maven-publish'
id 'ssg.common'
}
repositories {
mavenCentral()
}
configurations {
testFixturesApi {
extendsFrom configurations.testing
}
}
dependencies {
api libs.groovy
api libs.groovy.yaml
// https://mvnrepository.com/artifact/org.apache.groovy/groovy-templates
implementation 'org.apache.groovy:groovy-templates:4.0.12'
api libs.groowt.v
api libs.groowt.vc
api libs.groowt.wvc
api libs.di
api libs.fp
// https://mvnrepository.com/artifact/org.commonmark/commonmark
implementation 'org.commonmark:commonmark:0.21.0'
compileOnlyApi libs.jetbrains.anontations
// https://mvnrepository.com/artifact/org.commonmark/commonmark-ext-yaml-front-matter
implementation 'org.commonmark:commonmark-ext-yaml-front-matter:0.21.0'
implementation libs.classgraph
implementation libs.commonmark
implementation libs.commonmark.frontmatter
implementation libs.jsoup
// https://mvnrepository.com/artifact/org.jsoup/jsoup
implementation 'org.jsoup:jsoup:1.16.1'
runtimeOnly libs.groowt.wvcc
}
java {
withSourcesJar()
// https://mvnrepository.com/artifact/org.jgrapht/jgrapht-core
implementation 'org.jgrapht:jgrapht-core:1.5.2'
}
jar {
archivesBaseName = 'ssg-api'
}
sourcesJar {
archiveBaseName = 'ssg-api'
}
publishing {
publications {
create('ssgApi', MavenPublication) {
artifactId = 'api'
from components.java
}
}
}
}

View File

@ -0,0 +1,106 @@
package com.jessebrault.ssg
import com.jessebrault.ssg.buildscript.Build
import com.jessebrault.ssg.buildscript.BuildScriptConfiguratorFactory
import com.jessebrault.ssg.buildscript.BuildScripts
import com.jessebrault.ssg.util.Diagnostic
import org.jetbrains.annotations.Nullable
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.slf4j.Marker
import org.slf4j.MarkerFactory
import java.util.function.Consumer
final class BuildScriptBasedStaticSiteGenerator implements StaticSiteGenerator {
private static final Logger logger = LoggerFactory.getLogger(BuildScriptBasedStaticSiteGenerator)
private static final Marker enter = MarkerFactory.getMarker('enter')
private static final Marker exit = MarkerFactory.getMarker('exit')
private final Collection<BuildScriptConfiguratorFactory> configuratorFactories
@Nullable
private final File buildScript
private final Collection<File> buildSrcDirs
private final Map<String, Object> scriptArgs
private final Collection<Build> builds = []
private boolean ranBuildScript = false
BuildScriptBasedStaticSiteGenerator(
Collection<BuildScriptConfiguratorFactory> configuratorFactories = [],
@Nullable File buildScript = null,
Collection<File> buildSrcDirs = [],
Map<String, Object> scriptArgs = [:]
) {
this.configuratorFactories = configuratorFactories
this.buildScript = buildScript
this.buildSrcDirs = buildSrcDirs
this.scriptArgs = scriptArgs
}
private void runBuildScript() {
logger.trace(enter, '')
if (this.buildScript == null) {
logger.info('no specified build script; using defaults')
def result = BuildScripts.runBuildScript { base ->
this.configuratorFactories.each {
it.get().accept(base)
}
}
this.builds.addAll(result)
} else if (this.buildScript.exists() && this.buildScript.isFile()) {
logger.info('running buildScript: {}', this.buildScript)
def result = BuildScripts.runBuildScript(
this.buildScript.name,
this.buildScript.parentFile.toURI().toURL(),
this.buildSrcDirs.collect { it.toURI().toURL() },
[args: this.scriptArgs]
) { base ->
this.configuratorFactories.each {
it.get().accept(base)
}
}
this.builds.addAll(result)
} else {
throw new IllegalArgumentException("given buildScript ${ this.buildScript } either does not exist or is not a file")
}
this.ranBuildScript = true
logger.trace(exit, '')
}
@Override
boolean doBuild(String buildName, Consumer<Collection<Diagnostic>> diagnosticsConsumer = { }) {
logger.trace(enter, 'buildName: {}, diagnosticsConsumer: {}', buildName, diagnosticsConsumer)
if (!this.ranBuildScript) {
this.runBuildScript()
}
def build = this.builds.find { it.name == buildName }
if (!build) {
throw new IllegalArgumentException("there is no registered build with name: ${ buildName }")
}
def buildTasksConverter = new SimpleBuildTasksConverter()
def successful = true
def tasksResult = buildTasksConverter.convert(build)
if (tasksResult.hasDiagnostics()) {
successful = false
diagnosticsConsumer.accept(tasksResult.diagnostics)
} else {
def tasks = tasksResult.get()
def taskDiagnostics = tasks.collectMany { it.execute(tasks) }
if (!taskDiagnostics.isEmpty()) {
successful = false
diagnosticsConsumer.accept(taskDiagnostics)
}
}
logger.trace(exit, 'successful: {}', successful)
successful
}
}

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

@ -1,322 +0,0 @@
package com.jessebrault.ssg
import com.jessebrault.ssg.buildscript.BuildScriptGetter
import com.jessebrault.ssg.buildscript.BuildScriptToBuildSpecConverter
import com.jessebrault.ssg.buildscript.BuildSpec
import com.jessebrault.ssg.buildscript.delegates.BuildDelegate
import com.jessebrault.ssg.di.*
import com.jessebrault.ssg.page.DefaultWvcPage
import com.jessebrault.ssg.page.Page
import com.jessebrault.ssg.page.PageFactory
import com.jessebrault.ssg.page.PageSpec
import com.jessebrault.ssg.text.Text
import com.jessebrault.ssg.util.Diagnostic
import com.jessebrault.ssg.view.PageView
import com.jessebrault.ssg.view.SkipTemplate
import com.jessebrault.ssg.view.WvcCompiler
import com.jessebrault.ssg.view.WvcPageView
import groovy.transform.TupleConstructor
import com.jessebrault.di.ObjectFactory
import com.jessebrault.di.RegistryObjectFactory
import com.jessebrault.fp.option.Option
import groowt.view.component.compiler.SimpleComponentTemplateClassFactory
import groowt.view.component.factory.ComponentFactories
import groowt.view.component.web.DefaultWebViewComponentContext
import groowt.view.component.web.WebViewComponent
import groowt.view.component.web.WebViewComponentContext
import groowt.view.component.web.WebViewComponentScope
import io.github.classgraph.ClassGraph
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import java.nio.file.Files
import java.nio.file.Path
import static com.jessebrault.di.BindingUtil.named
import static com.jessebrault.di.BindingUtil.toSingleton
@TupleConstructor(includeFields = true, defaults = false)
class DefaultStaticSiteGenerator implements StaticSiteGenerator {
private static final Logger logger = LoggerFactory.getLogger(DefaultStaticSiteGenerator)
protected final GroovyClassLoader groovyClassLoader
protected final URL[] buildScriptBaseUrls
protected final boolean dryRun
protected Set<Text> getTexts(String buildScriptFqn, BuildSpec buildSpec) {
def textConverters = buildSpec.textConverters.get {
new SsgException("The textConverters Property in $buildScriptFqn must contain at least an empty Set.")
}
def textDirs = buildSpec.textsDirs.get {
new SsgException("The textDirs Property in $buildScriptFqn must contain at least an empty Set.")
}
def texts = [] as Set<Text>
textDirs.each { textDir ->
if (textDir.exists()) {
Files.walk(textDir.toPath()).each {
def asFile = it.toFile()
def lastDot = asFile.name.lastIndexOf('.')
if (lastDot != -1) {
def extension = asFile.name.substring(lastDot)
def converter = textConverters.find {
it.handledExtensions.contains(extension)
}
texts << converter.convert(textDir, asFile)
}
}
}
}
texts
}
protected WvcCompiler getWvcCompiler() {
new WvcCompiler(this.groovyClassLoader, new SimpleComponentTemplateClassFactory(this.groovyClassLoader))
}
protected WebViewComponentContext makeContext(
Set<Class<? extends WebViewComponent>> allWvc,
RegistryObjectFactory objectFactory,
WvcPageView pageView
) {
new DefaultWebViewComponentContext().tap {
configureRootScope(WebViewComponentScope) {
// custom components
allWvc.each { wvcClass ->
//noinspection GroovyAssignabilityCheck
add(wvcClass, ComponentFactories.ofClosureClassType(wvcClass) { Map attr, Object[] args ->
WebViewComponent component
if (!attr.isEmpty() && args.length > 0) {
component = objectFactory.createInstance(wvcClass, attr, *args)
} else if (!attr.isEmpty()) {
component = objectFactory.createInstance(wvcClass, attr)
} else if (args.length > 0) {
component = objectFactory.createInstance(wvcClass, *args)
} else {
component = objectFactory.createInstance(wvcClass)
}
component.context = pageView.context
if (component.componentTemplate == null
&& !wvcClass.isAnnotationPresent(SkipTemplate)) {
def compileResult = objectFactory.get(WvcCompiler).compileTemplate(
wvcClass,
wvcClass.simpleName + 'Template.wvc'
)
if (compileResult.isRight()) {
component.componentTemplate = compileResult.getRight()
} else {
def left = compileResult.getLeft()
throw new RuntimeException(left.message, left.exception)
}
}
return component
})
}
}
}
}
protected Option<Diagnostic> handlePage(
String buildScriptFqn,
Page page,
BuildSpec buildSpec,
Set<Class<? extends WebViewComponent>> allWvc,
ObjectFactory objectFactory
) {
// instantiate PageView
def pageViewResult = page.createView()
if (pageViewResult.isLeft()) {
return Option.lift(pageViewResult.getLeft())
}
PageView pageView = pageViewResult.getRight()
// Prepare for rendering
pageView.pageTitle = page.name
pageView.url = buildSpec.baseUrl.get() + page.path
if (pageView instanceof WvcPageView) {
pageView.context = this.makeContext(allWvc, objectFactory, pageView)
}
// Render the page
def sw = new StringWriter()
try {
pageView.renderTo(sw)
} catch (Exception exception) {
return Option.lift(new Diagnostic(
"There was an exception while rendering $page.name as $pageView.class.name",
exception
))
}
// Output the page if not dryRun
if (!this.dryRun) {
def outputDir = buildSpec.outputDir.get {
new SsgException("The outputDir Property in $buildScriptFqn must be set.")
}
outputDir.mkdirs()
def splitPathParts = page.path.split('/')
def pathParts = page.path.endsWith('/')
? splitPathParts + 'index'
: splitPathParts
def path = Path.of(pathParts[0], pathParts.drop(1))
def outputFile = new File(
outputDir,
path.toString() + page.fileExtension
)
outputFile.parentFile.mkdirs()
try {
outputFile.write(sw.toString())
} catch (Exception exception) {
return Option.lift(new Diagnostic(
"There was an exception while writing $page.name to $outputFile",
exception
))
}
}
return Option.empty()
}
@Override
Collection<Diagnostic> doBuild(
File projectDir,
String buildName,
String buildScriptFqn,
Map<String, String> buildScriptCliArgs
) {
def wvcCompiler = this.getWvcCompiler()
// run build script(s) and get buildSpec
def buildScriptGetter = new BuildScriptGetter(
this.groovyClassLoader,
this.buildScriptBaseUrls,
buildScriptCliArgs,
projectDir
)
def buildScriptToBuildSpecConverter = new BuildScriptToBuildSpecConverter(
buildScriptGetter, { String name -> BuildDelegate.withDefaults(name, projectDir) }
)
def buildSpec = buildScriptToBuildSpecConverter.convert(buildScriptFqn)
// prepare objectFactory
def objectFactoryBuilder = buildSpec.objectFactoryBuilder.get {
new SsgException(
"the objectFactoryBuilder Property in $buildScriptFqn " +
"must contain a RegistryObjectFactory.Builder instance."
)
}
// configure objectFactory for Page/PageFactory instantiation
objectFactoryBuilder.configureRegistry {
// extensions
addExtension(new TextsExtension().tap {
allTexts.addAll(this.getTexts(buildScriptFqn, buildSpec))
})
addExtension(new ModelsExtension().tap {
allModels.addAll(buildSpec.models.get {
new SsgException("The models Property in $buildScriptFqn must contain at least an empty Set.")
})
})
addExtension(new GlobalsExtension().tap {
globals.putAll(buildSpec.globals.get {
new SsgException("The globals Property in $buildScriptFqn must contain at least an empty Map.")
})
})
// bindings
bind(named('buildName', String), toSingleton(buildSpec.name))
bind(named('siteName', String), toSingleton(buildSpec.siteName.get {
new SsgException("The siteName Property in $buildScriptFqn must be set.")
}))
bind(named('baseUrl', String), toSingleton(buildSpec.baseUrl.get {
new SsgException("The baseUrl Property in $buildScriptFqn must be set.")
}))
bind(WvcCompiler, toSingleton(wvcCompiler))
}
// get the objectFactory
def objectFactory = objectFactoryBuilder.build()
objectFactory.configureRegistry {
bind(RegistryObjectFactory, toSingleton(objectFactory))
}
// prepare for basePackages scan for Pages and PageFactories
def basePackages = buildSpec.basePackages.get {
new SsgException("The basePackages Property in $buildScriptFqn must contain at least an empty Set.")
}
def classgraph = new ClassGraph()
.enableAnnotationInfo()
.addClassLoader(this.groovyClassLoader)
basePackages.each { classgraph.acceptPackages(it) }
def pages = [] as Set<Page>
def allWvc = [] as Set<Class<? extends WebViewComponent>>
try (def scanResult = classgraph.scan()) {
// single pages
def pageViewInfoList = scanResult.getClassesImplementing(PageView)
pageViewInfoList.each { pageViewInfo ->
def pageSpecInfo = pageViewInfo.getAnnotationInfo(PageSpec)
if (pageSpecInfo != null) {
def pageSpec = (PageSpec) pageSpecInfo.loadClassAndInstantiate()
pages << new DefaultWvcPage(
name: pageSpec.name(),
path: pageSpec.path(),
fileExtension: pageSpec.fileExtension(),
viewType: (Class<? extends PageView>) pageViewInfo.loadClass(),
templateResource: !pageSpec.templateResource().empty
? pageSpec.templateResource()
: pageViewInfo.simpleName + 'Template.wvc',
objectFactory: objectFactory,
wvcCompiler: wvcCompiler
)
}
}
// page factories
def pageFactoryInfoList = scanResult.getClassesImplementing(PageFactory)
def pageFactoryTypes = pageFactoryInfoList.collect() { pageFactoryInfo ->
(pageFactoryInfo.loadClass() as Class<? extends PageFactory>)
}
// instantiate page factory and create the pages
pageFactoryTypes.each { pageFactoryType ->
def pageFactory = objectFactory.createInstance(pageFactoryType)
def created = pageFactory.create()
pages.addAll(created)
}
// get all web view components
def wvcInfoList = scanResult.getClassesImplementing(WebViewComponent)
wvcInfoList.each {
allWvc << it.loadClass(WebViewComponent)
}
}
// Configure objectFactory for PageView instantiation with the Pages we found/created
objectFactory.configureRegistry {
// extensions
addExtension(new PagesExtension().tap {
allPages.addAll(pages)
})
addExtension(new SelfPageExtension())
}
def diagnostics = [] as Collection<Diagnostic>
pages.each {
def result = this.handlePage(buildScriptFqn, it, buildSpec, allWvc, objectFactory)
if (result.isPresent()) {
diagnostics << result.get()
}
}
// return diagnostics
diagnostics
}
}

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 build) {
def taskSpec = new TaskSpec(
build.name,
build.outputDirFunction.apply(build).asFile(),
build.siteSpec,
build.globals
)
Collection<Task> tasks = []
Collection<Diagnostic> diagnostics = []
build.taskFactorySpecs.each {
def factory = it.supplier.get()
it.configurators.each { it.accept(factory) }
def result = factory.getTasks(taskSpec)
diagnostics.addAll(result.diagnostics)
tasks.addAll(result.get())
}
Result.of(diagnostics, tasks)
}
}

View File

@ -0,0 +1,39 @@
package com.jessebrault.ssg
import com.jessebrault.ssg.util.Monoid
import com.jessebrault.ssg.util.Monoids
import groovy.transform.EqualsAndHashCode
import groovy.transform.NullCheck
import groovy.transform.TupleConstructor
@TupleConstructor(defaults = false)
@NullCheck(includeGenerated = true)
@EqualsAndHashCode
final class SiteSpec {
static final Monoid<SiteSpec> DEFAULT_MONOID = Monoids.of(getBlank(), SiteSpec::concat)
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

@ -1,13 +0,0 @@
package com.jessebrault.ssg
class SsgException extends RuntimeException {
SsgException(String message) {
super(message)
}
SsgException(String message, Throwable cause) {
super(message, cause)
}
}

View File

@ -2,11 +2,8 @@ package com.jessebrault.ssg
import com.jessebrault.ssg.util.Diagnostic
import java.util.function.Consumer
interface StaticSiteGenerator {
Collection<Diagnostic> doBuild(
File projectDir,
String buildName,
String buildScriptFqn,
Map<String, String> buildScriptCliArgs
)
}
boolean doBuild(String buildName, Consumer<Collection<Diagnostic>> diagnosticsConsumer)
}

View File

@ -0,0 +1,57 @@
package com.jessebrault.ssg.buildscript
import com.jessebrault.ssg.SiteSpec
import com.jessebrault.ssg.task.TaskFactory
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 {
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<TaskFactory>> ?: []
)
}
static Build concat(Build b0, Build b1) {
new Build(
b0.name.blank ? b1.name : b0.name,
OutputDirFunctions.concat(b0.outputDirFunction, b1.outputDirFunction),
SiteSpec.concat(b0.siteSpec, b1.siteSpec),
b0.globals + b1.globals,
b0.taskFactorySpecs + b1.taskFactorySpecs
)
}
final String name
final Function<Build, OutputDir> outputDirFunction
final SiteSpec siteSpec
final Map<String, Object> globals
final Collection<TaskFactorySpec<TaskFactory>> taskFactorySpecs
Build plus(Build other) {
concat(this, other)
}
}

View File

@ -0,0 +1,57 @@
package com.jessebrault.ssg.buildscript
import groovy.transform.NullCheck
import groovy.transform.PackageScope
@PackageScope
@NullCheck
final class BuildExtension {
static BuildExtension getEmpty() {
new BuildExtension()
}
static BuildExtension get(String buildName) {
new BuildExtension(buildName)
}
private final String buildName
private BuildExtension(String buildName) {
this.buildName = buildName
}
private BuildExtension() {
this.buildName = null
}
boolean isPresent() {
this.buildName != null
}
boolean isEmpty() {
!this.present
}
String getBuildName() {
Objects.requireNonNull(this.buildName)
}
@Override
String toString() {
this.present ? "BuildExtension(extending: ${ this.buildName })" : "BuildExtension(empty)"
}
@Override
int hashCode() {
Objects.hash(this.buildName)
}
@Override
boolean equals(Object obj) {
obj.is(this)
|| (obj instanceof BuildExtension && obj.present && obj.buildName == this.buildName)
|| (obj instanceof BuildExtension && !obj.present && !this.present)
}
}

View File

@ -0,0 +1,79 @@
package com.jessebrault.ssg.buildscript
import groovy.transform.PackageScope
import org.jgrapht.Graph
import org.jgrapht.graph.DefaultEdge
import org.jgrapht.graph.DirectedAcyclicGraph
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.slf4j.Marker
import org.slf4j.MarkerFactory
@PackageScope
final class BuildGraphUtil {
private static final Logger logger = LoggerFactory.getLogger(BuildGraphUtil)
private static final Marker enter = MarkerFactory.getMarker('ENTER')
private static final Marker exit = MarkerFactory.getMarker('EXIT')
private static void addParentEdges(
Graph<BuildSpec, DefaultEdge> graph,
BuildSpec spec,
Collection<BuildSpec> allBuildSpecs
) {
logger.trace(enter, 'graph: {}, spec: {}', graph, spec)
if (!graph.containsVertex(spec)) {
throw new IllegalStateException("given spec is not in the graph")
}
if (spec.extending.present) {
def parent = allBuildSpecs.find { it.name == spec.extending.buildName }
if (parent == null) {
throw new IllegalStateException("no such parent/extends from build: ${ spec.extending.buildName }")
}
if (!graph.containsVertex(parent)) {
graph.addVertex(parent)
}
graph.addEdge(parent, spec)
addParentEdges(graph, parent, allBuildSpecs)
}
logger.trace(exit, '')
}
static Graph<BuildSpec, DefaultEdge> getDependencyGraph(Collection<BuildSpec> buildSpecs) {
logger.trace(enter, '')
final Graph<BuildSpec, DefaultEdge> graph = new DirectedAcyclicGraph<>(DefaultEdge)
buildSpecs.each {
if (!graph.containsVertex(it)) {
graph.addVertex(it)
}
addParentEdges(graph, it, buildSpecs)
}
logger.trace(exit, 'graph: {}', graph)
graph
}
static Collection<BuildSpec> getAncestors(BuildSpec child, Graph<BuildSpec, DefaultEdge> graph) {
logger.trace(enter, 'child: {}, graph: {}', child, graph)
if (child.extending.isEmpty()) {
def r = [] as Collection<BuildSpec>
logger.trace(exit, 'r: {}', r)
r
} else {
// use incoming to get edges pointing to child
def incomingEdges = graph.incomingEdgesOf(child)
if (incomingEdges.size() == 0) {
throw new IllegalArgumentException("child does not have an edge to its parent")
}
if (incomingEdges.size() > 1) {
throw new IllegalArgumentException("child has more than one parent")
}
def parent = graph.getEdgeSource(incomingEdges[0])
def r = getAncestors(parent, graph) + [parent] // needs to be 'oldest' -> 'youngest'
logger.trace(exit, 'r: {}', r)
r
}
}
private BuildGraphUtil() {}
}

View File

@ -1,8 +1,7 @@
package com.jessebrault.ssg.buildscript
import com.jessebrault.ssg.buildscript.delegates.BuildDelegate
import org.jetbrains.annotations.ApiStatus
import org.jetbrains.annotations.Nullable
import groovy.transform.PackageScope
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.slf4j.Marker
@ -10,67 +9,55 @@ import org.slf4j.MarkerFactory
import static java.util.Objects.requireNonNull
@SuppressWarnings('unused')
abstract class BuildScriptBase extends Script {
/* --- Logging --- */
protected static final Logger logger = LoggerFactory.getLogger(BuildScriptBase)
protected static final Marker enter = MarkerFactory.getMarker('ENTER')
protected static final Marker exit = MarkerFactory.getMarker('EXIT')
static final Logger logger = LoggerFactory.getLogger(BuildScriptBase)
static final Marker enter = MarkerFactory.getMarker('ENTER')
static final Marker exit = MarkerFactory.getMarker('EXIT')
protected final Collection<BuildSpec> buildSpecs = []
/* --- build script proper --- */
private String extending
private Closure buildClosure = { }
private File projectRoot
private String buildName
/* --- Instance DSL helpers --- */
File file(String name) {
new File(this.projectRoot, name)
/**
* args keys: name (required), extending (optional)
*
* @param args
* @param buildClosure
*/
void abstractBuild(
Map<String, Object> args,
@DelegatesTo(value = BuildDelegate, strategy = Closure.DELEGATE_FIRST)
Closure<?> buildClosure
) {
this.buildSpecs << new BuildSpec(
requireNonNull(args.name as String),
true,
args.extending != null ? BuildExtension.get(args.extending as String) : BuildExtension.getEmpty(),
buildClosure
)
}
/* --- DSL --- */
void build(@Nullable String extending, @DelegatesTo(value = BuildDelegate) Closure buildClosure) {
this.extending = extending
this.buildClosure = buildClosure
/**
* args keys: name (required), extending (optional)
*
* @param args
* @param buildClosure
*/
void build(
Map<String, Object> args,
@DelegatesTo(value = BuildDelegate, strategy = Closure.DELEGATE_FIRST)
Closure<?> buildClosure
) {
this.buildSpecs << new BuildSpec(
requireNonNull(args.name as String),
false,
args.extending != null ? BuildExtension.get(args.extending as String) : BuildExtension.getEmpty(),
buildClosure
)
}
void build(@DelegatesTo(value = BuildDelegate) Closure buildClosure) {
this.extending = null
this.buildClosure = buildClosure
}
File getProjectRoot() {
requireNonNull(this.projectRoot)
}
@ApiStatus.Internal
void setProjectRoot(File projectRoot) {
this.projectRoot = requireNonNull(projectRoot)
}
String getBuildName() {
return buildName
}
@ApiStatus.Internal
void setBuildName(String buildName) {
this.buildName = buildName
}
@ApiStatus.Internal
@Nullable
String getExtending() {
this.extending
}
@ApiStatus.Internal
Closure getBuildClosure() {
this.buildClosure
@PackageScope
Collection<BuildSpec> getBuildSpecs() {
this.buildSpecs
}
}

View File

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

View File

@ -1,30 +0,0 @@
package com.jessebrault.ssg.buildscript
import groovy.transform.NullCheck
import groovy.transform.TupleConstructor
import org.codehaus.groovy.control.CompilerConfiguration
@NullCheck(includeGenerated = true)
@TupleConstructor(includeFields = true, defaults = false)
final class BuildScriptGetter {
private final GroovyClassLoader groovyClassLoader
private final URL[] scriptBaseUrls
private final Map<String, String> scriptCliArgs
private final File projectDir
BuildScriptBase getAndRunBuildScript(String fqn) {
def gcl = new GroovyClassLoader(this.groovyClassLoader, new CompilerConfiguration().tap {
it.scriptBaseClass = BuildScriptBase.name
})
this.scriptBaseUrls.each { gcl.addURL(it) }
def scriptClass = gcl.loadClass(fqn, true, true) as Class<BuildScriptBase>
def buildScript = scriptClass.getConstructor().newInstance()
buildScript.binding = new Binding(this.scriptCliArgs)
buildScript.projectRoot = projectDir
buildScript.buildName = fqn
buildScript.run()
buildScript
}
}

View File

@ -1,57 +0,0 @@
package com.jessebrault.ssg.buildscript
import com.jessebrault.ssg.buildscript.delegates.BuildDelegate
import groovy.transform.NullCheck
import groovy.transform.TupleConstructor
import java.util.function.Function
import java.util.function.Supplier
@NullCheck
@TupleConstructor(includeFields = true)
class BuildScriptToBuildSpecConverter {
private final BuildScriptGetter buildScriptGetter
private final Function<String, BuildDelegate> buildDelegateFactory
protected BuildSpec getFromDelegate(String name, BuildDelegate delegate) {
new BuildSpec(
name: name,
basePackages: delegate.basePackages,
siteName: delegate.siteName,
baseUrl: delegate.baseUrl,
outputDir: delegate.outputDir,
globals: delegate.globals,
models: delegate.models,
textsDirs: delegate.textsDirs,
textConverters: delegate.textConverters,
objectFactoryBuilder: delegate.objectFactoryBuilder
)
}
protected BuildSpec doConvert(String buildScriptFqn, BuildScriptBase buildScript) {
final Deque<BuildScriptBase> buildHierarchy = new LinkedList<>()
buildHierarchy.push(buildScript)
String extending = buildScript.extending
while (extending != null) {
def from = this.buildScriptGetter.getAndRunBuildScript(extending)
buildHierarchy.push(from)
extending = from.extending
}
def delegate = this.buildDelegateFactory.apply(buildScriptFqn)
while (!buildHierarchy.isEmpty()) {
def currentScript = buildHierarchy.pop()
currentScript.buildClosure.delegate = delegate
currentScript.buildClosure()
}
this.getFromDelegate(buildScriptFqn, delegate)
}
BuildSpec convert(String buildScriptFqn) {
def start = this.buildScriptGetter.getAndRunBuildScript(buildScriptFqn)
this.doConvert(buildScriptFqn, start)
}
}

View File

@ -0,0 +1,79 @@
package com.jessebrault.ssg.buildscript
import groovy.transform.stc.ClosureParams
import groovy.transform.stc.SimpleType
import org.codehaus.groovy.control.CompilerConfiguration
import java.util.function.Consumer
final class BuildScripts {
private static Collection<Build> runBase(BuildScriptBase base) {
base.run()
BuildSpecUtil.getBuilds(base.getBuildSpecs())
}
static Collection<Build> runBuildScript(
@DelegatesTo(value = BuildScriptBase, strategy = Closure.DELEGATE_FIRST)
@ClosureParams(value = SimpleType, options = 'com.jessebrault.ssg.buildscript.BuildScriptBase')
Closure<?> scriptBody
) {
def base = new BuildScriptBase() {
@Override
Object run() {
scriptBody.delegate = this
scriptBody.resolveStrategy = Closure.DELEGATE_FIRST
scriptBody.call(this)
}
}
runBase(base)
}
static Collection<Build> runBuildScript(
String scriptName,
URL scriptBaseDirUrl,
Collection<URL> otherUrls,
Map<String, Object> binding,
Consumer<BuildScriptBase> configureBuildScript
) {
def engine = new GroovyScriptEngine([scriptBaseDirUrl, *otherUrls] as URL[])
engine.config = new CompilerConfiguration().tap {
scriptBaseClass = 'com.jessebrault.ssg.buildscript.BuildScriptBase'
}
def base = engine.createScript(scriptName, new Binding(binding))
assert base instanceof BuildScriptBase
configureBuildScript.accept(base)
runBase(base)
}
static Collection<Build> runBuildScript(
String scriptName,
URL scriptBaseDirUrl,
Collection<URL> otherUrls,
Map<String, Object> binding
) {
runBuildScript(scriptName, scriptBaseDirUrl, otherUrls, binding) { }
}
static Collection<Build> runBuildScript(
String scriptName,
URL scriptBaseDirUrl,
Collection<URL> otherUrls
) {
runBuildScript(scriptName, scriptBaseDirUrl, otherUrls, [:]) { }
}
static Collection<Build> runBuildScript(
String scriptName,
URL scriptBaseDirUrl
) {
runBuildScript(scriptName, scriptBaseDirUrl, [], [:]) { }
}
private BuildScripts() {}
}

View File

@ -1,48 +1,50 @@
package com.jessebrault.ssg.buildscript
import com.jessebrault.ssg.model.Model
import com.jessebrault.ssg.text.TextConverter
import com.jessebrault.ssg.buildscript.delegates.BuildDelegate
import groovy.transform.EqualsAndHashCode
import groovy.transform.NullCheck
import com.jessebrault.di.RegistryObjectFactory
import com.jessebrault.fp.provider.Provider
import groovy.transform.PackageScope
import groovy.transform.TupleConstructor
import static com.jessebrault.ssg.util.ObjectUtil.requireProvider
import static com.jessebrault.ssg.util.ObjectUtil.requireString
@NullCheck(includeGenerated = true)
@EqualsAndHashCode
@PackageScope
@NullCheck()
@EqualsAndHashCode(excludes = 'buildClosure')
final class BuildSpec {
final String name
final Provider<Set<String>> basePackages
final Provider<String> siteName
final Provider<String> baseUrl
final Provider<File> outputDir
final Provider<Map<String, Object>> globals
final Provider<Set<Model>> models
final Provider<Set<File>> textsDirs
final Provider<Set<TextConverter>> textConverters
final Provider<RegistryObjectFactory.Builder> objectFactoryBuilder
static BuildSpec getEmpty() {
new BuildSpec('', false, BuildExtension.getEmpty(), { })
}
@SuppressWarnings('GroovyAssignabilityCheck')
BuildSpec(Map args) {
this.name = requireString(args.name)
this.basePackages = requireProvider(args.basePackages)
this.siteName = requireProvider(args.siteName)
this.baseUrl = requireProvider(args.baseUrl)
this.outputDir = requireProvider(args.outputDir)
this.globals = requireProvider(args.globals)
this.models = requireProvider(args.models)
this.textsDirs = requireProvider(args.textsDirs)
this.textConverters = requireProvider(args.textConverters)
this.objectFactoryBuilder = requireProvider(args.objectFactoryBuilder)
static BuildSpec get(Map<String, Object> args) {
new BuildSpec(
args.name as String ?: '',
args.isAbstract as boolean ?: false,
args.extending as BuildExtension ?: BuildExtension.getEmpty(),
args.buildClosure as Closure<?> ?: { }
)
}
final String name
final boolean isAbstract
final BuildExtension extending
final Closure<?> buildClosure
BuildSpec(
String name,
boolean isAbstract,
BuildExtension extending,
@DelegatesTo(value = BuildDelegate, strategy = Closure.DELEGATE_FIRST)
Closure<?> buildClosure
) {
this.name = name
this.isAbstract = isAbstract
this.extending = extending
this.buildClosure = buildClosure
}
@Override
String toString() {
"Build(name: ${this.name}, basePackages: $basePackages, siteName: $siteName, " +
"baseUrl: $baseUrl, outputDir: $outputDir, textsDirs: $textsDirs)"
"BuildSpec(name: ${ this.name }, isAbstract: ${ this.isAbstract }, extending: ${ this.extending })"
}
}

View File

@ -0,0 +1,105 @@
package com.jessebrault.ssg.buildscript
import com.jessebrault.ssg.SiteSpec
import com.jessebrault.ssg.buildscript.delegates.BuildDelegate
import com.jessebrault.ssg.task.TaskFactory
import com.jessebrault.ssg.task.TaskFactorySpec
import com.jessebrault.ssg.util.Monoid
import com.jessebrault.ssg.util.Monoids
import com.jessebrault.ssg.util.Zero
import org.jgrapht.traverse.DepthFirstIterator
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.slf4j.Marker
import org.slf4j.MarkerFactory
import java.util.function.BiFunction
final class BuildSpecUtil {
private static final Logger logger = LoggerFactory.getLogger(BuildSpecUtil)
private static final Marker enter = MarkerFactory.getMarker('ENTER')
private static final Marker exit = MarkerFactory.getMarker('EXIT')
private static final Monoid<Map<String, Object>> globalsMonoid = Monoids.of([:]) { m0, m1 ->
m0 + m1
}
private static final Monoid<Collection<TaskFactorySpec<TaskFactory>>> taskFactoriesMonoid =
Monoids.getMergeCollectionMonoid(TaskFactorySpec.SAME_NAME_AND_SUPPLIER_EQ, TaskFactorySpec.DEFAULT_SEMIGROUP)
private static <T> T reduceResults(
Collection<BuildDelegate.Results> resultsCollection,
Zero<T> tZero,
BiFunction<T, BuildDelegate.Results, T> resultsToT
) {
resultsCollection.inject(tZero.zero) { acc, r ->
resultsToT.apply(acc, r)
}
}
private static Collection<BuildDelegate.Results> mapBuildSpecsToResults(Collection<BuildSpec> buildSpecs) {
buildSpecs.collect {
def delegate = new BuildDelegate()
it.buildClosure.delegate = delegate
//noinspection UnnecessaryQualifiedReference
it.buildClosure.resolveStrategy = Closure.DELEGATE_FIRST
it.buildClosure()
new BuildDelegate.Results(delegate)
}
}
private static Build toBuild(
Collection<BuildSpec> specs
) {
if (specs.empty) {
throw new IllegalArgumentException('specs must contain at least one BuildSpec')
}
def allResults = mapBuildSpecsToResults(specs)
def outputDirFunctionResult = reduceResults(allResults, OutputDirFunctions.DEFAULT_MONOID) { acc, r ->
r.getOutputDirFunctionResult(acc, { acc })
}
def siteSpecResult = reduceResults(allResults, SiteSpec.DEFAULT_MONOID) { acc, r ->
r.getSiteSpecResult(acc, true, SiteSpec.DEFAULT_MONOID)
}
def globalsResult = reduceResults(allResults, globalsMonoid) { acc, r ->
r.getGlobalsResult(acc, true, globalsMonoid)
}
def typesResult = reduceResults(allResults, TypesContainer.DEFAULT_MONOID) { acc, r ->
r.getTypesResult(acc, true, TypesContainer.DEFAULT_MONOID)
}
def sourcesResult = reduceResults(allResults, SourceProviders.DEFAULT_MONOID) { acc, r ->
r.getSourcesResult(acc, true, SourceProviders.DEFAULT_MONOID, typesResult)
}
def taskFactoriesResult = reduceResults(allResults, taskFactoriesMonoid) { acc, r ->
r.getTaskFactoriesResult(acc, true, taskFactoriesMonoid, sourcesResult)
}
new Build(
specs.last().name,
outputDirFunctionResult,
siteSpecResult,
globalsResult,
taskFactoriesResult
)
}
static Collection<Build> getBuilds(Collection<BuildSpec> buildSpecs) {
logger.trace(enter, '')
def graph = BuildGraphUtil.getDependencyGraph(buildSpecs)
def r = new DepthFirstIterator<>(graph).findResults {
if (it.isAbstract) {
return null
}
def ancestors = BuildGraphUtil.getAncestors(it, graph)
logger.debug('ancestors of {}: {}', it, ancestors)
toBuild([*ancestors, it])
}
logger.trace(exit, 'r: {}', r)
r
}
private BuildSpecUtil() {}
}

View File

@ -0,0 +1,68 @@
package com.jessebrault.ssg.buildscript
import com.jessebrault.ssg.html.PageToHtmlTaskFactory
import com.jessebrault.ssg.html.TextToHtmlSpecProviders
import com.jessebrault.ssg.html.TextToHtmlTaskFactory
import com.jessebrault.ssg.page.PageTypes
import com.jessebrault.ssg.page.PagesProviders
import com.jessebrault.ssg.part.PartTypes
import com.jessebrault.ssg.part.PartsProviders
import com.jessebrault.ssg.template.TemplateTypes
import com.jessebrault.ssg.template.TemplatesProviders
import com.jessebrault.ssg.text.TextTypes
import com.jessebrault.ssg.text.TextsProviders
import groovy.transform.EqualsAndHashCode
import groovy.transform.NullCheck
import groovy.transform.TupleConstructor
import java.util.function.Consumer
@TupleConstructor(includeFields = true, defaults = false)
@NullCheck(includeGenerated = true)
@EqualsAndHashCode(includeFields = true)
final class DefaultBuildScriptConfiguratorFactory implements BuildScriptConfiguratorFactory {
private final File baseDir
@Override
Consumer<BuildScriptBase> get() {
return {
it.build(name: 'default') {
outputDirFunction = { Build build ->
new OutputDir(new File(this.baseDir, build.name == 'default' ? 'build' : build.name))
}
types {
textTypes << TextTypes.MARKDOWN
pageTypes << PageTypes.GSP
templateTypes << TemplateTypes.GSP
partTypes << PartTypes.GSP
}
sources { base, types ->
texts TextsProviders.from(new File(this.baseDir, 'texts'), types.textTypes)
pages PagesProviders.from(new File(this.baseDir, 'pages'), types.pageTypes)
templates TemplatesProviders.from(new File(this.baseDir, 'templates'), types.templateTypes)
parts PartsProviders.from(new File(this.baseDir, 'parts'), types.partTypes)
}
taskFactories { base, sources ->
register('textToHtml', TextToHtmlTaskFactory::new) {
it.specProvider += TextToHtmlSpecProviders.from(sources)
it.allTextsProvider += sources.textsProvider
it.allPartsProvider += sources.partsProvider
it.allModelsProvider += sources.modelsProvider
}
register('pageToHtml', PageToHtmlTaskFactory::new) {
it.pagesProvider += sources.pagesProvider
it.allTextsProvider += sources.textsProvider
it.allPartsProvider += sources.partsProvider
it.allModelsProvider += sources.modelsProvider
}
}
}
}
}
}

View File

@ -0,0 +1,41 @@
package com.jessebrault.ssg.buildscript
import groovy.transform.EqualsAndHashCode
import groovy.transform.NullCheck
@NullCheck
@EqualsAndHashCode
final class OutputDir {
private final String path
OutputDir(String path) {
this.path = path
}
OutputDir(File file) {
this(file.path)
}
File asFile() {
new File(this.path)
}
String asString() {
this.path
}
Object asType(Class<?> clazz) {
switch (clazz) {
case File -> this.asFile()
case String -> this.asString()
default -> throw new IllegalArgumentException('cannot cast to a class other than File or String')
}
}
@Override
String toString() {
"OutputDir(path: ${ this.path })"
}
}

View File

@ -0,0 +1,31 @@
package com.jessebrault.ssg.buildscript
import com.jessebrault.ssg.util.Monoid
import com.jessebrault.ssg.util.Monoids
import java.util.function.Function
final class OutputDirFunctions {
static final Function<Build, OutputDir> DEFAULT = { Build build -> new OutputDir(build.name) }
static final Monoid<Function<Build, OutputDir>> DEFAULT_MONOID = Monoids.of(DEFAULT, OutputDirFunctions::concat)
static Function<Build, OutputDir> concat(
Function<Build, OutputDir> f0,
Function<Build, OutputDir> f1
) {
f0 == OutputDirFunctions.DEFAULT ? f1 : f0
}
static Function<Build, OutputDir> of(File dir) {
return { new OutputDir(dir) }
}
static Function<Build, OutputDir> of(String path) {
return { new OutputDir(path) }
}
private OutputDirFunctions() {}
}

View File

@ -0,0 +1,75 @@
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 com.jessebrault.ssg.util.Monoid
import com.jessebrault.ssg.util.Monoids
import groovy.transform.EqualsAndHashCode
import groovy.transform.NullCheck
import groovy.transform.TupleConstructor
@TupleConstructor(defaults = false)
@NullCheck(includeGenerated = true)
@EqualsAndHashCode
final class SourceProviders {
static final Monoid<SourceProviders> DEFAULT_MONOID = Monoids.of(getEmpty(), SourceProviders::concat)
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)
}
@Override
String toString() {
"SourceProviders(textsProvider: ${ this.textsProvider }, modelsProvider: ${ this.modelsProvider }, " +
"pagesProvider: ${ this.pagesProvider }, templatesProvider: ${ this.templatesProvider }, " +
"partsProvider: ${ this.partsProvider })"
}
}

View File

@ -0,0 +1,57 @@
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 com.jessebrault.ssg.util.Monoid
import com.jessebrault.ssg.util.Monoids
import groovy.transform.EqualsAndHashCode
import groovy.transform.NullCheck
import groovy.transform.TupleConstructor
@TupleConstructor(defaults = false)
@NullCheck(includeGenerated = true)
@EqualsAndHashCode
final class TypesContainer {
static final Monoid<TypesContainer> DEFAULT_MONOID = Monoids.of(getEmpty(), TypesContainer::concat)
static TypesContainer getEmpty() {
new TypesContainer([], [], [], [])
}
static TypesContainer get(Map<String, Object> args) {
new TypesContainer(
args.textTypes ? args.textTypes as Collection<TextType> : [],
args.pageTypes ? args.pageTypes as Collection<PageType> : [],
args.templateTypes ? args.templateTypes as Collection<TemplateType> : [],
args.partTypes ? args.partTypes as Collection<PartType> : []
)
}
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
)
}
final Collection<TextType> textTypes
final Collection<PageType> pageTypes
final Collection<TemplateType> templateTypes
final Collection<PartType> partTypes
TypesContainer plus(TypesContainer other) {
concat(this, other)
}
@Override
String toString() {
"TypesContainer(textTypes: ${ this.textTypes }, pageTypes: ${ this.pageTypes }, " +
"templateTypes: ${ this.templateTypes }, partTypes: ${ this.partTypes })"
}
}

View File

@ -1,132 +1,287 @@
package com.jessebrault.ssg.buildscript.delegates
import com.jessebrault.ssg.model.Model
import com.jessebrault.ssg.model.Models
import com.jessebrault.ssg.text.MarkdownTextConverter
import com.jessebrault.ssg.text.TextConverter
import com.jessebrault.ssg.util.PathUtil
import com.jessebrault.di.DefaultRegistryObjectFactory
import com.jessebrault.di.RegistryObjectFactory
import com.jessebrault.fp.property.DefaultProperty
import com.jessebrault.fp.property.Property
import com.jessebrault.fp.provider.DefaultProvider
import com.jessebrault.fp.provider.NamedProvider
import com.jessebrault.fp.provider.Provider
import com.jessebrault.ssg.SiteSpec
import com.jessebrault.ssg.buildscript.Build
import com.jessebrault.ssg.buildscript.OutputDir
import com.jessebrault.ssg.buildscript.SourceProviders
import com.jessebrault.ssg.buildscript.TypesContainer
import com.jessebrault.ssg.mutable.Mutable
import com.jessebrault.ssg.mutable.Mutables
import com.jessebrault.ssg.task.TaskFactory
import com.jessebrault.ssg.task.TaskFactorySpec
import com.jessebrault.ssg.util.Monoid
import groovy.transform.EqualsAndHashCode
import groovy.transform.NullCheck
import groovy.transform.TupleConstructor
import groovy.transform.stc.ClosureParams
import groovy.transform.stc.FromString
import groovy.transform.stc.SimpleType
import java.nio.file.Path
import java.util.function.Function
import java.util.function.Supplier
import java.util.function.UnaryOperator
@NullCheck(includeGenerated = true)
@EqualsAndHashCode(includeFields = true)
final class BuildDelegate {
static BuildDelegate withDefaults(String buildName, File projectDir) {
new BuildDelegate(projectDir).tap {
basePackages.convention = [] as Set<String>
outputDir.convention = PathUtil.resolve(projectDir, Path.of('dist', buildName.split(/\\./)))
globals.convention = [:]
models.convention = [] as Set<Model>
textsDirs.convention = [new File(projectDir, 'texts')] as Set<File>
textConverters.convention = [new MarkdownTextConverter()] as Set<TextConverter>
objectFactoryBuilder.convention = DefaultRegistryObjectFactory.Builder.withDefaults()
@TupleConstructor(includeFields = true, defaults = false)
@NullCheck(includeGenerated = true)
@EqualsAndHashCode(includeFields = true)
static final class Results {
private final BuildDelegate delegate
Function<Build, OutputDir> getOutputDirFunctionResult(
Function<Build, OutputDir> base,
Supplier<Function<Build, OutputDir>> onEmpty
) {
this.delegate.outputDirFunction.getOrElse {
this.delegate.outputDirFunctionMapper.match(onEmpty) {
it.apply(base)
}
}
}
SiteSpec getSiteSpecResult(
SiteSpec base,
boolean onConcatWithBaseEmpty,
Monoid<SiteSpec> siteSpecMonoid
) {
def concatWithBase = this.delegate.siteSpecConcatBase.isPresent()
? this.delegate.siteSpecConcatBase.get()
: onConcatWithBaseEmpty
def onEmpty = { concatWithBase ? base : siteSpecMonoid.zero }
this.delegate.siteSpecClosure.match(onEmpty) {
def d = new SiteSpecDelegate(siteSpecMonoid)
it.delegate = d
//noinspection UnnecessaryQualifiedReference
it.resolveStrategy = Closure.DELEGATE_FIRST
it(base)
def r = d.getResult()
concatWithBase ? siteSpecMonoid.concat.apply(base, r) : r
}
}
Map<String, Object> getGlobalsResult(
Map<String, Object> base,
boolean onConcatWithBaseEmpty,
Monoid<Map<String, Object>> globalsMonoid
) {
def concatWithBase = this.delegate.globalsConcatBase.isPresent()
? this.delegate.globalsConcatBase.get()
: onConcatWithBaseEmpty
def onEmpty = { concatWithBase ? base : globalsMonoid.zero }
this.delegate.globalsClosure.match(onEmpty) {
def d = new GlobalsDelegate()
it.delegate = d
//noinspection UnnecessaryQualifiedReference
it.resolveStrategy = Closure.DELEGATE_FIRST
it(base)
def r = d.getResult()
concatWithBase ? globalsMonoid.concat.apply(base, r) : r
}
}
TypesContainer getTypesResult(
TypesContainer base,
boolean onConcatWithBaseEmpty,
Monoid<TypesContainer> typesContainerMonoid
) {
def concatWithBase = this.delegate.typesConcatBase.isPresent()
? this.delegate.typesConcatBase.get()
: onConcatWithBaseEmpty
def onEmpty = { concatWithBase ? base : typesContainerMonoid.zero }
this.delegate.typesClosure.match(onEmpty) {
def d = new TypesDelegate()
it.delegate = d
//noinspection UnnecessaryQualifiedReference
it.resolveStrategy = Closure.DELEGATE_FIRST
it(base)
def r = d.getResult()
concatWithBase ? typesContainerMonoid.concat.apply(base, r) : r
}
}
SourceProviders getSourcesResult(
SourceProviders base,
boolean onConcatWithBaseEmpty,
Monoid<SourceProviders> sourceProvidersMonoid,
TypesContainer types
) {
def concatWithBase = this.delegate.sourcesConcatBase.isPresent()
? this.delegate.sourcesConcatBase.get()
: onConcatWithBaseEmpty
def onEmpty = { concatWithBase ? base : sourceProvidersMonoid.zero }
this.delegate.sourcesClosure.match(onEmpty) {
def d = new SourceProvidersDelegate()
it.delegate = d
//noinspection UnnecessaryQualifiedReference
it.resolveStrategy = Closure.DELEGATE_FIRST
it(base, types)
def r = d.getResult()
concatWithBase ? sourceProvidersMonoid.concat.apply(base, r) : r
}
}
Collection<TaskFactorySpec<TaskFactory>> getTaskFactoriesResult(
Collection<TaskFactorySpec<TaskFactory>> base,
boolean onConcatWithBaseEmpty,
Monoid<Collection<TaskFactorySpec<TaskFactory>>> taskFactorySpecsMonoid,
SourceProviders sources
) {
def concatWithBase = this.delegate.taskFactoriesConcatBase.isPresent()
? this.delegate.taskFactoriesConcatBase.get()
: onConcatWithBaseEmpty
def onEmpty = { concatWithBase ? base : taskFactorySpecsMonoid.zero }
this.delegate.taskFactoriesClosure.match(onEmpty) {
def d = new TaskFactoriesDelegate()
it.delegate = d
//noinspection UnnecessaryQualifiedReference
it.resolveStrategy = Closure.DELEGATE_FIRST
it(base, sources)
def r = d.getResult()
concatWithBase ? taskFactorySpecsMonoid.concat.apply(base, r) : r
}
}
}
final File projectDir
private final Mutable<Function<Build, OutputDir>> outputDirFunction = Mutables.getEmpty()
private final Mutable<UnaryOperator<Function<Build, OutputDir>>> outputDirFunctionMapper = Mutables.getEmpty()
final Property<Set<String>> basePackages = DefaultProperty.<Set<String>>empty(Set)
final Property<String> siteName = DefaultProperty.empty(String)
final Property<String> baseUrl = DefaultProperty.empty(String)
final Property<File> outputDir = DefaultProperty.empty(File)
final Property<Map<String, Object>> globals = DefaultProperty.<Map<String, Object>>empty(Map)
final Property<Set<Model>> models = DefaultProperty.<Set<Model>>empty(Set)
final Property<Set<File>> textsDirs = DefaultProperty.<Set<File>>empty(Set)
final Property<Set<TextConverter>> textConverters = DefaultProperty.<Set<TextConverter>>empty(Set)
final Property<RegistryObjectFactory.Builder> objectFactoryBuilder =
DefaultProperty.empty(RegistryObjectFactory.Builder)
private final Mutable<Boolean> siteSpecConcatBase = Mutables.getEmpty()
private final Mutable<Closure<?>> siteSpecClosure = Mutables.getEmpty()
private BuildDelegate(File projectDir) {
this.projectDir = projectDir
private final Mutable<Boolean> globalsConcatBase = Mutables.getEmpty()
private final Mutable<Closure<?>> globalsClosure = Mutables.getEmpty()
private final Mutable<Boolean> typesConcatBase = Mutables.getEmpty()
private final Mutable<Closure<?>> typesClosure = Mutables.getEmpty()
private final Mutable<Boolean> sourcesConcatBase = Mutables.getEmpty()
private final Mutable<Closure<?>> sourcesClosure = Mutables.getEmpty()
private final Mutable<Boolean> taskFactoriesConcatBase = Mutables.getEmpty()
private final Mutable<Closure<?>> taskFactoriesClosure = Mutables.getEmpty()
void setOutputDirFunction(Function<Build, OutputDir> outputDirFunction) {
this.outputDirFunction.set(outputDirFunction)
}
/* TODO: add friendly DSL methods for setting all properties */
void basePackage(String toAdd) {
this.basePackages.configure { it.add(toAdd) }
void setOutputDir(File file) {
this.outputDirFunction.set { new OutputDir(file) }
}
void basePackages(String... toAdd) {
toAdd.each { this.basePackage(it) }
void setOutputDir(String path) {
this.outputDirFunction.set { new OutputDir(path) }
}
void siteName(String siteName) {
this.siteName.set(siteName)
// Maps the *base*
void outputDirFunction(UnaryOperator<Function<Build, OutputDir>> mapper) {
this.outputDirFunctionMapper.set(mapper)
}
void siteName(Provider<String> siteNameProvider) {
this.siteName.set(siteNameProvider)
void siteSpec(
@DelegatesTo(value = SiteSpecDelegate, strategy = Closure.DELEGATE_FIRST)
@ClosureParams(value = SimpleType, options = 'com.jessebrault.ssg.SiteSpec')
Closure<?> siteSpecClosure
) {
this.siteSpec(true, siteSpecClosure)
}
void baseUrl(String baseUrl) {
this.baseUrl.set(baseUrl)
void siteSpec(
boolean concatWithBase,
@DelegatesTo(value = SiteSpecDelegate, strategy = Closure.DELEGATE_FIRST)
@ClosureParams(value = SimpleType, options = 'com.jessebrault.ssg.SiteSpec')
Closure<?> siteSpecClosure
) {
this.siteSpecConcatBase.set(concatWithBase)
this.siteSpecClosure.set(siteSpecClosure)
}
void baseUrl(Provider<String> baseUrlProvider) {
this.baseUrl.set(baseUrlProvider)
void globals(
@DelegatesTo(value = GlobalsDelegate, strategy = Closure.DELEGATE_FIRST)
@ClosureParams(value = FromString, options = 'Map<String, Object>')
Closure<?> globalsClosure
) {
this.globals(true, globalsClosure)
}
void outputDir(File outputDir) {
this.outputDir.set(outputDir)
void globals(
boolean concatWithBase,
@DelegatesTo(value = GlobalsDelegate, strategy = Closure.DELEGATE_FIRST)
@ClosureParams(value = FromString, options = 'Map<String, Object>')
Closure<?> globalsClosure
) {
this.globalsConcatBase.set(concatWithBase)
this.globalsClosure.set(globalsClosure)
}
void outputDir(Provider<File> outputDirProvider) {
this.outputDir.set(outputDirProvider)
void types(
@DelegatesTo(value = TypesDelegate, strategy = Closure.DELEGATE_FIRST)
@ClosureParams(value = SimpleType, options = 'com.jessebrault.ssg.buildscript.TypesContainer')
Closure<?> typesClosure
) {
this.types(true, typesClosure)
}
void globals(@DelegatesTo(value = GlobalsDelegate, strategy = Closure.DELEGATE_FIRST) Closure globalsClosure) {
def globalsDelegate = new GlobalsDelegate()
globalsClosure.delegate = globalsDelegate
globalsClosure.resolveStrategy = Closure.DELEGATE_FIRST
globalsClosure()
this.globals.set this.globals.get() + globalsDelegate
void types(
boolean concatWithBase,
@DelegatesTo(value = TypesDelegate, strategy = Closure.DELEGATE_FIRST)
@ClosureParams(value = SimpleType, options = 'com.jessebrault.ssg.buildscript.TypesContainer')
Closure<?> typesClosure
) {
this.typesConcatBase.set(concatWithBase)
this.typesClosure.set(typesClosure)
}
void model(String name, Object obj) {
this.models.configure {it.add(Models.of(name, obj)) }
void sources(
@DelegatesTo(value = SourceProvidersDelegate, strategy = Closure.DELEGATE_FIRST)
@ClosureParams(
value = FromString,
options = 'com.jessebrault.ssg.buildscript.SourceProviders, com.jessebrault.ssg.buildscript.TypesContainer'
)
Closure<?> sourcesClosure
) {
this.sources(true, sourcesClosure)
}
void model(Model model) {
this.models.configure { it.add(model) }
void sources(
boolean concatWithBase,
@DelegatesTo(value = SourceProvidersDelegate, strategy = Closure.DELEGATE_FIRST)
@ClosureParams(
value = FromString,
options = 'com.jessebrault.ssg.buildscript.SourceProviders, com.jessebrault.ssg.buildscript.TypesContainer'
)
Closure<?> sourcesClosure
) {
this.sourcesConcatBase.set(concatWithBase)
this.sourcesClosure.set(sourcesClosure)
}
void model(String name, Provider tProvider) {
this.models.configure { it.add(Models.ofProvider(name, tProvider)) }
void taskFactories(
@DelegatesTo(value = TaskFactoriesDelegate, strategy = Closure.DELEGATE_FIRST)
@ClosureParams(
value = FromString,
options = 'java.util.Collection<com.jessebrault.ssg.task.TaskFactorySpec<com.jessebrault.ssg.task.TaskFactory>>, com.jessebrault.ssg.buildscript.SourceProviders'
)
Closure<?> taskFactoriesClosure
) {
this.taskFactories(true, taskFactoriesClosure)
}
<T> void model(String name, Class<T> type, Supplier<? extends T> tSupplier) {
this.models.configure { it.add(Models.ofSupplier(name, type, tSupplier)) }
}
void model(NamedProvider namedProvider) {
this.models.configure { it.add(Models.ofNamedProvider(namedProvider)) }
}
void textsDir(File textsDir) {
this.textsDirs.configure { it.add(textsDir) }
}
void textsDirs(File... textsDirs) {
textsDirs.each { this.textsDir(it) }
}
void textConverter(TextConverter textConverter) {
this.textConverters.configure { it.add(textConverter) }
}
void textConverters(TextConverter... textConverters) {
textConverters.each { this.textConverter(it) }
}
void objectFactoryBuilder(RegistryObjectFactory.Builder builder) {
this.objectFactoryBuilder.set(builder)
void taskFactories(
boolean concatWithBase,
@DelegatesTo(value = TaskFactoriesDelegate, strategy = Closure.DELEGATE_FIRST)
@ClosureParams(
value = FromString,
options = 'java.util.Collection<com.jessebrault.ssg.task.TaskFactorySpec<com.jessebrault.ssg.task.TaskFactory>>, com.jessebrault.ssg.buildscript.SourceProviders'
)
Closure<?> taskFactoriesClosure
) {
this.taskFactoriesConcatBase.set(concatWithBase)
this.taskFactoriesClosure.set(taskFactoriesClosure)
}
}

View File

@ -0,0 +1,38 @@
package com.jessebrault.ssg.buildscript.delegates
import com.jessebrault.ssg.SiteSpec
import com.jessebrault.ssg.util.Zero
import static java.util.Objects.requireNonNull
final class SiteSpecDelegate {
private String name
private String baseUrl
SiteSpecDelegate(Zero<SiteSpec> siteSpecZero) {
this.name = siteSpecZero.zero.name
this.baseUrl = siteSpecZero.zero.baseUrl
}
String getName() {
return this.name
}
void setName(String name) {
this.name = requireNonNull(name)
}
String getBaseUrl() {
return this.baseUrl
}
void setBaseUrl(String baseUrl) {
this.baseUrl = requireNonNull(baseUrl)
}
SiteSpec getResult() {
new SiteSpec(this.name, this.baseUrl)
}
}

View File

@ -0,0 +1,54 @@
package com.jessebrault.ssg.buildscript.delegates
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
import groovy.transform.EqualsAndHashCode
import groovy.transform.NullCheck
@NullCheck
@EqualsAndHashCode
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,66 @@
package com.jessebrault.ssg.buildscript.delegates
import com.jessebrault.ssg.task.TaskFactory
import com.jessebrault.ssg.task.TaskFactorySpec
import groovy.transform.EqualsAndHashCode
import groovy.transform.NullCheck
import java.util.function.Consumer
import java.util.function.Supplier
@NullCheck
@EqualsAndHashCode(includeFields = true)
final class TaskFactoriesDelegate {
private final Collection<TaskFactorySpec<TaskFactory>> specs = []
private boolean isRegistered(String name) {
this.specs.find { it.name == name } != null
}
private void checkNotRegistered(String name) {
if (this.isRegistered(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 << new TaskFactorySpec<>(name, factorySupplier, [])
}
def <T extends TaskFactory> void register(
String name,
Supplier<T> factorySupplier,
Consumer<T> factoryConfigurator
) {
this.checkNotRegistered(name)
this.specs << new TaskFactorySpec<>(name, factorySupplier, [factoryConfigurator])
}
void register(TaskFactorySpec<TaskFactory> spec) {
this.specs << spec
}
void registerAll(Collection<TaskFactorySpec<TaskFactory>> specs) {
this.specs.addAll(specs)
}
def <T extends TaskFactory> void configure(
String name,
Class<T> factoryClass, // Dummy so we get better auto-complete
Consumer<T> factoryConfigurator
) {
if (!this.isRegistered(name)) {
throw new IllegalArgumentException("there is no TaskFactory registered by name ${ name }")
}
def spec = this.specs.find { it.name == name }
// Potentially dangerous, but the configurators Collection *should* only contain the correct types.
spec.configurators << (factoryConfigurator as Consumer<TaskFactory>)
}
Collection<TaskFactorySpec<TaskFactory>> getResult() {
this.specs
}
}

View File

@ -0,0 +1,24 @@
package com.jessebrault.ssg.buildscript.delegates
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

@ -1,15 +0,0 @@
package com.jessebrault.ssg.di
import jakarta.inject.Qualifier
import java.lang.annotation.ElementType
import java.lang.annotation.Retention
import java.lang.annotation.RetentionPolicy
import java.lang.annotation.Target
@Qualifier
@Retention(RetentionPolicy.RUNTIME)
@Target([ElementType.PARAMETER, ElementType.METHOD, ElementType.FIELD])
@interface Global {
String value()
}

View File

@ -1,43 +0,0 @@
package com.jessebrault.ssg.di
import groovy.transform.TupleConstructor
import com.jessebrault.di.Binding
import com.jessebrault.di.QualifierHandler
import com.jessebrault.di.QualifierHandlerContainer
import com.jessebrault.di.RegistryExtension
import com.jessebrault.di.SingletonBinding
import java.lang.annotation.Annotation
class GlobalsExtension implements QualifierHandlerContainer, RegistryExtension {
@TupleConstructor(includeFields = true)
static class GlobalQualifierHandler implements QualifierHandler<Global> {
private final GlobalsExtension extension
@Override
<T> Binding<T> handle(Global global, Class<T> aClass) {
if (extension.globals.containsKey(global.value())) {
return new SingletonBinding<T>(extension.globals.get(global.value()) as T)
} else {
throw new IllegalArgumentException("There is no global for ${global.value()}")
}
}
}
final Map<String, Object> globals = [:]
private final GlobalQualifierHandler globalQualifierHandler = new GlobalQualifierHandler(this)
@Override
<A extends Annotation> QualifierHandler<A> getQualifierHandler(Class<A> aClass) {
if (Global.is(aClass)) {
return this.globalQualifierHandler as QualifierHandler<A>
} else {
return null
}
}
}

View File

@ -1,15 +0,0 @@
package com.jessebrault.ssg.di
import jakarta.inject.Qualifier
import java.lang.annotation.ElementType
import java.lang.annotation.Retention
import java.lang.annotation.RetentionPolicy
import java.lang.annotation.Target
@Qualifier
@Retention(RetentionPolicy.RUNTIME)
@Target([ElementType.PARAMETER, ElementType.METHOD, ElementType.FIELD])
@interface InjectModel {
String value()
}

View File

@ -1,26 +0,0 @@
package com.jessebrault.ssg.di
import groovy.transform.TupleConstructor
import com.jessebrault.di.Binding
import com.jessebrault.di.QualifierHandler
import com.jessebrault.di.SingletonBinding
@TupleConstructor(includeFields = true)
class InjectModelQualifierHandler implements QualifierHandler<InjectModel> {
private final ModelsExtension extension
@Override
<T> Binding<T> handle(InjectModel injectModel, Class<T> requestedClass) {
def found = this.extension.allModels.find {
requestedClass.isAssignableFrom(it.class) && it.name == injectModel.value()
}
if (found == null) {
throw new IllegalArgumentException(
"Could not find a Model with name ${injectModel.value()} and/or type $requestedClass.name"
)
}
new SingletonBinding<T>(found.get() as T)
}
}

View File

@ -1,15 +0,0 @@
package com.jessebrault.ssg.di
import jakarta.inject.Qualifier
import java.lang.annotation.ElementType
import java.lang.annotation.Retention
import java.lang.annotation.RetentionPolicy
import java.lang.annotation.Target
@Qualifier
@Retention(RetentionPolicy.RUNTIME)
@Target([ElementType.PARAMETER, ElementType.METHOD, ElementType.FIELD])
@interface InjectModels {
String[] value()
}

View File

@ -1,27 +0,0 @@
package com.jessebrault.ssg.di
import groovy.transform.TupleConstructor
import com.jessebrault.di.Binding
import com.jessebrault.di.QualifierHandler
import com.jessebrault.di.SingletonBinding
@TupleConstructor(includeFields = true)
class InjectModelsQualifierHandler implements QualifierHandler<InjectModels> {
private final ModelsExtension extension
@Override
<T> Binding<T> handle(InjectModels injectModels, Class<T> requestedType) {
if (!List.is(requestedType)) {
throw new IllegalArgumentException("@InjectModels must be used with List.")
}
def allFound = this.extension.allModels.inject([] as T) { acc, model ->
if (model.type.isAssignableFrom(requestedType) && model.name in injectModels.value()) {
acc << model.get()
}
acc
}
new SingletonBinding<T>(allFound as T)
}
}

View File

@ -1,20 +0,0 @@
package com.jessebrault.ssg.di
import jakarta.inject.Qualifier
import java.lang.annotation.ElementType
import java.lang.annotation.Retention
import java.lang.annotation.RetentionPolicy
import java.lang.annotation.Target
@Qualifier
@Retention(RetentionPolicy.RUNTIME)
@Target([ElementType.PARAMETER, ElementType.METHOD, ElementType.FIELD])
@interface InjectPage {
/**
* May be either a page name or a path starting with '/'
*/
String value()
}

View File

@ -1,33 +0,0 @@
package com.jessebrault.ssg.di
import com.jessebrault.ssg.page.Page
import groovy.transform.TupleConstructor
import com.jessebrault.di.Binding
import com.jessebrault.di.QualifierHandler
import com.jessebrault.di.SingletonBinding
@TupleConstructor(includeFields = true)
class InjectPageQualifierHandler implements QualifierHandler<InjectPage> {
private final PagesExtension pagesExtension
@Override
<T> Binding<T> handle(InjectPage injectPage, Class<T> requestedClass) {
if (!Page.isAssignableFrom(requestedClass)) {
throw new IllegalArgumentException("Cannot inject a Page into a non-Page parameter/setter/field.")
}
def requested = injectPage.value()
def found = this.pagesExtension.allPages.find {
if (requested.startsWith('/')) {
it.path == requested
} else {
it.name == requested
}
}
if (found == null) {
throw new IllegalArgumentException("Cannot find a page with the following name or path: $requested")
}
new SingletonBinding<T>(found as T)
}
}

View File

@ -1,20 +0,0 @@
package com.jessebrault.ssg.di
import jakarta.inject.Qualifier
import java.lang.annotation.ElementType
import java.lang.annotation.Retention
import java.lang.annotation.RetentionPolicy
import java.lang.annotation.Target
@Qualifier
@Retention(RetentionPolicy.RUNTIME)
@Target([ElementType.PARAMETER, ElementType.METHOD, ElementType.FIELD])
@interface InjectPages {
/**
* Names of pages and/or globs (starting with '/') of pages
*/
String[] value()
}

View File

@ -1,44 +0,0 @@
package com.jessebrault.ssg.di
import com.jessebrault.ssg.page.Page
import com.jessebrault.ssg.util.Glob
import groovy.transform.TupleConstructor
import com.jessebrault.di.Binding
import com.jessebrault.di.QualifierHandler
import com.jessebrault.di.SingletonBinding
@TupleConstructor(includeFields = true)
class InjectPagesQualifierHandler implements QualifierHandler<InjectPages> {
private final PagesExtension pagesExtension
@Override
<T> Binding<T> handle(InjectPages injectPages, Class<T> requestedClass) {
if (!(Set.isAssignableFrom(requestedClass))) {
throw new IllegalArgumentException(
'Cannot inject a Collection of Pages into a non-Collection parameter/setter/field.'
)
}
def foundPages = [] as Set<Page>
for (final String requested : injectPages.value()) {
if (requested.startsWith('/')) {
def glob = new Glob(requested)
def allFound = this.pagesExtension.allPages.inject([] as Set<Page>) { acc, page ->
if (glob.matches(page.path)) {
acc << page
}
acc
}
allFound.each { foundPages << it }
} else {
def found = this.pagesExtension.allPages.find { it.name == requested }
if (found == null) {
throw new IllegalArgumentException("Cannot find page with the name: $requested")
}
foundPages << found
}
}
new SingletonBinding<T>(foundPages as T)
}
}

View File

@ -1,20 +0,0 @@
package com.jessebrault.ssg.di
import jakarta.inject.Qualifier
import java.lang.annotation.ElementType
import java.lang.annotation.Retention
import java.lang.annotation.RetentionPolicy
import java.lang.annotation.Target
@Qualifier
@Retention(RetentionPolicy.RUNTIME)
@Target([ElementType.PARAMETER, ElementType.METHOD, ElementType.FIELD])
@interface InjectText {
/**
* The name of the text, or the path of the text, starting with '/'
*/
String value()
}

View File

@ -1,28 +0,0 @@
package com.jessebrault.ssg.di
import com.jessebrault.ssg.text.Text
import groovy.transform.TupleConstructor
import com.jessebrault.di.Binding
import com.jessebrault.di.QualifierHandler
import com.jessebrault.di.SingletonBinding
@TupleConstructor(includeFields = true)
class InjectTextQualifierHandler implements QualifierHandler<InjectText> {
private final TextsExtension extension
@Override
<T> Binding<T> handle(InjectText injectText, Class<T> requestedClass) {
if (!Text.isAssignableFrom(requestedClass)) {
throw new IllegalArgumentException("Cannot @InjectText on a non-Text parameter/method/field.")
}
def found = this.extension.allTexts.find {
it.name == injectText.value() || it.path == injectText.value()
}
if (found == null) {
throw new IllegalArgumentException("Could not find a Text with name or path ${injectText.value()}")
}
new SingletonBinding<T>(found as T)
}
}

View File

@ -1,20 +0,0 @@
package com.jessebrault.ssg.di
import jakarta.inject.Qualifier
import java.lang.annotation.ElementType
import java.lang.annotation.Retention
import java.lang.annotation.RetentionPolicy
import java.lang.annotation.Target
@Qualifier
@Retention(RetentionPolicy.RUNTIME)
@Target([ElementType.PARAMETER, ElementType.METHOD, ElementType.FIELD])
@interface InjectTexts {
/**
* Names of texts and/or globs (starting with '/') of texts
*/
String[] value()
}

View File

@ -1,39 +0,0 @@
package com.jessebrault.ssg.di
import com.jessebrault.ssg.text.Text
import com.jessebrault.ssg.util.Glob
import groovy.transform.TupleConstructor
import com.jessebrault.di.Binding
import com.jessebrault.di.QualifierHandler
import com.jessebrault.di.SingletonBinding
@TupleConstructor(includeFields = true)
class InjectTextsQualifierHandler implements QualifierHandler<InjectTexts> {
private final TextsExtension extension
@Override
<T> Binding<T> handle(InjectTexts injectTexts, Class<T> aClass) {
if (!Set.is(aClass)) {
throw new IllegalArgumentException('Cannot @InjectTexts on a non-Set parameter/method/field.')
}
def allFound = injectTexts.value().inject([] as Set<Text>) { acc, nameOrPathGlob ->
if (nameOrPathGlob.startsWith('/')) {
def glob = new Glob(nameOrPathGlob)
def matching = this.extension.allTexts.inject([] as Set<Text>) { matchingAcc, text ->
if (glob.matches(text.path)) {
matchingAcc << text
}
matchingAcc
}
acc.addAll(matching)
} else {
def found = this.extension.allTexts.find { it.name == nameOrPathGlob }
acc << found
}
acc
}
new SingletonBinding<T>(allFound as T)
}
}

View File

@ -1,28 +0,0 @@
package com.jessebrault.ssg.di
import com.jessebrault.ssg.model.Model
import com.jessebrault.di.QualifierHandler
import com.jessebrault.di.QualifierHandlerContainer
import com.jessebrault.di.RegistryExtension
import java.lang.annotation.Annotation
class ModelsExtension implements QualifierHandlerContainer, RegistryExtension {
final Set<Model> allModels = []
private final QualifierHandler<InjectModel> injectModelQualifierHandler = new InjectModelQualifierHandler(this)
private final QualifierHandler<InjectModels> injectModelsQualifierHandler = new InjectModelsQualifierHandler(this)
@Override
<A extends Annotation> QualifierHandler<A> getQualifierHandler(Class<A> aClass) {
if (aClass == InjectModel) {
return this.injectModelQualifierHandler as QualifierHandler<A>
} else if (aClass == InjectModels) {
return this.injectModelsQualifierHandler as QualifierHandler<A>
} else {
return null
}
}
}

View File

@ -1,27 +0,0 @@
package com.jessebrault.ssg.di
import com.jessebrault.ssg.page.Page
import com.jessebrault.di.QualifierHandler
import com.jessebrault.di.QualifierHandlerContainer
import com.jessebrault.di.RegistryExtension
import java.lang.annotation.Annotation
class PagesExtension implements QualifierHandlerContainer, RegistryExtension {
final Set<Page> allPages = []
private final QualifierHandler<InjectPage> injectPage = new InjectPageQualifierHandler(this)
private final QualifierHandler<InjectPages> injectPages = new InjectPagesQualifierHandler(this)
@Override
<A extends Annotation> QualifierHandler<A> getQualifierHandler(Class<A> annotationClass) {
if (annotationClass == InjectPage) {
return this.injectPage as QualifierHandler<A>
} else if (annotationClass == InjectPages) {
return this.injectPages as QualifierHandler<A>
}
return null
}
}

View File

@ -1,13 +0,0 @@
package com.jessebrault.ssg.di
import jakarta.inject.Qualifier
import java.lang.annotation.ElementType
import java.lang.annotation.Retention
import java.lang.annotation.RetentionPolicy
import java.lang.annotation.Target
@Qualifier
@Retention(RetentionPolicy.RUNTIME)
@Target([ElementType.PARAMETER, ElementType.METHOD, ElementType.FIELD])
@interface SelfPage {}

View File

@ -1,42 +0,0 @@
package com.jessebrault.ssg.di
import com.jessebrault.ssg.page.Page
import groovy.transform.TupleConstructor
import com.jessebrault.di.*
import java.lang.annotation.Annotation
class SelfPageExtension implements RegistryExtension, QualifierHandlerContainer {
@TupleConstructor(includeFields = true)
static class SelfPageQualifierHandler implements QualifierHandler<SelfPage> {
private final SelfPageExtension extension
@Override
<T> Binding<T> handle(SelfPage selfPage, Class<T> requestedType) {
if (!Page.class.isAssignableFrom(requestedType)) {
throw new IllegalArgumentException('Cannot put @SelfPage on a non-Page parameter/method/field.')
}
if (this.extension.currentPage == null) {
throw new IllegalStateException('Cannot get @SelfPage because extension.currentPage is null.')
}
new SingletonBinding<T>(this.extension.currentPage as T)
}
}
Page currentPage
private final SelfPageQualifierHandler selfPageQualifierHandler = new SelfPageQualifierHandler(this)
@Override
<A extends Annotation> QualifierHandler<A> getQualifierHandler(Class<A> annotationType) {
if (SelfPage.is(annotationType)) {
return this.selfPageQualifierHandler as QualifierHandler<A>
} else {
return null
}
}
}

View File

@ -1,229 +0,0 @@
package com.jessebrault.ssg.di;
import com.jessebrault.di.*;
import jakarta.inject.Named;
import org.jetbrains.annotations.Nullable;
import java.lang.annotation.Annotation;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.function.Consumer;
import java.util.function.Predicate;
public class SsgNamedRegistryExtension implements NamedRegistryExtension {
protected static void checkName(String name) {
if (name.startsWith(":") || name.endsWith(":")) {
throw new IllegalArgumentException(
"Illegal ssg @Named format: cannot start or end with colon (':'); given: " + name
);
}
}
protected static @Nullable String getPrefix(String fullName) {
final var firstColon = fullName.indexOf(":");
if (firstColon == -1) {
return null;
} else {
return fullName.substring(0, firstColon);
}
}
protected static String getAfterPrefix(String fullName) {
final int firstColon = fullName.indexOf(":");
if (firstColon == -1) {
return fullName;
} else {
return fullName.substring(firstColon + 1);
}
}
protected static boolean hasPrefix(String fullName) {
return fullName.contains(":");
}
protected static String getSimplePrefix(Class<?> dependencyClass) {
final String simpleTypeName = dependencyClass.getSimpleName();
final String simpleTypeNameStart = simpleTypeName.substring(0, 1).toLowerCase();
final String uncapitalizedSimpleTypeName;
if (simpleTypeName.length() > 1) {
uncapitalizedSimpleTypeName = simpleTypeNameStart + simpleTypeName.substring(1);
} else {
uncapitalizedSimpleTypeName = simpleTypeNameStart;
}
return uncapitalizedSimpleTypeName;
}
protected static String getCanonicalPrefix(Class<?> dependencyClass) {
return dependencyClass.getName();
}
public static class SsgNamedQualifierHandler implements QualifierHandler<Named> {
private final SsgNamedRegistryExtension extension;
public SsgNamedQualifierHandler(SsgNamedRegistryExtension extension) {
this.extension = extension;
}
@Override
public @Nullable <T> Binding<T> handle(Named annotation, Class<T> dependencyClass) {
return this.extension.getBinding(new SimpleKeyHolder<>(
SsgNamedRegistryExtension.class,
dependencyClass,
annotation.value()
));
}
}
public static class SsgNamedWithPrefixKeyHolder<T> implements KeyHolder<NamedRegistryExtension, String, T> {
private final Class<T> dependencyType;
private final @Nullable String prefix;
private final String afterPrefix;
public SsgNamedWithPrefixKeyHolder(
Class<T> dependencyType,
@Nullable String prefix,
String afterPrefix
) {
this.dependencyType = dependencyType;
this.afterPrefix = afterPrefix;
this.prefix = prefix;
}
@Override
public Class<NamedRegistryExtension> binderType() {
return NamedRegistryExtension.class;
}
@Override
public Class<T> type() {
return this.dependencyType;
}
@Override
public String key() {
return this.afterPrefix;
}
public @Nullable String prefix() {
return this.prefix;
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj instanceof SsgNamedWithPrefixKeyHolder<?> other) {
return this.dependencyType.equals(other.type())
&& this.key().equals(other.key())
&& Objects.equals(this.prefix(), other.prefix());
} else {
return false;
}
}
@Override
public int hashCode() {
int result = dependencyType.hashCode();
result = 31 * result + afterPrefix.hashCode();
result = 31 * result + Objects.hashCode(prefix);
return result;
}
}
private final Map<KeyHolder<NamedRegistryExtension, String, ?>, Binding<?>> bindings = new HashMap<>();
private final QualifierHandler<Named> qualifierHandler = this.getNamedQualifierHandler();
protected QualifierHandler<Named> getNamedQualifierHandler() {
return new SsgNamedQualifierHandler(this);
}
@Override
public Class<String> getKeyClass() {
return String.class;
}
@Override
public <B extends KeyBinder<String>, T> void bind(
KeyHolder<B, ? extends String, T> keyHolder,
Consumer<? super BindingConfigurator<T>> configure
) {
final var configurator = new SimpleBindingConfigurator<>(keyHolder.type());
configure.accept(configurator);
final String fullName = keyHolder.key();
checkName(fullName);
if (hasPrefix(fullName)) {
this.bindings.put(new SsgNamedWithPrefixKeyHolder<>(
keyHolder.type(),
getPrefix(fullName),
getAfterPrefix(fullName)
), configurator.getBinding());
} else {
this.bindings.put(new SimpleKeyHolder<>(
NamedRegistryExtension.class,
keyHolder.type(),
keyHolder.key()
), configurator.getBinding());
}
}
@SuppressWarnings("unchecked")
@Override
public @Nullable <B extends KeyBinder<String>, T> Binding<T> getBinding(
KeyHolder<B, ? extends String, T> keyHolder
) {
final String fullName = keyHolder.key();
checkName(fullName);
if (hasPrefix(fullName)) {
if (keyHolder instanceof SsgNamedWithPrefixKeyHolder<?> && this.bindings.containsKey(keyHolder)) {
return (Binding<T>) this.bindings.get(keyHolder);
} else {
final String afterPrefix = getAfterPrefix(fullName);
final Class<T> type = keyHolder.type();
final @Nullable Binding<T> withSimple = (Binding<T>) this.bindings.get(
new SsgNamedWithPrefixKeyHolder<>(type, afterPrefix, getSimplePrefix(type))
);
if (withSimple != null) {
return withSimple;
}
return (Binding<T>) this.bindings.get(new SsgNamedWithPrefixKeyHolder<>(
type, afterPrefix, getCanonicalPrefix(type)
));
}
} else {
return (Binding<T>) this.bindings.getOrDefault(keyHolder, null);
}
}
@Override
public <B extends KeyBinder<String>, T> void removeBinding(KeyHolder<B, ? extends String, T> keyHolder) {
throw new UnsupportedOperationException();
}
@Override
public <B extends KeyBinder<String>, T> void removeBindingIf(
KeyHolder<B, ? extends String, T> keyHolder,
Predicate<? super Binding<T>> filter
) {
throw new UnsupportedOperationException();
}
@Override
public void clearAllBindings() {
this.bindings.clear();
}
@SuppressWarnings("unchecked")
@Override
public @Nullable <A extends Annotation> QualifierHandler<A> getQualifierHandler(Class<A> qualifierType) {
return (QualifierHandler<A>) this.qualifierHandler;
}
}

View File

@ -1,19 +0,0 @@
package com.jessebrault.ssg.di
import com.jessebrault.di.DefaultRegistryObjectFactory
import com.jessebrault.di.RegistryObjectFactory
final class SsgObjectFactoryUtil {
static RegistryObjectFactory getDefault() {
DefaultRegistryObjectFactory.Builder.withDefaults().with {
it.configureRegistry { registry ->
registry.addExtension(new PagesExtension())
}
build()
}
}
private SsgObjectFactoryUtil() {}
}

View File

@ -1,28 +0,0 @@
package com.jessebrault.ssg.di
import com.jessebrault.ssg.text.Text
import com.jessebrault.di.QualifierHandler
import com.jessebrault.di.QualifierHandlerContainer
import com.jessebrault.di.RegistryExtension
import java.lang.annotation.Annotation
class TextsExtension implements QualifierHandlerContainer, RegistryExtension {
final Set<Text> allTexts = []
private final QualifierHandler<InjectText> injectTextQualifierHandler = new InjectTextQualifierHandler(this)
private final QualifierHandler<InjectTexts> injectTextsQualifierHandler = new InjectTextsQualifierHandler(this)
@Override
<A extends Annotation> QualifierHandler<A> getQualifierHandler(Class<A> aClass) {
if (InjectText.is(aClass)) {
return this.injectTextQualifierHandler as QualifierHandler<A>
} else if (InjectTexts.is(aClass)) {
return this.injectTextsQualifierHandler as QualifierHandler<A>
} else {
return null
}
}
}

View File

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

View File

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

View File

@ -0,0 +1,63 @@
package com.jessebrault.ssg.dsl
import com.jessebrault.ssg.text.FrontMatter
import com.jessebrault.ssg.text.Text
import com.jessebrault.ssg.util.Diagnostic
import groovy.transform.EqualsAndHashCode
import groovy.transform.Memoized
import groovy.transform.NullCheck
import groovy.transform.TupleConstructor
import java.util.function.Consumer
@TupleConstructor(includeFields = true, defaults = false)
@NullCheck(includeGenerated = true)
@EqualsAndHashCode(includeFields = true)
final class EmbeddableText {
private final Text text
private final Consumer<Collection<Diagnostic>> diagnosticsConsumer
@Memoized
String render() {
def result = this.text.type.renderer.render(this.text)
if (result.diagnostics.size() > 0) {
this.diagnosticsConsumer.accept(result.diagnostics)
''
} else {
result.get()
}
}
@Memoized
FrontMatter getFrontMatter() {
def result = this.text.type.frontMatterGetter.get(this.text)
if (result.hasDiagnostics()) {
this.diagnosticsConsumer.accept(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.diagnosticsConsumer.accept(result.diagnostics)
''
} else {
result.get()
}
}
String getPath() {
this.text.path
}
@Override
String toString() {
"EmbeddableText(text: ${ this.text })"
}
}

View File

@ -0,0 +1,28 @@
package com.jessebrault.ssg.dsl
import com.jessebrault.ssg.text.Text
import com.jessebrault.ssg.util.Diagnostic
import groovy.transform.EqualsAndHashCode
import groovy.transform.NullCheck
import java.util.function.Consumer
@NullCheck
@EqualsAndHashCode(includeFields = true)
final class EmbeddableTextsCollection {
@Delegate
private final Collection<EmbeddableText> embeddableTexts = []
EmbeddableTextsCollection(Collection<Text> texts, Consumer<Collection<Diagnostic>> diagnosticsConsumer) {
texts.each {
this << new EmbeddableText(it, diagnosticsConsumer)
}
}
@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,81 @@
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 com.jessebrault.ssg.util.Diagnostic
import groovy.transform.NullCheck
import org.slf4j.LoggerFactory
import java.util.function.Consumer
final class StandardDslMap {
@NullCheck(includeGenerated = true)
static final class Builder {
private final Map<String, Object> custom = [:]
Consumer<Collection<Diagnostic>> diagnosticsConsumer = { }
String loggerName = ''
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,
Consumer<Builder> builderConsumer
) {
def b = new Builder()
builderConsumer.accept(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.diagnosticsConsumer,
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.diagnosticsConsumer
) : null
it.texts = new EmbeddableTextsCollection(
context.texts,
b.diagnosticsConsumer
)
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, Object> 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, Object> 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, Object> attributes)
String create(String name, String body)
String create(String name, Map<String, Object> attributes, String body)
}

View File

@ -0,0 +1,59 @@
package com.jessebrault.ssg.html
import com.jessebrault.ssg.task.AbstractTask
import com.jessebrault.ssg.task.Task
import com.jessebrault.ssg.task.TaskInput
import com.jessebrault.ssg.util.Diagnostic
import com.jessebrault.ssg.util.Result
import groovy.transform.EqualsAndHashCode
import groovy.transform.NullCheck
import org.jsoup.Jsoup
@NullCheck
@EqualsAndHashCode
abstract class AbstractHtmlTask<I extends TaskInput> extends AbstractTask implements HtmlTask {
final String htmlPath
final I input
final HtmlOutput output
AbstractHtmlTask(
String name,
String htmlPath,
I input,
File buildDir
) {
super(name)
this.htmlPath = htmlPath
this.input = input
this.output = new SimpleHtmlOutput(
"htmlOutput:${ htmlPath }",
new File(buildDir, htmlPath),
htmlPath
)
}
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 document = Jsoup.parse(content)
document.outputSettings().indentAmount(4)
def formatted = document.toString()
this.output.file.createParentDirectories()
this.output.file.write(formatted)
[]
}
}
@Override
String toString() {
"AbstractHtmlTask(path: ${ this.htmlPath }, super: ${ super.toString() })"
}
}

View File

@ -0,0 +1,7 @@
package com.jessebrault.ssg.html
import com.jessebrault.ssg.task.FileOutput
interface HtmlOutput extends FileOutput {
String getHtmlPath()
}

View File

@ -0,0 +1,12 @@
package com.jessebrault.ssg.html
import com.jessebrault.ssg.task.Task
import com.jessebrault.ssg.task.TaskInput
interface HtmlTask extends Task {
@Deprecated
String getHtmlPath()
TaskInput getInput()
HtmlOutput getOutput()
}

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,71 @@
package com.jessebrault.ssg.html
import com.jessebrault.ssg.SiteSpec
import com.jessebrault.ssg.model.Model
import com.jessebrault.ssg.model.ModelInput
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<ModelInput<T>> {
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 relativeHtmlPath,
TaskSpec taskSpec,
Model<T> model,
Template template,
Collection<Text> allTexts,
Collection<Model<Object>> allModels,
Collection<Part> allParts
) {
super(
"modelToHtml:${ relativeHtmlPath }",
relativeHtmlPath,
new ModelInput<>(model.name, model),
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.htmlPath,
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,68 @@
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.page.PageInput
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<PageInput> {
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 relativeHtmlPath,
TaskSpec taskSpec,
Page page,
Collection<Text> allTexts,
Collection<Model<Object>> allModels,
Collection<Part> allParts
) {
super(
"pageToHtml:${ relativeHtmlPath }",
relativeHtmlPath,
new PageInput(page.path, page),
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.htmlPath,
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,15 @@
package com.jessebrault.ssg.html
import groovy.transform.EqualsAndHashCode
import groovy.transform.NullCheck
import groovy.transform.PackageScope
import groovy.transform.TupleConstructor
@TupleConstructor(defaults = false)
@NullCheck(includeGenerated = true)
@EqualsAndHashCode
final class SimpleHtmlOutput implements HtmlOutput {
final String name
final File file
final String htmlPath
}

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,36 @@
package com.jessebrault.ssg.html
import com.jessebrault.ssg.buildscript.SourceProviders
import com.jessebrault.ssg.provider.CollectionProvider
import com.jessebrault.ssg.provider.CollectionProviders
import com.jessebrault.ssg.util.ExtensionUtil
import com.jessebrault.ssg.util.Result
final class TextToHtmlSpecProviders {
static CollectionProvider<Result<TextToHtmlSpec>> from(SourceProviders sources) {
CollectionProviders.fromSupplier {
def templates = sources.templatesProvider.provide()
sources.textsProvider.provide().findResults {
def frontMatterResult = it.type.frontMatterGetter.get(it)
if (frontMatterResult.hasDiagnostics()) {
return Result.ofDiagnostics(frontMatterResult.diagnostics) as Result<TextToHtmlSpec>
}
def templateValue = frontMatterResult.get().get('template')
if (templateValue) {
def template = templates.find { it.path == templateValue }
return Result.of(new TextToHtmlSpec(
it,
template,
ExtensionUtil.stripExtension(it.path) + '.html'
))
} else {
return null
}
}
}
}
private TextToHtmlSpecProviders() {}
}

View File

@ -0,0 +1,71 @@
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.text.TextInput
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<TextInput> {
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 relativeHtmlPath,
TaskSpec taskSpec,
Text text,
Template template,
Collection<Text> allTexts,
Collection<Model<Object>> allModels,
Collection<Part> allParts
) {
super(
"textToHtml:${ relativeHtmlPath }",
relativeHtmlPath,
new TextInput(text.path, text),
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.htmlPath,
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

@ -2,6 +2,5 @@ package com.jessebrault.ssg.model
interface Model<T> {
String getName()
Class<T> getType()
T get()
}

View File

@ -0,0 +1,14 @@
package com.jessebrault.ssg.model
import com.jessebrault.ssg.task.TaskInput
import groovy.transform.EqualsAndHashCode
import groovy.transform.NullCheck
import groovy.transform.TupleConstructor
@TupleConstructor(defaults = false)
@NullCheck(includeGenerated = true)
@EqualsAndHashCode
final class ModelInput<T> implements TaskInput {
final String name
final Model<T> model
}

View File

@ -1,27 +1,15 @@
package com.jessebrault.ssg.model
import com.jessebrault.fp.provider.NamedProvider
import com.jessebrault.fp.provider.Provider
import java.util.function.Supplier
final class Models {
@SuppressWarnings('GroovyAssignabilityCheck')
static <T> Model<T> of(String name, T t) {
new SimpleModel<>(name, t.class, t)
new SimpleModel<>(name, t)
}
static <T> Model<T> ofSupplier(String name, Class<T> type, Supplier<? extends T> tClosure) {
new SupplierBasedModel<>(name, type, tClosure)
}
static <T> Model<T> ofProvider(String name, Provider<? extends T> modelProvider) {
new ProviderModel<T>(name, modelProvider.type, modelProvider)
}
static <T> Model<T> ofNamedProvider(NamedProvider<? extends T> namedModelProvider) {
new ProviderModel<T>(namedModelProvider.name, namedModelProvider.type, namedModelProvider)
static <T> Model<T> fromSupplier(String name, Supplier<T> tClosure) {
new SupplierBasedModel<>(name, tClosure)
}
private Models() {}

View File

@ -12,7 +12,6 @@ import groovy.transform.TupleConstructor
final class SimpleModel<T> implements Model<T> {
final String name
final Class<T> type
private final T t
@Override
@ -22,7 +21,7 @@ final class SimpleModel<T> implements Model<T> {
@Override
String toString() {
"SimpleModel(name: $name, type: $type.name)"
"SimpleModel(${ this.t })"
}
}

View File

@ -14,17 +14,11 @@ import java.util.function.Supplier
final class SupplierBasedModel<T> implements Model<T> {
final String name
final Class<T> type
private final Supplier<? extends T> supplier
private final Supplier<T> supplier
@Override
T get() {
this.supplier.get()
}
@Override
String toString() {
"SupplierBasedModel(name: $name, type: $type.name)"
}
}

View File

@ -0,0 +1,33 @@
package com.jessebrault.ssg.mutable;
import org.jetbrains.annotations.ApiStatus;
import java.util.Optional;
import java.util.function.*;
@ApiStatus.Experimental
public interface Mutable<T> {
T get();
void set(T t);
void unset();
boolean isPresent();
default boolean isEmpty() {
return !this.isPresent();
}
void filterInPlace(Predicate<T> filter);
void mapInPlace(UnaryOperator<T> mapper);
<U> void zipInPlace(Mutable<U> other, Supplier<T> onEmpty, Supplier<U> onOtherEmpty, BiFunction<T, U, T> zipper);
<U> U match(Supplier<U> onEmpty, Function<T, U> onPresentMapper);
T getOrElse(Supplier<T> onEmpty);
<U> Mutable<U> chain(Function<T, Mutable<U>> mapper);
<U> Mutable<U> map(Function<T, U> mapper);
<U, R> Mutable<R> zip(Mutable<U> other, BiFunction<T, U, R> zipper);
Optional<T> asOptional();
}

View File

@ -0,0 +1,36 @@
package com.jessebrault.ssg.mutable;
import com.jessebrault.ssg.util.Monoid;
import com.jessebrault.ssg.util.Monoids;
import com.jessebrault.ssg.util.Semigroup;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
@ApiStatus.Experimental
public final class Mutables {
public static <T> Monoid<Mutable<T>> getMonoid(final Semigroup<T> tSemigroup) {
return Monoids.of(Mutables.getEmpty(), (m0, m1) -> {
if (m0.isPresent() && m1.isPresent()) {
return get(tSemigroup.getConcat().apply(m0.get(), m1.get()));
} else if (m0.isPresent()) {
return m0;
} else if (m1.isPresent()) {
return m1;
} else {
return getEmpty();
}
});
}
public static <T> @NotNull Mutable<T> getEmpty() {
return new SimpleMutable<>();
}
public static <T> Mutable<T> get(T initialValue) {
return new SimpleMutable<>(initialValue);
}
private Mutables() {}
}

View File

@ -0,0 +1,94 @@
package com.jessebrault.ssg.mutable;
import org.jetbrains.annotations.ApiStatus;
import java.util.Optional;
import java.util.function.*;
@ApiStatus.Experimental
final class SimpleMutable<T> implements Mutable<T> {
private T t;
public SimpleMutable(T initialValue) {
this.t = initialValue;
}
public SimpleMutable() {}
@Override
public T get() {
if (this.t == null) {
throw new NullPointerException();
}
return this.t;
}
@Override
public void set(T t) {
this.t = t;
}
@Override
public void unset() {
this.t = null;
}
@Override
public boolean isPresent() {
return this.t != null;
}
@Override
public void filterInPlace(Predicate<T> filter) {
if (this.t != null && !filter.test(this.t)) {
this.unset();
}
}
@Override
public void mapInPlace(UnaryOperator<T> mapper) {
this.t = mapper.apply(this.t);
}
@Override
public <U> void zipInPlace(Mutable<U> other, Supplier<T> onEmpty, Supplier<U> onOtherEmpty, BiFunction<T, U, T> zipper) {
this.t = zipper.apply(
this.isPresent() ? this.t : onEmpty.get(),
other.isPresent() ? other.get() : onOtherEmpty.get()
);
}
@Override
public <U> U match(Supplier<U> onEmpty, Function<T, U> onPresentMapper) {
return this.t != null ? onPresentMapper.apply(this.t) : onEmpty.get();
}
@Override
public T getOrElse(Supplier<T> other) {
return this.t != null ? this.t : other.get();
}
@Override
public <U> Mutable<U> chain(Function<T, Mutable<U>> mapper) {
return this.map(mapper).get();
}
@Override
public <U> Mutable<U> map(Function<T, U> mapper) {
return this.t != null ? Mutables.get(mapper.apply(this.t)) : Mutables.getEmpty();
}
@Override
public <U, R> Mutable<R> zip(Mutable<U> other, BiFunction<T, U, R> zipper) {
return this.isPresent() && other.isPresent()
? Mutables.get(zipper.apply(this.get(), other.get()))
: Mutables.getEmpty();
}
@Override
public Optional<T> asOptional() {
return Optional.ofNullable(this.t);
}
}

View File

@ -1,33 +0,0 @@
package com.jessebrault.ssg.page
abstract class AbstractPage implements Page {
final String name
final String path
final String fileExtension
AbstractPage(Map args) {
name = args.name
path = args.path
fileExtension = args.fileExtension
}
@Override
int hashCode() {
Objects.hash(name, path, fileExtension)
}
@Override
boolean equals(Object obj) {
if (this.is(obj)) {
return true
} else if (obj instanceof Page) {
return name == obj.name
&& path == obj.path
&& fileExtension == obj.fileExtension
} else {
return false
}
}
}

View File

@ -1,76 +0,0 @@
package com.jessebrault.ssg.page
import com.jessebrault.ssg.di.SelfPageExtension
import com.jessebrault.ssg.util.Diagnostic
import com.jessebrault.ssg.view.PageView
import com.jessebrault.ssg.view.WvcCompiler
import com.jessebrault.ssg.view.WvcPageView
import com.jessebrault.di.RegistryObjectFactory
import com.jessebrault.fp.either.Either
class DefaultWvcPage extends AbstractPage implements Page {
final Class<? extends WvcPageView> viewType
final String templateResource
final RegistryObjectFactory objectFactory
final WvcCompiler wvcCompiler
DefaultWvcPage(Map args) {
super(args)
viewType = args.viewType
templateResource = args.templateResource ?: viewType.simpleName + 'Template.wvc'
objectFactory = args.objectFactory
wvcCompiler = args.wvcCompiler
}
@Override
Either<Diagnostic, PageView> createView() {
WvcPageView pageView
try {
objectFactory.registry.getExtension(SelfPageExtension).currentPage = this
pageView = objectFactory.createInstance(viewType)
} catch (Exception exception) {
return Either.left(new Diagnostic(
"There was an exception while constructing $viewType.name for $name",
exception
))
}
if (pageView.componentTemplate == null) {
def compileResult = wvcCompiler.compileTemplate(viewType, templateResource)
if (compileResult.isRight()) {
pageView.componentTemplate = compileResult.getRight()
} else {
return Either.left(compileResult.getLeft())
}
}
return Either.right(pageView)
}
@Override
int hashCode() {
Objects.hash(name, path, fileExtension, viewType, templateResource, objectFactory, wvcCompiler)
}
@Override
boolean equals(Object obj) {
if (!super.equals(obj)) {
return false
} else if (obj instanceof DefaultWvcPage) {
return viewType == obj.viewType
&& templateResource == obj.templateResource
&& objectFactory == obj.objectFactory
&& wvcCompiler == obj.wvcCompiler
} else {
return false
}
}
@Override
String toString() {
"DefaultPage(name: $name, path: $path, fileExtension: $fileExtension, " +
"viewType: $viewType, templateResource: $templateResource)"
}
}

View File

@ -0,0 +1,44 @@
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.diagnosticsConsumer = diagnostics.&addAll
it.loggerName = "GspSpecialPage(${ specialPage.path })"
}
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

@ -1,15 +1,21 @@
package com.jessebrault.ssg.page
import com.jessebrault.ssg.util.Diagnostic
import com.jessebrault.ssg.view.PageView
import com.jessebrault.fp.either.Either
import groovy.transform.EqualsAndHashCode
import groovy.transform.NullCheck
import groovy.transform.TupleConstructor
interface Page {
@TupleConstructor(defaults = false)
@NullCheck(includeGenerated = true)
@EqualsAndHashCode
final class Page {
String getName()
String getPath()
String getFileExtension()
final String path
final PageType type
final String text
Either<Diagnostic, PageView> createView()
@Override
String toString() {
"Page(path: ${ this.path }, type: ${ this.type })"
}
}

View File

@ -1,5 +0,0 @@
package com.jessebrault.ssg.page
interface PageFactory {
Collection<Page> create()
}

View File

@ -0,0 +1,14 @@
package com.jessebrault.ssg.page
import com.jessebrault.ssg.task.TaskInput
import groovy.transform.EqualsAndHashCode
import groovy.transform.NullCheck
import groovy.transform.TupleConstructor
@TupleConstructor(defaults = false)
@NullCheck(includeGenerated = true)
@EqualsAndHashCode
final class PageInput implements TaskInput {
final String name
final Page page
}

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

@ -1,15 +0,0 @@
package com.jessebrault.ssg.page
import java.lang.annotation.ElementType
import java.lang.annotation.Retention
import java.lang.annotation.RetentionPolicy
import java.lang.annotation.Target
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@interface PageSpec {
String name()
String path()
String templateResource() default ''
String fileExtension() default '.html'
}

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,33 @@
package com.jessebrault.ssg.page
import com.jessebrault.ssg.provider.CollectionProvider
import com.jessebrault.ssg.provider.CollectionProviders
import com.jessebrault.ssg.util.ExtensionUtil
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.fromDirectory(pagesDirectory) { file, relativePath ->
def extension = ExtensionUtil.getExtension(relativePath)
if (extension) {
def pageType = pageTypes.find { it.ids.contains(extension) }
if (!pageType) {
logger.debug('there is no PageType for file {}; skipping', file)
return null
} else {
return new Page(relativePath, pageType, file.getText())
}
} else {
logger.debug('there is no extension for file {}; skipping', file)
return null
}
}
}
private PagesProviders() {}
}

View File

@ -0,0 +1,55 @@
package com.jessebrault.ssg.part
import com.jessebrault.ssg.render.RenderContext
import com.jessebrault.ssg.render.StandardGspRenderer
import com.jessebrault.ssg.text.Text
import com.jessebrault.ssg.util.Diagnostic
import com.jessebrault.ssg.util.Result
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.diagnosticsConsumer = diagnostics.&addAll
it.loggerName = "GspPart(${ part.path })"
if (text) {
it.text = text
}
}
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,33 @@
package com.jessebrault.ssg.part
import com.jessebrault.ssg.provider.CollectionProvider
import com.jessebrault.ssg.provider.CollectionProviders
import com.jessebrault.ssg.util.ExtensionUtil
import org.slf4j.Logger
import org.slf4j.LoggerFactory
final class PartsProviders {
private static final Logger logger = LoggerFactory.getLogger(PartsProviders)
static CollectionProvider<Part> from(File partsDir, Collection<PartType> partTypes) {
CollectionProviders.fromDirectory(partsDir) { file, relativePath ->
def extension = ExtensionUtil.getExtension(relativePath)
if (extension) {
def partType = partTypes.find { it.ids.contains(extension) }
if (!partType) {
logger.debug('there is no PartType for file {}; skipping', file)
return null
} else {
return new Part(relativePath, partType, file.getText())
}
} else {
logger.debug('there is no extension for file {}; skipping', file)
return null
}
}
}
private PartsProviders() {}
}

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