Compare commits
No commits in common. "master" and "v0.2.0-SNAPSHOT" have entirely different histories.
master
...
v0.2.0-SNA
@ -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
29
.github/workflows/release.yml
vendored
Normal 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 }}
|
||||
|
16
README.md
16
README.md
@ -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
60
TODO.md
@ -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.*
|
||||
```
|
||||
```
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
|
||||
}
|
@ -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)
|
||||
}
|
@ -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
|
||||
}
|
||||
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
39
api/src/main/groovy/com/jessebrault/ssg/SiteSpec.groovy
Normal file
39
api/src/main/groovy/com/jessebrault/ssg/SiteSpec.groovy
Normal 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 })"
|
||||
}
|
||||
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
@ -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)
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
@ -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() {}
|
||||
|
||||
}
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,7 @@
|
||||
package com.jessebrault.ssg.buildscript
|
||||
|
||||
import java.util.function.Consumer
|
||||
|
||||
interface BuildScriptConfiguratorFactory {
|
||||
Consumer<BuildScriptBase> get()
|
||||
}
|
@ -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
|
||||
}
|
||||
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
@ -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() {}
|
||||
|
||||
}
|
@ -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 })"
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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() {}
|
||||
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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 })"
|
||||
}
|
||||
|
||||
}
|
@ -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() {}
|
||||
|
||||
}
|
@ -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 })"
|
||||
}
|
||||
|
||||
}
|
@ -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 })"
|
||||
}
|
||||
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
@ -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
|
||||
)
|
||||
}
|
||||
|
||||
}
|
@ -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
|
||||
}
|
||||
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
@ -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()
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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()
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
@ -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()
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
@ -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()
|
||||
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
@ -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()
|
||||
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
@ -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()
|
||||
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
@ -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()
|
||||
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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
|
||||
}
|
||||
|
||||
}
|
@ -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 {}
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -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() {}
|
||||
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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 })"
|
||||
}
|
||||
|
||||
}
|
@ -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 })"
|
||||
}
|
||||
|
||||
}
|
@ -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 })"
|
||||
}
|
||||
|
||||
}
|
@ -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 })"
|
||||
}
|
||||
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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>
|
||||
}
|
||||
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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)
|
||||
}
|
@ -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() })"
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
package com.jessebrault.ssg.html
|
||||
|
||||
import com.jessebrault.ssg.task.FileOutput
|
||||
|
||||
interface HtmlOutput extends FileOutput {
|
||||
String getHtmlPath()
|
||||
}
|
12
api/src/main/groovy/com/jessebrault/ssg/html/HtmlTask.groovy
Normal file
12
api/src/main/groovy/com/jessebrault/ssg/html/HtmlTask.groovy
Normal 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()
|
||||
}
|
@ -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
|
||||
}
|
@ -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() })"
|
||||
}
|
||||
|
||||
}
|
@ -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
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
}
|
@ -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() })"
|
||||
}
|
||||
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
@ -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() {}
|
||||
|
||||
}
|
@ -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() })"
|
||||
}
|
||||
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
@ -2,6 +2,5 @@ package com.jessebrault.ssg.model
|
||||
|
||||
interface Model<T> {
|
||||
String getName()
|
||||
Class<T> getType()
|
||||
T get()
|
||||
}
|
||||
|
@ -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
|
||||
}
|
@ -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() {}
|
||||
|
@ -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 })"
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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)"
|
||||
}
|
||||
}
|
||||
|
33
api/src/main/groovy/com/jessebrault/ssg/mutable/Mutable.java
Normal file
33
api/src/main/groovy/com/jessebrault/ssg/mutable/Mutable.java
Normal 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();
|
||||
}
|
@ -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() {}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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)"
|
||||
}
|
||||
|
||||
}
|
@ -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()"
|
||||
}
|
||||
|
||||
}
|
@ -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 })"
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,5 +0,0 @@
|
||||
package com.jessebrault.ssg.page
|
||||
|
||||
interface PageFactory {
|
||||
Collection<Page> create()
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
)
|
||||
|
||||
}
|
@ -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'
|
||||
}
|
20
api/src/main/groovy/com/jessebrault/ssg/page/PageType.groovy
Normal file
20
api/src/main/groovy/com/jessebrault/ssg/page/PageType.groovy
Normal 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 })"
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
package com.jessebrault.ssg.page
|
||||
|
||||
final class PageTypes {
|
||||
|
||||
static final PageType GSP = new PageType(['.gsp'], new GspPageRenderer())
|
||||
|
||||
private PageTypes() {}
|
||||
|
||||
}
|
@ -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() {}
|
||||
|
||||
}
|
@ -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()"
|
||||
}
|
||||
|
||||
}
|
21
api/src/main/groovy/com/jessebrault/ssg/part/Part.groovy
Normal file
21
api/src/main/groovy/com/jessebrault/ssg/part/Part.groovy
Normal 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 })"
|
||||
}
|
||||
|
||||
}
|
@ -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
|
||||
)
|
||||
|
||||
}
|
20
api/src/main/groovy/com/jessebrault/ssg/part/PartType.groovy
Normal file
20
api/src/main/groovy/com/jessebrault/ssg/part/PartType.groovy
Normal 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 })"
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
package com.jessebrault.ssg.part
|
||||
|
||||
final class PartTypes {
|
||||
|
||||
static final PartType GSP = new PartType(['.gsp'], new GspPartRenderer())
|
||||
|
||||
private PartTypes() {}
|
||||
|
||||
}
|
@ -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
Loading…
Reference in New Issue
Block a user