Compare commits

...

128 Commits

Author SHA1 Message Date
Jesse Brault
e97381687a Add installDist step before release step.
All checks were successful
Ssg Check, Publish, and Release / ci (push) Successful in 2m50s
2025-06-30 19:47:27 -05:00
Jesse Brault
1826b9bc58 Change out release action.
All checks were successful
Ssg Check, Publish, and Release / ci (push) Successful in 2m46s
2025-06-30 15:03:35 -05:00
Jesse Brault
90a2769fb7 Remove jbArchiva plugin and switch to Gitea maven.
Some checks failed
Ssg Check, Publish, and Release / ci (push) Failing after 2m55s
2025-06-30 08:04:54 -05:00
Jesse Brault
aa44f0550d Update sourcesJar base name.
Some checks failed
Ssg Check, Publish, and Release / ci (push) Has been cancelled
2025-06-30 07:49:31 -05:00
Jesse Brault
5b53676a82 Version bump. 2025-06-30 07:48:16 -05:00
Jesse Brault
ad2b24754f Replace old GitHub workflow with Gitea workflow. 2025-06-30 07:47:38 -05:00
Jesse Brault
6ef7fb0117 Switch to new com.jessebrault.di/fp. 2025-06-30 07:38:25 -05:00
Jesse Brault
807556cd73 Update dependencies. 2025-06-01 14:43:44 -05:00
Jesse Brault
ab5b678920 Fix bug with compileUnit not seeing all ssg-gradle found classpath. 2025-05-31 23:07:06 -05:00
Jesse Brault
82a8be76b5 Work on 0.5.0-SNAPSHOT; various updates and such. 2025-05-31 20:43:40 -05:00
Jesse Brault
4757a2e9a5 Delete old test-ssg-project which was causing problems. 2025-05-31 20:08:56 -05:00
Jesse Brault
3170d2b2e9 Drop redundant 'ssg' from Maven coordinates. 2025-02-16 18:27:43 -06:00
Jesse Brault
57fa73f4c3 Set to 0.4.3-SNAPSHOT. 2025-02-16 18:25:42 -06:00
Jesse Brault
1799db2e37 Upgrade groowt and groovy dependencies. 2025-02-16 18:13:41 -06:00
Jesse Brault
753f3b67b1 Updated to Gradle 8.9 and jbarchiva 0.2.2. 2024-07-17 14:56:50 -05:00
JesseBrault0709
894bd55033 Added a todo. 2024-06-14 15:18:16 +02:00
JesseBrault0709
8355ec548b Added a todo. 2024-06-14 12:52:02 +02:00
JesseBrault0709
dfc9324a23 TODO and a bit of clean up. 2024-06-14 12:48:58 +02:00
JesseBrault0709
779cd26753 Updated test project. 2024-06-14 12:45:42 +02:00
JesseBrault0709
ae625d1229 Big update to TODO. 2024-06-14 12:40:44 +02:00
JesseBrault0709
0d7544f202 Move to v0.4.2.
Some checks failed
StaticSiteGenerator Release / release (push) Has been cancelled
2024-06-13 22:13:36 +02:00
JesseBrault0709
57dd965153 Move to v0.4.1.
Some checks failed
StaticSiteGenerator Release / release (push) Has been cancelled
2024-06-13 22:05:11 +02:00
JesseBrault0709
4930034ae2 Fixed failing test with case-insensitive enum values. 2024-06-13 22:03:46 +02:00
JesseBrault0709
640dc1f856 Moved to Groowt 0.1.2. 2024-06-13 22:01:02 +02:00
JesseBrault0709
022b4a018f Added a todo for 0.4.1. 2024-06-12 19:11:11 +02:00
JesseBrault0709
a233395cc7 Changed groovy enum to java enum for profiling.
Some checks failed
StaticSiteGenerator Release / release (push) Has been cancelled
2024-06-08 10:37:51 +02:00
JesseBrault0709
760b734f21 Rename .groovy to .java 2024-06-08 10:37:40 +02:00
JesseBrault0709
c384bf15e9 Paths ending with '/' generate index pages. 2024-06-06 07:54:54 +02:00
JesseBrault0709
b4906b1066 Refactoring gradle plugin. 2024-06-03 21:59:30 +02:00
JesseBrault0709
78bec0dd53 Removed commented-out old code. 2024-06-03 08:39:18 +02:00
JesseBrault0709
599fb919c7 Refactoring of DefaultStaticSiteGenerator and related bugs. 2024-06-02 13:43:38 +02:00
JesseBrault0709
8ae16e327f Removed manually adding std component lib items. 2024-05-31 16:51:33 +02:00
JesseBrault0709
150a2d71cb Fixed BuildDelegate globals. 2024-05-31 16:51:22 +02:00
JesseBrault0709
1ae3ef43bb Started adding standard lib to root scope. 2024-05-31 10:20:23 +02:00
JesseBrault0709
f9f5bf5889 Skip template annotation. 2024-05-31 10:17:57 +02:00
JesseBrault0709
cfafaf0df9 Bug fix with wildcard. 2024-05-31 10:17:49 +02:00
JesseBrault0709
8967338fae Got rid of todo. 2024-05-30 11:10:46 +02:00
JesseBrault0709
52145cf013 Fixed small copy/paste bug. 2024-05-30 11:10:28 +02:00
JesseBrault0709
3c363cb71c Removed @Inject since it's not needed anymore. 2024-05-30 11:10:02 +02:00
JesseBrault0709
b5a7b1f67d Components automatically added to root scope and instantiated via objectFactory. 2024-05-30 10:43:22 +02:00
JesseBrault0709
31a6c79929 Fixes to groovy class loading mechanisms. 2024-05-30 09:36:23 +02:00
JesseBrault0709
d32ac97caf Improvements to gradle plugin, source set dependencies. 2024-05-30 07:55:09 +02:00
JesseBrault0709
d9f8cae0ea Working on better di. 2024-05-29 19:56:06 +02:00
JesseBrault0709
0998d1a11d Biography page working with Text injection and rendering. 2024-05-28 08:15:20 +02:00
JesseBrault0709
5975f5110b Better convention for outputDir. 2024-05-28 07:43:33 +02:00
JesseBrault0709
13b22b1afa Added 'default' as default build name for ssg-cli. 2024-05-27 10:53:15 +02:00
JesseBrault0709
a221980d98 OutputDir defaults to the build name. 2024-05-27 10:48:49 +02:00
JesseBrault0709
d1c8a74355 Added wvc-compiler to runtime. 2024-05-27 10:40:30 +02:00
JesseBrault0709
adc96982f2 Updated to match new package names of web-view-components. 2024-05-27 10:16:12 +02:00
JesseBrault0709
69552922c1 Updated TODO. 2024-05-17 15:11:20 +02:00
JesseBrault0709
526f01c683 Biography example working better. 2024-05-17 15:07:47 +02:00
JesseBrault0709
30e463f1cf Successfully building a dist from a simple page. 2024-05-16 17:45:59 +02:00
JesseBrault0709
e34bb38350 Creating source sets and configurations. 2024-05-16 16:04:49 +02:00
JesseBrault0709
bc1d545297 Massive clean up and api/cli/gradle work. Running successfully now in test-ssg-project. 2024-05-16 10:35:21 +02:00
JesseBrault0709
140dffefc6 Working on Build and ssg object factory. 2024-05-15 08:54:37 +02:00
JesseBrault0709
76c6280b5d Worked on buildscript and added some annotations and views. 2024-05-14 21:41:25 +02:00
JesseBrault0709
c502727243 Worked on buildscript. 2024-05-14 15:52:37 +02:00
JesseBrault0709
f11334c74b Working with groowt now from mavenLocal. 2024-05-13 13:34:19 +02:00
JesseBrault0709
02180cc522 Initialized ssg-gradle-plugin. 2024-05-13 11:00:55 +02:00
JesseBrault0709
597310f031 Elaborated on what the root project can/should do. 2024-05-13 10:56:41 +02:00
JesseBrault0709
96d5d59df7 Started brainstorming in ssg-0.4.0.md. 2024-05-13 10:49:36 +02:00
JesseBrault0709
103d10e8c4 Upgraded gradle and all dependencies. 2024-05-13 10:33:18 +02:00
JesseBrault0709
aa7194fa89 Upgrade to Gradle 8.5. 2023-12-27 19:54:17 -06:00
JesseBrault0709
5514208735 Added getDirectoryCollectionProiderChildren(). 2023-06-19 14:50:10 +02:00
JesseBrault0709
40cfacf646 Added containsType and getChildrenOfType methods to CollectionProvider. 2023-06-19 14:50:10 +02:00
JesseBrault0709
0e260a45a1 Set source and target compatibility to Java 17. 2023-06-19 14:50:10 +02:00
JesseBrault0709
0a230775b9 Little things. 2023-06-16 18:03:26 +02:00
JesseBrault0709
3a1ecfe524 Added a TextProvider with filter. 2023-06-16 15:40:01 +02:00
JesseBrault0709
c7ba01380e Added multiple inheritance for builds. 2023-06-16 14:03:09 +02:00
JesseBrault0709
f3c6f1ef3c Should be able to include builds. 2023-06-14 21:35:14 +02:00
JesseBrault0709
c18438ff6a Small bug fixes; everything working. 2023-06-14 12:59:54 +02:00
JesseBrault0709
f44f2df797 Massive amount of work: 1. html specs, 2. classloaders. 2023-06-14 11:20:53 +02:00
JesseBrault0709
b741765b24 Fixed bugs with gst; now using one GroovyScriptEngine. 2023-06-12 15:57:37 +02:00
JesseBrault0709
2208c9f4c0 Updated Slf4j, and ModelCollectionTests. 2023-06-12 09:36:01 +02:00
JesseBrault0709
956642339c Imports in gsp/gst files now working. 2023-06-12 08:35:56 +02:00
JesseBrault0709
bc28a00cfc Now using gst-lib. 2023-06-11 13:38:30 +02:00
JesseBrault0709
f5697fb99b Tweaking for better usability. 2023-06-08 20:40:20 +02:00
JesseBrault0709
9cd303d317 Introduced TaskInput and TaskOutput interfaces.
Some checks failed
StaticSiteGenerator Release / release (push) Has been cancelled
2023-05-15 18:31:46 +02:00
JesseBrault0709
fa58875f88 Fixed help messages. 2023-05-15 10:15:22 +02:00
JesseBrault0709
89f4d37b10 Version fix. 2023-05-15 09:58:22 +02:00
JesseBrault0709
cf29843371 Small release.yml fix. 2023-05-15 09:39:23 +02:00
JesseBrault0709
ab0b09fb62 OutputDirFunction merging working correctly. All api/cli tests passing. 2023-05-15 07:55:07 +02:00
JesseBrault0709
5eaa32f536 TaskFactories result is now included in BuildSpecUtil.toBuild. All tests passing. 2023-05-14 22:07:29 +02:00
JesseBrault0709
594ed8ba7d Deleted BuildScriptRunner and its implementation. 2023-05-14 16:17:23 +02:00
JesseBrault0709
d73f2ba521 Updated to DefaultBuildScriptConfiguratorFactory and related. 2023-05-14 15:44:36 +02:00
JesseBrault0709
cdda27ea3a Beginning to move logic from Runner to util methods in BuildScripts. 2023-05-14 15:44:07 +02:00
JesseBrault0709
450e5ca428 Fixed small issue with AbstractProvider and EmptyProvider. 2023-05-13 13:55:49 +02:00
JesseBrault0709
369f9da53d Merge branch 'buildscript_dsl' into v0.2.0
# Conflicts:
#	api/src/main/groovy/com/jessebrault/ssg/provider/Provider.groovy
2023-05-13 13:53:21 +02:00
JesseBrault0709
1f8bd3270c Major work on buildscript dsl; new abstract build concept. N.B.: task factories not merged yet. 2023-05-13 13:51:43 +02:00
JesseBrault0709
c74d80a69e Experimenting with Monoid, etc. 2023-05-05 11:31:29 +02:00
JesseBrault0709
4fa2ba0ac9 Some Property/etc. work. 2023-05-03 16:56:45 +02:00
JesseBrault0709
2a4d3d8025 Created Property class and related work. 2023-05-03 11:27:08 +02:00
JesseBrault0709
3b181307a1 Created Property class and related work. 2023-05-03 09:57:14 +02:00
JesseBrault0709
7204b1b694 Deleted unused TaskFactoryCollector classes. 2023-05-02 20:06:03 +02:00
JesseBrault0709
dfcc2364e3 Fixed visibility. 2023-05-02 17:57:27 +02:00
JesseBrault0709
375cb5444c Small tidy-up. 2023-05-02 17:57:07 +02:00
JesseBrault0709
4c920fb485 meatyInitAndBuild test written; added Jsoup; notion of baseDir; ResourceUtil. 2023-05-02 17:55:22 +02:00
JesseBrault0709
5a70f9c91c Lots of work refactoring and introduction of StaticSiteGenerator and implementation. 2023-05-01 20:57:35 +02:00
JesseBrault0709
7708ac66e0 BuildScriptRunner tests working; added binding and other params. 2023-04-30 20:15:01 +02:00
JesseBrault0709
2dbbe53a10 AbstractBuildCommand now uses BuildScriptRunner correctly. 2023-04-30 12:53:55 +02:00
JesseBrault0709
7ec9107165 Working on GroovyBuildScriptRunner tests. 2023-04-30 12:51:07 +02:00
JesseBrault0709
f5f5bf9f6c OutputDir.path now private. 2023-04-30 06:53:25 +02:00
JesseBrault0709
f611aa3227 OutputDir no longer accepts null constructor param. 2023-04-30 06:50:35 +02:00
JesseBrault0709
3803b26c04 BuildScriptBaseTests begun and related work. 2023-04-30 06:49:54 +02:00
JesseBrault0709
9559b53003 CollectionProviders tests; added log config to tests. 2023-04-28 10:25:57 +02:00
JesseBrault0709
ced050b793 Testing various buildscript dsl delegates; CollectionProviders now have contain/isCase methods. 2023-04-28 09:15:10 +02:00
JesseBrault0709
334fa655dd Created SiteSpecTests and slightly modified SiteSpec. 2023-04-27 16:04:21 +02:00
JesseBrault0709
bec9dcb234 Build tests and some related work. 2023-04-27 15:57:51 +02:00
JesseBrault0709
3d74a79088 Worked on build command section. 2023-04-27 14:06:05 +02:00
JesseBrault0709
8fd08f51f1 Updated Cli to include new options. 2023-04-27 14:05:46 +02:00
JesseBrault0709
3e276232c3 Renaming of parameter. 2023-04-27 07:22:18 +02:00
JesseBrault0709
93dac553a4 Working on manual. 2023-04-26 21:17:17 +02:00
JesseBrault0709
07228050cb Updated TODO.md. 2023-04-26 15:32:54 +02:00
JesseBrault0709
fdc2c83e99 Removed lib module. 2023-04-26 15:29:42 +02:00
JesseBrault0709
95f86629f3 Updated TODO.md. 2023-04-26 15:29:28 +02:00
JesseBrault0709
cca5b0c1d4 TaskFactorySpec is now generic and using Consumer<TaskFactory> instead of Closure<Void>. 2023-04-26 15:23:07 +02:00
JesseBrault0709
958d3ca0ff Changed onDiagnostics Closure<Void> to diagnosticsConsumer Consumer<Collection<Diagnostic>>. 2023-04-26 15:08:15 +02:00
JesseBrault0709
2af6eeddec Removed DiagnosticsConsumer. 2023-04-26 10:25:12 +02:00
JesseBrault0709
9b743c52a4 ExtensionUtilTests added tests for no extension and dot files. 2023-04-26 10:24:34 +02:00
JesseBrault0709
2ce269bbdc Changing Closures to various functional interfaces. 2023-04-26 10:22:25 +02:00
JesseBrault0709
a8c6955ca8 Map<String, Object> instead of Map<String, ?>. 2023-04-26 09:53:46 +02:00
JesseBrault0709
306b4bb8d3 buildscript classes now use Closure<?> instead of Closure<Void>. 2023-04-26 09:52:06 +02:00
JesseBrault0709
44d4baeb62 Updated TODO.md. 2023-04-25 20:58:14 +02:00
JesseBrault0709
6cae49ca14 Clean up and added default build. 2023-04-25 20:55:47 +02:00
JesseBrault0709
b1488f7434 Removed unnecessary safe-navigation operator from Build.get. 2023-04-25 20:48:59 +02:00
JesseBrault0709
accf9d6d05 Cleaned up OutputDir and OutputDirFunctions. 2023-04-25 20:46:02 +02:00
JesseBrault0709
8b5bf93822 Updated TODO.md. 2023-04-25 18:41:36 +02:00
JesseBrault0709
db5d00bdca Major work for v0.2.0; moved from lib to api; tests all working. 2023-04-25 18:32:10 +02:00
209 changed files with 3832 additions and 3795 deletions

View File

@ -0,0 +1,28 @@
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

View File

@ -1,29 +0,0 @@
name: StaticSiteGenerator Release
on:
push:
tags:
- 'v*'
jobs:
release:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Set up JDK 19
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 Normal file
View File

@ -0,0 +1,16 @@
# 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.

49
TODO.md
View File

@ -1,11 +1,51 @@
# 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.
## Next
## 0.6.0
- [ ] Plugin system for build scripts
### Add
- [ ] Add some kind of `outputs` map to dsl that can be used to retrieve various info about another output of the current build. For example:
## 0.5.0
- [ ] watch/dev mode and server
- [ ] Reorganize gradle project layout so there is less hunting around for files
## 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
## 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.
## 0.4.1
- [x] Update groowt to 0.1.2.
### v0.2.0
- [x] Investigate imports, including static, in scripts
- Does not work; must use binding
- [x] Get rid of `taskTypes` DSL, replace with static import of task types to scripts
- Done via the binding directly
- [x] Plan out `data` models DSL
- Done via `models` dsl
### v0.1.0
- [x] Add some kind of `outputs` map to dsl that can be used to retrieve various info about another output of the current build. For example:
```groovy
// while in a special page 'special.gsp' we could get the 'output' info for a text 'blog/post.md'
def post = outputs['blog/post.md']
@ -14,6 +54,3 @@ Here will be kept all of the various todos for this project, organized by releas
assert post.targetPath = 'blog/post.html'
// as well as some other information, perhaps such as the Type, extension, *etc.*
```
- [ ] Add `extensionUtil` object to dsl.
### Fix

58
api/build.gradle Normal file
View File

@ -0,0 +1,58 @@
plugins {
id 'ssg-common'
id 'groovy'
id 'java-library'
id 'java-test-fixtures'
id 'maven-publish'
}
repositories {
mavenCentral()
}
configurations {
testFixturesApi {
extendsFrom configurations.testing
}
}
dependencies {
api libs.groovy
api libs.groovy.yaml
api libs.groowt.v
api libs.groowt.vc
api libs.groowt.wvc
api libs.di
api libs.fp
compileOnlyApi libs.jetbrains.anontations
implementation libs.classgraph
implementation libs.commonmark
implementation libs.commonmark.frontmatter
implementation libs.jsoup
runtimeOnly libs.groowt.wvcc
}
java {
withSourcesJar()
}
jar {
archivesBaseName = 'ssg-api'
}
sourcesJar {
archiveBaseName = 'ssg-api'
}
publishing {
publications {
create('ssgApi', MavenPublication) {
artifactId = 'api'
from components.java
}
}
}

View File

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

View File

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

View File

@ -0,0 +1,12 @@
package com.jessebrault.ssg
import com.jessebrault.ssg.util.Diagnostic
interface StaticSiteGenerator {
Collection<Diagnostic> doBuild(
File projectDir,
String buildName,
String buildScriptFqn,
Map<String, String> buildScriptCliArgs
)
}

View File

@ -0,0 +1,76 @@
package com.jessebrault.ssg.buildscript
import com.jessebrault.ssg.buildscript.delegates.BuildDelegate
import org.jetbrains.annotations.ApiStatus
import org.jetbrains.annotations.Nullable
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.slf4j.Marker
import org.slf4j.MarkerFactory
import static java.util.Objects.requireNonNull
@SuppressWarnings('unused')
abstract class BuildScriptBase extends Script {
/* --- Logging --- */
static final Logger logger = LoggerFactory.getLogger(BuildScriptBase)
static final Marker enter = MarkerFactory.getMarker('ENTER')
static final Marker exit = MarkerFactory.getMarker('EXIT')
/* --- 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)
}
/* --- DSL --- */
void build(@Nullable String extending, @DelegatesTo(value = BuildDelegate) Closure buildClosure) {
this.extending = extending
this.buildClosure = 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
}
}

View File

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

View File

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

View File

@ -0,0 +1,48 @@
package com.jessebrault.ssg.buildscript
import com.jessebrault.ssg.model.Model
import com.jessebrault.ssg.text.TextConverter
import groovy.transform.EqualsAndHashCode
import groovy.transform.NullCheck
import com.jessebrault.di.RegistryObjectFactory
import com.jessebrault.fp.provider.Provider
import static com.jessebrault.ssg.util.ObjectUtil.requireProvider
import static com.jessebrault.ssg.util.ObjectUtil.requireString
@NullCheck(includeGenerated = true)
@EqualsAndHashCode
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
@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)
}
@Override
String toString() {
"Build(name: ${this.name}, basePackages: $basePackages, siteName: $siteName, " +
"baseUrl: $baseUrl, outputDir: $outputDir, textsDirs: $textsDirs)"
}
}

View File

@ -0,0 +1,132 @@
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 java.nio.file.Path
import java.util.function.Supplier
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()
}
}
final File projectDir
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 BuildDelegate(File projectDir) {
this.projectDir = projectDir
}
/* TODO: add friendly DSL methods for setting all properties */
void basePackage(String toAdd) {
this.basePackages.configure { it.add(toAdd) }
}
void basePackages(String... toAdd) {
toAdd.each { this.basePackage(it) }
}
void siteName(String siteName) {
this.siteName.set(siteName)
}
void siteName(Provider<String> siteNameProvider) {
this.siteName.set(siteNameProvider)
}
void baseUrl(String baseUrl) {
this.baseUrl.set(baseUrl)
}
void baseUrl(Provider<String> baseUrlProvider) {
this.baseUrl.set(baseUrlProvider)
}
void outputDir(File outputDir) {
this.outputDir.set(outputDir)
}
void outputDir(Provider<File> outputDirProvider) {
this.outputDir.set(outputDirProvider)
}
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 model(String name, Object obj) {
this.models.configure {it.add(Models.of(name, obj)) }
}
void model(Model model) {
this.models.configure { it.add(model) }
}
void model(String name, Provider tProvider) {
this.models.configure { it.add(Models.ofProvider(name, tProvider)) }
}
<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)
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,8 +1,8 @@
package com.jessebrault.ssg.url
package com.jessebrault.ssg.dsl.urlbuilder
import java.nio.file.Path
class PathBasedUrlBuilder implements UrlBuilder {
final class PathBasedUrlBuilder implements UrlBuilder {
private final String absolute
private final String baseUrl

View File

@ -1,4 +1,4 @@
package com.jessebrault.ssg.url
package com.jessebrault.ssg.dsl.urlbuilder
interface UrlBuilder {
String getAbsolute()

View File

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

View File

@ -0,0 +1,29 @@
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)
}
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)
}
private Models() {}
}

View File

@ -0,0 +1,28 @@
package com.jessebrault.ssg.model
import groovy.transform.EqualsAndHashCode
import groovy.transform.NullCheck
import groovy.transform.PackageScope
import groovy.transform.TupleConstructor
import com.jessebrault.fp.provider.Provider
@PackageScope
@TupleConstructor(includeFields = true, defaults = false)
@NullCheck(includeGenerated = true)
@EqualsAndHashCode
class ProviderModel<T> implements Model<T> {
final String name
final Class<T> type
private final Provider<T> modelProvider
@Override
T get() {
this.modelProvider.get()
}
@Override
String toString() {
"ProviderModel(name: $name, type: $type)"
}
}

View File

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

View File

@ -0,0 +1,30 @@
package com.jessebrault.ssg.model
import groovy.transform.EqualsAndHashCode
import groovy.transform.NullCheck
import groovy.transform.PackageScope
import groovy.transform.TupleConstructor
import java.util.function.Supplier
@PackageScope
@TupleConstructor(includeFields = true, defaults = false)
@NullCheck(includeGenerated = true)
@EqualsAndHashCode(includeFields = true)
final class SupplierBasedModel<T> implements Model<T> {
final String name
final Class<T> type
private final Supplier<? extends T> supplier
@Override
T get() {
this.supplier.get()
}
@Override
String toString() {
"SupplierBasedModel(name: $name, type: $type.name)"
}
}

View File

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

View File

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

View File

@ -0,0 +1,15 @@
package com.jessebrault.ssg.page
import com.jessebrault.ssg.util.Diagnostic
import com.jessebrault.ssg.view.PageView
import com.jessebrault.fp.either.Either
interface Page {
String getName()
String getPath()
String getFileExtension()
Either<Diagnostic, PageView> createView()
}

View File

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

View File

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

View File

@ -0,0 +1,132 @@
package com.jessebrault.ssg.text
import groovy.transform.EqualsAndHashCode
import groovy.transform.NullCheck
import groovy.yaml.YamlSlurper
import org.commonmark.ext.front.matter.YamlFrontMatterExtension
import org.commonmark.node.AbstractVisitor
import org.commonmark.node.Node
import org.commonmark.parser.Parser
import org.commonmark.renderer.html.HtmlRenderer
@NullCheck(includeGenerated = true)
@EqualsAndHashCode
class MarkdownText implements Text {
protected static class MarkdownExcerptVisitor extends AbstractVisitor {
private final int length
private final List<String> words
MarkdownExcerptVisitor(int length) {
this.length = length
this.words = []
}
private int getCurrentLength() {
this.words.inject(0) { acc, word -> acc + word.length() }
}
@Override
void visit(org.commonmark.node.Text text) {
if (this.currentLength < length) {
def words = text.literal.split('\\s+').toList()
def wordsIter = words.iterator()
while (wordsIter.hasNext() && this.currentLength < length) {
def word = wordsIter.next()
if (word.length() > this.length - this.currentLength) {
break
} else {
//noinspection GroovyVariableNotAssigned -- this is certainly an IntelliJ bug
this.words << word
}
}
}
}
String getResult() {
this.words.join(' ')
}
}
private static final Parser markdownParser = Parser.builder()
.extensions(List.of(YamlFrontMatterExtension.create()))
.build()
private static final HtmlRenderer htmlRenderer = HtmlRenderer.builder().build()
final String name
final String path
private final File source
private boolean didInit
private Object frontMatter
private Node parsed
MarkdownText(String name, String path, File source) {
this.name = name
this.path = path
this.source = source
}
private void initFrontMatter(String completeSourceText) {
if (completeSourceText.startsWith('---')) {
def delimiterIndex = completeSourceText.indexOf('---', 3)
def frontMatter = completeSourceText.substring(3, delimiterIndex)
this.frontMatter = new YamlSlurper().parseText(frontMatter)
} else {
this.frontMatter = [:]
}
}
private void initParsed(String completeSourceText) {
if (completeSourceText.startsWith('---')) {
def delimiterIndex = completeSourceText.indexOf('---', 3)
def body = completeSourceText.substring(delimiterIndex + 3)
if (body.startsWith('\n')) {
this.parsed = markdownParser.parse(body.substring(1))
} else {
this.parsed = markdownParser.parse(body)
}
} else {
this.parsed = markdownParser.parse(completeSourceText)
}
}
private void init() {
if (!this.didInit) {
def completeSourceText = this.source.text
this.initFrontMatter(completeSourceText)
this.initParsed(completeSourceText)
this.didInit = true
}
}
@Override
Object getFrontMatter() {
this.init()
this.frontMatter
}
@Override
String getExcerpt(int length) {
this.init()
def v = new MarkdownExcerptVisitor(length)
this.parsed.accept(v)
v.result
}
@Override
void renderTo(Writer writer) {
this.init()
htmlRenderer.render(this.parsed, writer)
}
@Override
String toString() {
"MarkdownText(name: ${this.name}, path: ${this.path})"
}
}

View File

@ -0,0 +1,24 @@
package com.jessebrault.ssg.text
import com.jessebrault.ssg.util.ExtensionUtil
import com.jessebrault.ssg.util.PathUtil
class MarkdownTextConverter implements TextConverter {
private static final Set<String> handledExtensions = ['.md']
@Override
Set<String> getHandledExtensions() {
handledExtensions
}
@Override
Text convert(File textsDir, File textFile) {
new MarkdownText(
ExtensionUtil.stripExtension(textFile.name),
'/' + PathUtil.relative(textsDir, textFile).toString(),
textFile
)
}
}

View File

@ -0,0 +1,18 @@
package com.jessebrault.ssg.text
interface Text {
String getName()
String getPath()
Object getFrontMatter()
String getExcerpt(int length)
void renderTo(Writer writer)
default String render() {
def w = new StringWriter()
this.renderTo(w)
w.toString()
}
}

View File

@ -0,0 +1,6 @@
package com.jessebrault.ssg.text
interface TextConverter {
Set<String> getHandledExtensions()
Text convert(File textsDir, File textFile)
}

View File

@ -0,0 +1,23 @@
package com.jessebrault.ssg.util
import groovy.transform.EqualsAndHashCode
import groovy.transform.TupleConstructor
import org.jetbrains.annotations.Nullable
@TupleConstructor
@EqualsAndHashCode
final class Diagnostic {
final String message
final @Nullable Exception exception
@Override
String toString() {
if (this.exception != null) {
"Diagnostic(message: $message, exception: $exception.class.name)"
} else {
"Diagnostic(message: $message)"
}
}
}

View File

@ -1,8 +1,10 @@
package com.jessebrault.ssg.util
import org.jetbrains.annotations.Nullable
import java.util.regex.Pattern
class ExtensionsUtil {
final class ExtensionUtil {
private static final Pattern stripExtensionPattern = ~/(.+)\..+$/
private static final Pattern getExtensionPattern = ~/.+(\..+)$/
@ -12,13 +14,15 @@ class ExtensionsUtil {
m.matches() ? m.group(1) : path
}
static String getExtension(String path) {
static @Nullable String getExtension(String path) {
def m = getExtensionPattern.matcher(path)
if (m.matches()) {
m.group(1)
} else {
throw new IllegalArgumentException("cannot get extension for path: ${ path }")
null
}
}
private ExtensionUtil() {}
}

View File

@ -0,0 +1,111 @@
package com.jessebrault.ssg.util
import groovy.transform.TupleConstructor
import java.util.regex.Pattern
/**
* A very basic class for handling globs. Can handle one ** at most.
* In file/directory names, only '.' is escaped; any other special characters
* may cause an invalid regex pattern.
*/
final class Glob {
private sealed interface GlobPart permits Literal, AnyDirectoryHierarchy, GlobFileOrDirectory {}
@TupleConstructor
private static final class Literal implements GlobPart {
final String literal
}
private static final class AnyDirectoryHierarchy implements GlobPart {}
@TupleConstructor
private static final class GlobFileOrDirectory implements GlobPart {
final String original
final Pattern regexPattern
}
private static List<GlobPart> toParts(String glob) {
final List<String> originalParts
if (glob.startsWith('/')) {
originalParts = glob.substring(1).split('/') as List<String>
} else {
originalParts = glob.split('/') as List<String>
}
def result = originalParts.collect {
if (it == '**') {
new AnyDirectoryHierarchy()
} else if (it.contains('*')) {
def replaced = it.replace([
'*': '.*',
'.': '\\.'
])
new GlobFileOrDirectory(it, ~replaced)
} else {
new Literal(it)
}
}
result
}
private final List<GlobPart> parts
Glob(String glob) {
this.parts = toParts(glob)
}
boolean matches(File file) {
this.matches(file.toString().replace(File.separator, '/'))
}
/**
* @param subject Must contain only '/' as a separator.
* @return whether the subject String matches this glob.
*/
boolean matches(String subject) {
final List<String> subjectParts
if (subject.startsWith('/')) {
subjectParts = subject.substring(1).split('/') as List<String>
} else {
subjectParts = subject.split('/') as List<String>
}
boolean result = true
parts:
for (def part : this.parts) {
if (part instanceof Literal) {
if (subjectParts.isEmpty()) {
result = false
break
}
def subjectPart = subjectParts.removeFirst()
if (part.literal != subjectPart) {
result = false
break
}
} else if (part instanceof AnyDirectoryHierarchy) {
while (!subjectParts.isEmpty()) {
def current = subjectParts.removeFirst()
if (subjectParts.isEmpty()) {
subjectParts.push(current)
continue parts
}
}
} else if (part instanceof GlobFileOrDirectory) {
def subjectPart = subjectParts.removeFirst()
def m = part.regexPattern.matcher(subjectPart)
if (!m.matches()) {
result = false
break parts
}
} else {
throw new IllegalStateException('Should not get here.')
}
}
result
}
}

View File

@ -0,0 +1,40 @@
package com.jessebrault.ssg.util
import com.jessebrault.fp.property.Property
import com.jessebrault.fp.provider.Provider
import static java.util.Objects.requireNonNull
final class ObjectUtil {
static <T> T requireType(Class<T> type, Object t) {
type.cast(requireNonNull(t))
}
static String requireString(s) {
requireNonNull(s) as String
}
static File requireFile(f) {
requireNonNull(f) as File
}
static Map requireMap(m) {
requireNonNull(m) as Map
}
static Set requireSet(s) {
requireNonNull(s) as Set
}
static Property requireProperty(p) {
requireNonNull(p) as Property
}
static Provider requireProvider(p) {
requireNonNull(p) as Provider
}
private ObjectUtil() {}
}

View File

@ -0,0 +1,21 @@
package com.jessebrault.ssg.util
import java.nio.file.Path
final class PathUtil {
static File relative(File base, File target) {
base.toPath().relativize(target.toPath()).toFile()
}
static String relative(String base, String target) {
Path.of(base).relativize(Path.of(target)).toString()
}
static File resolve(File base, Path target) {
base.toPath().resolve(target).toFile()
}
private PathUtil() {}
}

View File

@ -0,0 +1,9 @@
package com.jessebrault.ssg.util
final class URLUtil {
static URL ofJarFile(File jarFile) {
URI.create( "jar:file:$jarFile!/").toURL()
}
}

View File

@ -0,0 +1,14 @@
package com.jessebrault.ssg.view
import groowt.view.StandardGStringTemplateView
abstract class GStringPageView extends StandardGStringTemplateView implements PageView {
String pageTitle
String url
GStringPageView(Map<String, Object> args) {
super(args)
}
}

View File

@ -0,0 +1,11 @@
package com.jessebrault.ssg.view
import groowt.view.View
interface PageView extends View {
String getPageTitle()
void setPageTitle(String pageTitle)
String getUrl()
void setUrl(String url)
}

View File

@ -0,0 +1,10 @@
package com.jessebrault.ssg.view
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 SkipTemplate {}

View File

@ -0,0 +1,14 @@
package com.jessebrault.ssg.view
import org.jsoup.Jsoup
trait WithHtmlHelpers {
String prettyFormat(String html) {
Jsoup.parse(html).with {
outputSettings().prettyPrint(true)
it.toString()
}
}
}

View File

@ -0,0 +1,57 @@
package com.jessebrault.ssg.view
import com.jessebrault.ssg.util.Diagnostic
import groovy.transform.TupleConstructor
import com.jessebrault.fp.either.Either
import groowt.view.component.ComponentTemplate
import groowt.view.component.ViewComponent
import groowt.view.component.compiler.ComponentTemplateClassFactory
import groowt.view.component.compiler.source.ComponentTemplateSource
import groowt.view.component.web.compiler.DefaultWebViewComponentTemplateCompileUnit
@TupleConstructor
class WvcCompiler {
private static class SsgWvcTemplateCompileUnit extends DefaultWebViewComponentTemplateCompileUnit {
SsgWvcTemplateCompileUnit(
String descriptiveName,
Class<? extends ViewComponent> forClass,
ComponentTemplateSource source,
String defaultPackageName,
GroovyClassLoader groovyClassLoader
) {
super(descriptiveName, forClass, source, defaultPackageName)
this.groovyCompilationUnit.setClassLoader(groovyClassLoader)
}
}
final GroovyClassLoader groovyClassLoader
final ComponentTemplateClassFactory templateClassFactory
Either<Diagnostic, ComponentTemplate> compileTemplate(
Class<? extends ViewComponent> componentClass,
String resourceName
) {
def templateUrl = componentClass.getResource(resourceName)
if (templateUrl == null) {
return Either.left(new Diagnostic(
"Could not find templateResource: $resourceName"
))
}
def source = ComponentTemplateSource.of(templateUrl)
def compileUnit = new SsgWvcTemplateCompileUnit(
source.descriptiveName,
componentClass,
source,
componentClass.packageName,
this.groovyClassLoader
)
def compileResult = compileUnit.compile()
def templateClass = templateClassFactory.getTemplateClass(compileResult)
def componentTemplate = templateClass.getConstructor().newInstance()
return Either.right(componentTemplate)
}
}

View File

@ -0,0 +1,47 @@
package com.jessebrault.ssg.view
import groowt.view.component.AbstractViewComponent
import groowt.view.component.ComponentTemplate
import groowt.view.component.compiler.ComponentTemplateCompileUnit
import groowt.view.component.compiler.source.ComponentTemplateSource
import groowt.view.component.web.BaseWebViewComponent
import java.util.function.Function
abstract class WvcPageView extends BaseWebViewComponent implements PageView, WithHtmlHelpers {
String pageTitle
String url
WvcPageView() {}
WvcPageView(ComponentTemplate template) {
super(template)
}
WvcPageView(Class<? extends ComponentTemplate> templateClass) {
super(templateClass)
}
WvcPageView(
Function<? super Class<? extends AbstractViewComponent>, ComponentTemplateCompileUnit> compileUnitFunction
) {
super(compileUnitFunction)
}
WvcPageView(ComponentTemplateSource source) {
super(source)
}
WvcPageView(Object source) {
super(source)
}
@Override
void renderTo(Writer out) throws IOException {
def sw = new StringWriter()
super.renderTo(sw)
out.write(this.prettyFormat(sw.toString()))
}
}

View File

@ -1,6 +1,6 @@
package com.jessebrault.ssg.url
package com.jessebrault.ssg.dsl.urlbuilder
class PathBasedUrlBuilderTests extends AbstractUrlBuilderTests {
final class PathBasedUrlBuilderTests extends AbstractUrlBuilderTests {
@Override
protected UrlBuilder getUrlBuilder(String targetPath, String baseUrl) {

View File

@ -0,0 +1,58 @@
package com.jessebrault.ssg.util
import org.junit.jupiter.api.Test
import static org.junit.jupiter.api.Assertions.assertEquals
import static org.junit.jupiter.api.Assertions.assertNull
final class ExtensionUtilTests {
static class StripExtensionTests {
@Test
void simple() {
assertEquals('test', ExtensionUtil.stripExtension('test.txt'))
}
@Test
void withSlashes() {
assertEquals('test/test', ExtensionUtil.stripExtension('test/test.txt'))
}
@Test
void withMultipleExtensions() {
assertEquals('test.txt', ExtensionUtil.stripExtension('test.txt.html'))
}
}
static class GetExtensionTests {
@Test
void simple() {
assertEquals('.txt', ExtensionUtil.getExtension('test.txt'))
}
@Test
void withSlashes() {
assertEquals('.txt', ExtensionUtil.getExtension('test/test.txt'))
}
@Test
void withMultipleExtensions() {
assertEquals('.txt', ExtensionUtil.getExtension('test.test.txt'))
}
@Test
void noExtensionReturnsNull() {
assertNull(ExtensionUtil.getExtension('NO_EXTENSION'))
}
@Test
void dotFileReturnsNull() {
assertNull(ExtensionUtil.getExtension('.dot_file'))
}
}
}

View File

@ -1,4 +1,4 @@
package com.jessebrault.ssg.url
package com.jessebrault.ssg.dsl.urlbuilder
import org.junit.jupiter.api.Test
@ -6,7 +6,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals
abstract class AbstractUrlBuilderTests {
protected abstract UrlBuilder getUrlBuilder(String targetPath, String baseUrl);
protected abstract UrlBuilder getUrlBuilder(String targetPath, String baseUrl)
@Test
void upDownDown() {

View File

@ -0,0 +1,51 @@
package com.jessebrault.ssg.util
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import java.nio.file.Path
import static org.junit.jupiter.api.Assertions.assertEquals
final class FileAssertions {
private static final Logger logger = LoggerFactory.getLogger(FileAssertions)
private static Map<String, Object> fileToMap(File file) {
[
type: file.isDirectory() ? 'directory' : (file.isFile() ? 'file' : null),
text: file.file ? file.text : null
]
}
static void assertFileStructureAndContents(
File expectedBaseDir,
File actualBaseDir
) {
final Map<Path, File> expectedPathsAndFiles = [:]
def expectedBasePath = expectedBaseDir.toPath()
expectedBaseDir.eachFileRecurse {
def relativePath = expectedBasePath.relativize(it.toPath())
expectedPathsAndFiles[relativePath] = it
}
logger.debug('expectedPaths: {}', expectedPathsAndFiles.keySet())
expectedPathsAndFiles.forEach { relativePath, expectedFile ->
def actualFile = new File(actualBaseDir, relativePath.toString())
logger.debug(
'relativePath: {}, expectedFile: {}, actualFile: {}',
relativePath,
fileToMap(expectedFile),
fileToMap(actualFile)
)
assertEquals(expectedFile.directory, actualFile.directory)
assertEquals(expectedFile.file, actualFile.file)
if (expectedFile.file) {
assertEquals(expectedFile.text, actualFile.text)
}
}
}
private FileAssertions() {}
}

View File

@ -8,4 +8,10 @@ repositories {
asciidoctor {
sourceDir = 'docs/asciidoc'
}
asciidoctorj {
modules {
diagram.use()
}
}

View File

@ -1,3 +1,27 @@
plugins {
id 'groovy-gradle-plugin'
}
repositories {
maven {
url 'https://archiva.jessebrault.com/repository/internal/'
credentials {
username System.getenv('JBARCHIVA_USERNAME')
password System.getenv('JBARCHIVA_PASSWORD')
}
}
maven {
url 'https://archiva.jessebrault.com/repository/snapshots/'
credentials {
username System.getenv('JBARCHIVA_USERNAME')
password System.getenv('JBARCHIVA_PASSWORD')
}
}
}
dependencies {
implementation 'com.jessebrault.jbarchiva:jbarchiva:0.2.2'
}

View File

@ -0,0 +1,67 @@
plugins {
id 'java'
id 'maven-publish'
}
group 'com.jessebrault.ssg'
version '0.6.0-SNAPSHOT'
repositories {
mavenCentral()
maven {
name = 'Gitea'
url = uri('https://git.jessebrault.com/api/packages/jessebrault/maven')
}
}
configurations {
testing {
canBeConsumed = false
canBeResolved = false
}
testImplementation {
extendsFrom configurations.testing
}
}
dependencies {
implementation libs.slf4j.api
testing libs.slf4j.api
testing libs.junit.jupiter.api
testing libs.mockito.core
testing libs.mockito.junit.jupiter
}
java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
}
}
testing {
suites {
test {
useJUnitJupiter()
}
}
}
publishing {
repositories {
maven {
name = 'Gitea'
url = uri('https://git.jessebrault.com/api/packages/jessebrault/maven')
credentials(HttpHeaderCredentials) {
name = "Authorization"
value = "token ${System.getenv("GITEA_ACCESS_TOKEN")}"
}
authentication {
header(HttpHeaderAuthentication)
}
}
}
}

View File

@ -1,59 +0,0 @@
plugins {
id 'groovy'
id 'java-library'
id 'java-test-fixtures'
}
group 'com.jessebrault.ssg'
version '0.1.0'
repositories {
mavenCentral()
}
dependencies {
// https://mvnrepository.com/artifact/org.apache.groovy/groovy
api 'org.apache.groovy:groovy:4.0.9'
// https://mvnrepository.com/artifact/org.jetbrains/annotations
api 'org.jetbrains:annotations:24.0.0'
/**
* Logging
*/
// https://mvnrepository.com/artifact/org.slf4j/slf4j-api
implementation 'org.slf4j:slf4j-api:1.7.36'
testFixturesImplementation 'org.slf4j:slf4j-api:1.7.36'
/**
* TESTING
*/
// https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter-api
testFixturesApi 'org.junit.jupiter:junit-jupiter-api:5.9.2'
// https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter-engine
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.9.2'
/**
* Mockito
*/
// https://mvnrepository.com/artifact/org.mockito/mockito-core
testFixturesApi 'org.mockito:mockito-core:5.1.1'
// https://mvnrepository.com/artifact/org.mockito/mockito-junit-jupiter
testFixturesApi 'org.mockito:mockito-junit-jupiter:5.1.1'
/**
* Test Logging
*/
// https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-slf4j-impl
testRuntimeOnly 'org.apache.logging.log4j:log4j-slf4j-impl:2.19.0'
// https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-core
testRuntimeOnly 'org.apache.logging.log4j:log4j-core:2.19.0'
}
test {
useJUnitPlatform()
}

View File

@ -1,29 +1,24 @@
plugins {
id 'ssg.common'
id 'ssg-common'
id 'groovy'
id 'application'
id 'maven-publish'
}
repositories {
mavenCentral()
maven { url 'https://repo.gradle.org/gradle/libs-releases' }
}
dependencies {
implementation project(':lib')
implementation project(':api')
implementation project(':ssg-gradle-model')
implementation libs.picocli
implementation libs.log4j2.api
implementation libs.log4j2.core
implementation "org.gradle:gradle-tooling-api:8.14.1"
// https://mvnrepository.com/artifact/info.picocli/picocli
implementation 'info.picocli:picocli:4.7.1'
/**
* Logging
*/
// https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-api
implementation 'org.apache.logging.log4j:log4j-api:2.19.0'
// https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-slf4j-impl
runtimeOnly 'org.apache.logging.log4j:log4j-slf4j-impl:2.19.0'
// https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-core
implementation 'org.apache.logging.log4j:log4j-core:2.19.0'
runtimeOnly libs.log4j2.slf4j2.impl
}
application {
@ -31,8 +26,16 @@ application {
applicationName = 'ssg'
}
java {
withSourcesJar()
}
jar {
archivesBaseName = "ssg-cli"
archivesBaseName = 'ssg-cli'
}
sourcesJar {
archiveBaseName = 'ssg-cli'
}
distributions {
@ -40,4 +43,13 @@ distributions {
//noinspection GroovyAssignabilityCheck
distributionBaseName = 'ssg'
}
}
}
publishing {
publications {
create('ssgCli', MavenPublication) {
artifactId = 'cli'
from components.java
}
}
}

View File

@ -1,113 +1,146 @@
package com.jessebrault.ssg
import com.jessebrault.ssg.buildscript.GroovyBuildScriptRunner
import com.jessebrault.ssg.task.Output
import com.jessebrault.ssg.part.GspPartRenderer
import com.jessebrault.ssg.part.PartFilePartsProvider
import com.jessebrault.ssg.part.PartType
import com.jessebrault.ssg.specialpage.GspSpecialPageRenderer
import com.jessebrault.ssg.specialpage.SpecialPageFileSpecialPagesProvider
import com.jessebrault.ssg.specialpage.SpecialPageType
import com.jessebrault.ssg.task.TaskExecutorContext
import com.jessebrault.ssg.template.GspTemplateRenderer
import com.jessebrault.ssg.template.TemplateFileTemplatesProvider
import com.jessebrault.ssg.template.TemplateType
import com.jessebrault.ssg.text.MarkdownExcerptGetter
import com.jessebrault.ssg.text.MarkdownFrontMatterGetter
import com.jessebrault.ssg.text.MarkdownTextRenderer
import com.jessebrault.ssg.text.TextFileTextsProvider
import com.jessebrault.ssg.text.TextType
import com.jessebrault.ssg.gradle.SsgBuildModel
import com.jessebrault.ssg.util.Diagnostic
import com.jessebrault.ssg.util.URLUtil
import org.apache.logging.log4j.LogManager
import org.apache.logging.log4j.Logger
import org.gradle.tooling.GradleConnector
import picocli.CommandLine
import java.nio.file.Files
import java.nio.file.Path
abstract class AbstractBuildCommand extends AbstractSubCommand {
private static final Logger logger = LogManager.getLogger(AbstractBuildCommand)
protected final Collection<Build> builds = []
protected final StaticSiteGenerator ssg
@CommandLine.Option(
names = ['-b', '--build', '--build-name'],
description = 'The name of a build to execute. May be the name of a script (without the .groovy extension) in a build script dir, or a fully-qualified-name of a build script on the classpath or nested in a build script dir.',
arity = '1..*',
required = true,
paramLabel = 'buildName',
defaultValue = 'default'
)
Set<String> requestedBuilds
AbstractBuildCommand() {
// Configure
def markdownText = new TextType(['.md'], new MarkdownTextRenderer(), new MarkdownFrontMatterGetter(), new MarkdownExcerptGetter())
def gspTemplate = new TemplateType(['.gsp'], new GspTemplateRenderer())
def gspPart = new PartType(['.gsp'], new GspPartRenderer())
def gspSpecialPage = new SpecialPageType(['.gsp'], new GspSpecialPageRenderer())
@CommandLine.Option(
names = ['--build-script-dir'],
description = 'A directory containing build scripts, relative to the project directory.',
arity = '1..*',
required = true,
defaultValue = 'ssg',
paramLabel = 'build-script-dir'
)
Set<Path> buildScriptDirs
def defaultTextsProvider = new TextFileTextsProvider(new File('texts'), [markdownText])
def defaultTemplatesProvider = new TemplateFileTemplatesProvider(new File('templates'), [gspTemplate])
def defaultPartsProvider = new PartFilePartsProvider(new File('parts'), [gspPart])
def defaultSpecialPagesProvider = new SpecialPageFileSpecialPagesProvider(new File('specialPages'), [gspSpecialPage])
@CommandLine.Option(
names = ['-A', '--script-arg'],
description = 'Named args to pass directly to the build script.'
)
Map<String, String> scriptArgs
def defaultConfig = new Config(
textProviders: [defaultTextsProvider],
templatesProviders: [defaultTemplatesProvider],
partsProviders: [defaultPartsProvider],
specialPagesProviders: [defaultSpecialPagesProvider]
)
def defaultSiteSpec = new SiteSpec(
name: '',
baseUrl: ''
)
def defaultGlobals = [:]
@CommandLine.Option(
names = ['-g', '--gradle'],
description = 'If set, build the associated Gradle project and use its output in the ssg build process.',
negatable = true
)
boolean gradle
// Run build script, if applicable
if (new File('ssgBuilds.groovy').exists()) {
logger.info('found buildScript: ssgBuilds.groovy')
def buildScriptRunner = new GroovyBuildScriptRunner()
this.builds.addAll(buildScriptRunner.runBuildScript('ssgBuilds.groovy', defaultConfig, defaultGlobals))
logger.debug('after running ssgBuilds.groovy, builds: {}', this.builds)
}
@CommandLine.Option(
names = ['--gradle-project-dir'],
description = 'The Gradle project directory for the project containing Pages, Components, Models, etc., relative to the project directory.',
defaultValue = '.'
)
Path gradleProjectDir
if (this.builds.empty) {
// Add default build
builds << new Build(
'default',
defaultConfig,
defaultSiteSpec,
defaultGlobals,
new File('build')
)
}
@CommandLine.Option(
names = ['-l', '--lib-dir'],
description = 'A lib dir where jars containing Pages, Components, Models, etc., can be found, relative to the project directory.',
defaultValue = 'lib'
)
Set<Path> libDirs
// Get ssg object
this.ssg = new SimpleStaticSiteGenerator()
}
@CommandLine.Option(
names = ['--dry-run'],
description = 'Do a dry run of the build; do not actually output anything.'
)
boolean dryRun
protected final Integer doBuild() {
logger.traceEntry('builds: {}, ssg: {}', this.builds, this.ssg)
protected StaticSiteGenerator staticSiteGenerator = null
def hadDiagnostics = false
// Do each build
this.builds.each {
def result = this.ssg.generate(it)
if (result.hasDiagnostics()) {
hadDiagnostics = true
result.diagnostics.each {
logger.error(it.message)
protected final Integer doSingleBuild(String buildName) {
logger.traceEntry('buildName: {}', buildName)
if (this.staticSiteGenerator == null) {
def groovyClassLoader = new GroovyClassLoader(this.class.classLoader)
if (this.gradle) {
def projectConnection = GradleConnector.newConnector()
.forProjectDirectory(
this.commonCliOptions.projectDir.toPath().resolve(this.gradleProjectDir).toFile()
).connect()
def ssgGradleModel = projectConnection.getModel(SsgBuildModel)
ssgGradleModel.buildOutputLibs.each { outputLib ->
if (outputLib.name.endsWith('.jar')) {
groovyClassLoader.addURL(URLUtil.ofJarFile(outputLib))
}
}
} else {
def tasks = result.get()
Collection<Diagnostic> executionDiagnostics = []
def context = new TaskExecutorContext(
it,
tasks,
this.ssg.taskTypes,
{ Collection<Diagnostic> diagnostics ->
executionDiagnostics.addAll(diagnostics)
ssgGradleModel.runtimeClasspath.each { classpathElement ->
if (classpathElement.name.endsWith('.jar')) {
groovyClassLoader.addURL(URLUtil.ofJarFile(classpathElement))
}
}
projectConnection.newBuild().forTasks('ssgJars').run()
projectConnection.close()
}
this.libDirs.each { libDir ->
def resolved = this.commonCliOptions.projectDir.toPath().resolve(libDir)
if (Files.exists(resolved)) {
Files.walk(resolved).each {
def asFile = it.toFile()
if (asFile.isFile() && asFile.name.endsWith('.jar')) {
groovyClassLoader.addURL(URLUtil.ofJarFile(asFile))
}
)
result.get().each { it.execute(context) }
if (!executionDiagnostics.isEmpty()) {
hadDiagnostics = true
executionDiagnostics.each {
logger.error(it.message)
}
}
}
def buildScriptDirUrls = this.buildScriptDirs.collect {
def withProjectDir = new File(this.commonCliOptions.projectDir, it.toString())
withProjectDir.toURI().toURL()
} as URL[]
this.staticSiteGenerator = new DefaultStaticSiteGenerator(
groovyClassLoader,
buildScriptDirUrls,
this.dryRun
)
}
logger.traceExit(hadDiagnostics ? 1 : 0)
final Collection<Diagnostic> diagnostics = this.staticSiteGenerator.doBuild(
this.commonCliOptions.projectDir,
buildName,
buildName,
this.scriptArgs ?: [:]
)
if (!diagnostics.isEmpty()) {
diagnostics.each {
logger.error(it.message)
if (it.exception != null) {
it.exception.printStackTrace()
}
}
logger.traceExit(1)
} else {
logger.traceExit(0)
}
}
}

View File

@ -12,10 +12,10 @@ abstract class AbstractSubCommand implements Callable<Integer> {
private static final Logger logger = LogManager.getLogger(AbstractSubCommand)
@CommandLine.ParentCommand
StaticSiteGeneratorCli cli
@CommandLine.Mixin
CommonCliOptions commonCliOptions
abstract Integer doSubCommand()
protected abstract Integer doSubCommand()
@Override
Integer call() {
@ -26,14 +26,15 @@ abstract class AbstractSubCommand implements Callable<Integer> {
def configuration = context.getConfiguration()
def rootLoggerConfig = configuration.getRootLogger()
if (this.cli.logLevel?.info) {
def logLevel = this.commonCliOptions.logLevel
if (logLevel == LogLevel.INFO) {
rootLoggerConfig.level = Level.INFO
} else if (this.cli.logLevel?.debug) {
} else if (logLevel == LogLevel.DEBUG) {
rootLoggerConfig.level = Level.DEBUG
} else if (this.cli.logLevel?.trace) {
} else if (logLevel == LogLevel.TRACE) {
rootLoggerConfig.level = Level.TRACE
} else {
rootLoggerConfig.level = Level.WARN
rootLoggerConfig.level = Level.INFO
}
context.updateLoggers()

View File

@ -0,0 +1,13 @@
package com.jessebrault.ssg
import picocli.CommandLine
class CommonCliOptions {
@CommandLine.Option(names = '--project-dir', defaultValue = '.', description = 'The ssg project directory.')
File projectDir
@CommandLine.Option(names = '--log-level', defaultValue = 'info', description = 'The logging level.')
LogLevel logLevel
}

View File

@ -0,0 +1,5 @@
package com.jessebrault.ssg;
public enum LogLevel {
INFO, DEBUG, TRACE
}

View File

@ -9,13 +9,20 @@ import picocli.CommandLine
mixinStandardHelpOptions = true,
description = 'Builds the project.'
)
class SsgBuild extends AbstractBuildCommand {
final class SsgBuild extends AbstractBuildCommand {
private static final Logger logger = LogManager.getLogger(SsgBuild)
@Override
Integer doSubCommand() {
this.doBuild()
protected Integer doSubCommand() {
logger.traceEntry()
def result = 0
this.requestedBuilds.each {
if (this.doSingleBuild(it) != 0) {
result = 1
}
}
logger.traceExit(result)
}
}

View File

@ -9,46 +9,72 @@ import picocli.CommandLine
mixinStandardHelpOptions = true,
description = 'Generates a blank project, optionally with some basic files.'
)
class SsgInit extends AbstractSubCommand {
final class SsgInit extends AbstractSubCommand {
private static final Logger logger = LogManager.getLogger(SsgInit)
static void copyResourceToFile(String resourceName, File target) {
SsgInit.getResource(resourceName).withReader { reader ->
target.withWriter { writer ->
reader.transferTo(writer)
}
}
}
@CommandLine.Option(names = ['-s', '--skeleton'], description = 'Include some basic files in the generated project.')
boolean withSkeletonFiles
@Override
Integer doSubCommand() {
logger.traceEntry()
new FileTreeBuilder().with {
// Generate dirs
static void init(File targetDir, boolean meaty) {
new FileTreeBuilder(targetDir).with {
dir('texts') {
if (this.withSkeletonFiles) {
file('hello.md', this.getClass().getResource('/hello.md').text)
if (meaty) {
file('hello.md').tap {
copyResourceToFile('hello.md', it)
}
}
}
dir('pages') {
if (meaty) {
file('page.gsp').tap {
copyResourceToFile('page.gsp', it)
}
}
}
dir('templates') {
if (this.withSkeletonFiles) {
file('hello.gsp', this.getClass().getResource('/hello.gsp').text)
if (meaty) {
file('hello.gsp').tap {
copyResourceToFile('hello.gsp', it)
}
}
}
dir('parts') {
if (this.withSkeletonFiles) {
file('head.gsp', this.getClass().getResource('/head.gsp').text)
}
}
dir('specialPages') {
if (this.withSkeletonFiles) {
file('specialPage.gsp', this.getClass().getResource('/specialPage.gsp').text)
if (meaty) {
file('head.gsp').tap {
copyResourceToFile('head.gsp', it)
}
}
}
// Generate ssgBuilds.groovy
if (this.withSkeletonFiles) {
file('ssgBuilds.groovy', this.getClass().getResource('/ssgBuilds.groovy').text)
if (meaty) {
file('ssgBuilds.groovy').tap {
copyResourceToFile('ssgBuildsMeaty.groovy', it)
}
} else {
file('ssgBuilds.groovy', this.getClass().getResource('/ssgBuildsBasic.groovy').text)
file('ssgBuilds.groovy').tap {
copyResourceToFile('ssgBuildsBasic.groovy', it)
}
}
}
}
private static final Logger logger = LogManager.getLogger(SsgInit)
@CommandLine.Option(names = ['-m', '--meaty'], description = 'Include some basic files in the generated project.')
boolean meaty
@CommandLine.Option(names = '--targetDir', description = 'The directory in which to generate the project')
File target = new File('.')
@Override
protected Integer doSubCommand() {
logger.traceEntry()
init(this.target, this.meaty)
logger.traceExit(0)
}
}

View File

@ -1,24 +1,16 @@
package com.jessebrault.ssg
import com.jessebrault.ssg.provider.WithWatchableDir
import groovy.io.FileType
import org.apache.logging.log4j.LogManager
import org.apache.logging.log4j.Logger
import picocli.CommandLine
import java.nio.file.FileSystems
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.StandardWatchEventKinds
import java.nio.file.WatchEvent
import java.nio.file.WatchKey
@CommandLine.Command(
hidden = true,
name = 'watch',
mixinStandardHelpOptions = true,
description = 'Run in watch mode, rebuilding the project whenever files are created/updated/deleted.'
)
class SsgWatch extends AbstractBuildCommand {
final class SsgWatch extends AbstractBuildCommand {
private static final Logger logger = LogManager.getLogger(SsgWatch)
@ -26,88 +18,88 @@ class SsgWatch extends AbstractBuildCommand {
Integer doSubCommand() {
logger.traceEntry()
// Setup watchService and watchKeys
def watchService = FileSystems.getDefault().newWatchService()
Map<WatchKey, Path> watchKeys = [:]
// Our Closure to register a directory path
def registerPath = { Path path ->
if (!Files.isDirectory(path)) {
throw new IllegalArgumentException('path must be a directory, given: ' + path)
}
logger.debug('registering dir with path: {}', path)
def watchKey = path.register(
watchService,
StandardWatchEventKinds.ENTRY_CREATE,
StandardWatchEventKinds.ENTRY_DELETE,
StandardWatchEventKinds.ENTRY_MODIFY
)
watchKeys[watchKey] = path
logger.debug('watchKeys: {}', watchKeys)
}
// Get all base watchableDirs
Collection<WithWatchableDir> watchableProviders = []
this.builds.each {
it.config.textProviders.each {
if (it instanceof WithWatchableDir) {
watchableProviders << it
}
}
it.config.templatesProviders.each {
if (it instanceof WithWatchableDir) {
watchableProviders << it
}
}
it.config.partsProviders.each {
if (it instanceof WithWatchableDir) {
watchableProviders << it
}
}
it.config.specialPagesProviders.each {
if (it instanceof WithWatchableDir) {
watchableProviders << it
}
}
}
// register them and their child directories using the Closure above
watchableProviders.each {
def baseDirFile = it.watchableDir
registerPath(baseDirFile.toPath())
baseDirFile.eachFile(FileType.DIRECTORIES) {
registerPath(it.toPath())
}
}
//noinspection GroovyInfiniteLoopStatement
while (true) {
def watchKey = watchService.take()
def path = watchKeys[watchKey]
if (path == null) {
logger.warn('unexpected watchKey: {}', watchKey)
} else {
watchKey.pollEvents().each {
assert it instanceof WatchEvent<Path>
def childName = it.context()
def childPath = path.resolve(childName)
if (it.kind() == StandardWatchEventKinds.ENTRY_CREATE && Files.isDirectory(childPath)) {
registerPath(childPath)
} else if (Files.isRegularFile(childPath)) {
logger.debug('detected {} for regularFile with path {}', it.kind(), childPath)
def t = new Thread({
this.doBuild()
})
t.setName('workerThread')
t.start()
}
}
}
def valid = watchKey.reset()
if (!valid) {
def removedPath = watchKeys.remove(watchKey)
logger.debug('removed path: {}', removedPath)
}
}
// // Setup watchService and watchKeys
// def watchService = FileSystems.getDefault().newWatchService()
// Map<WatchKey, Path> watchKeys = [:]
//
// // Our Closure to register a directory path
// def registerPath = { Path path ->
// if (!Files.isDirectory(path)) {
// throw new IllegalArgumentException('path must be a directory, given: ' + path)
// }
// logger.debug('registering dir with path: {}', path)
// def watchKey = path.register(
// watchService,
// StandardWatchEventKinds.ENTRY_CREATE,
// StandardWatchEventKinds.ENTRY_DELETE,
// StandardWatchEventKinds.ENTRY_MODIFY
// )
// watchKeys[watchKey] = path
// logger.debug('watchKeys: {}', watchKeys)
// }
//
// // Get all base watchableDirs
// Collection<WithWatchableDir> watchableProviders = []
// this.builds.each {
// it.config.textProviders.each {
// if (it instanceof WithWatchableDir) {
// watchableProviders << it
// }
// }
// it.config.templatesProviders.each {
// if (it instanceof WithWatchableDir) {
// watchableProviders << it
// }
// }
// it.config.partsProviders.each {
// if (it instanceof WithWatchableDir) {
// watchableProviders << it
// }
// }
// it.config.specialPagesProviders.each {
// if (it instanceof WithWatchableDir) {
// watchableProviders << it
// }
// }
// }
// // register them and their child directories using the Closure above
// watchableProviders.each {
// def baseDirFile = it.watchableDir
// registerPath(baseDirFile.toPath())
// baseDirFile.eachFile(FileType.DIRECTORIES) {
// registerPath(it.toPath())
// }
// }
//
// //noinspection GroovyInfiniteLoopStatement
// while (true) {
// def watchKey = watchService.take()
// def path = watchKeys[watchKey]
// if (path == null) {
// logger.warn('unexpected watchKey: {}', watchKey)
// } else {
// watchKey.pollEvents().each {
// assert it instanceof WatchEvent<Path>
// def childName = it.context()
// def childPath = path.resolve(childName)
// if (it.kind() == StandardWatchEventKinds.ENTRY_CREATE && Files.isDirectory(childPath)) {
// registerPath(childPath)
// } else if (Files.isRegularFile(childPath)) {
// logger.debug('detected {} for regularFile with path {}', it.kind(), childPath)
// def t = new Thread({
// this.doBuild()
// })
// t.setName('workerThread')
// t.start()
// }
// }
// }
// def valid = watchKey.reset()
// if (!valid) {
// def removedPath = watchKeys.remove(watchKey)
// logger.debug('removed path: {}', removedPath)
// }
// }
//noinspection GroovyUnreachableStatement
logger.traceExit(0)

View File

@ -5,30 +5,17 @@ import picocli.CommandLine
@CommandLine.Command(
name = 'ssg',
mixinStandardHelpOptions = true,
version = '0.0.1-SNAPSHOT',
description = 'Generates a set of html files from a given configuration.',
version = '0.4.0',
description = 'A static site generator which can interface with Gradle for high extensibility.',
subcommands = [SsgInit, SsgBuild, SsgWatch]
)
class StaticSiteGeneratorCli {
final class StaticSiteGeneratorCli {
static void main(String[] args) {
System.exit(new CommandLine(StaticSiteGeneratorCli).execute(args))
System.exit(new CommandLine(StaticSiteGeneratorCli).with {
caseInsensitiveEnumValuesAllowed = true
execute(args)
})
}
static class LogLevel {
@CommandLine.Option(names = ['--info'], description = 'Log at INFO level.')
boolean info
@CommandLine.Option(names = ['--debug'], description = 'Log at DEBUG level.')
boolean debug
@CommandLine.Option(names = ['--trace'], description = 'Log at TRACE level.')
boolean trace
}
@CommandLine.ArgGroup(exclusive = true, heading = 'Log Level')
LogLevel logLevel
}

View File

@ -0,0 +1,10 @@
import com.jessebrault.ssg.buildscript.BuildScriptBase
import groovy.transform.BaseScript
@BaseScript
BuildScriptBase base
build {
siteName 'My Site'
baseUrl 'https://mysite.com'
}

View File

@ -0,0 +1,10 @@
import com.jessebrault.ssg.buildscript.BuildScriptBase
import groovy.transform.BaseScript
@BaseScript
BuildScriptBase base
build {
extending 'default'
baseUrl baseUrl.map { defaultBaseUrl -> defaultBaseUrl + '/preview' }
}

View File

@ -0,0 +1,9 @@
import com.jessebrault.ssg.buildscript.BuildScriptBase
import groovy.transform.BaseScript
@BaseScript
BuildScriptBase base
build {
extending 'default'
}

View File

@ -1,10 +1,11 @@
<html>
<%
println "delegate.text: $delegate.text"
out << parts['head.gsp'].render([
title: "${ globals.siteTitle }: ${ frontMatter.title }"
title: "${ siteSpec.name }: ${ text.frontMatter.title }"
])
%>
<body>
<%= text %>
<%= text.render() %>
</body>
</html>

View File

@ -3,15 +3,17 @@
<Appenders>
<Console name="standard" target="SYSTEM_OUT">
<PatternLayout>
<MarkerPatternSelector defaultPattern="%highlight{%-5level} %logger{1}: %msg%n%ex">
<PatternMatch key="FLOW" pattern="%highlight{%-5level} %logger{1}: %markerSimpleName %msg%n%ex" />
<MarkerPatternSelector defaultPattern="%highlight{%-5level} %logger: %msg%n%ex">
<PatternMatch key="FLOW" pattern="%highlight{%-5level} %logger: %markerSimpleName %msg%n%ex" />
</MarkerPatternSelector>
</PatternLayout>
</Console>
</Appenders>
<Loggers>
<Root level="warn">
<Root level="info">
<AppenderRef ref="standard" />
</Root>
<Logger name="org.gradle" level="off" />
<Logger name="groowt" level="off" />
</Loggers>
</Configuration>
</Configuration>

View File

@ -0,0 +1,8 @@
<html>
<head>
<title>${ siteSpec.name }: Page</title>
</head>
<body>
<%= texts.find { it.path == 'hello.md' }.render() %>
</body>
</html>

View File

@ -1,8 +0,0 @@
<html>
<head>
<title>${ globals.siteTitle }: Special Page</title>
</head>
<body>
<%= texts.find { it.path == 'hello' }.render() %>
</body>
</html>

View File

@ -1,14 +0,0 @@
// This file was auto-generated by the ssg init command.
build {
name = 'My Simple Build'
outDir = new File('mySimpleBuild')
config {
// Config options here
}
globals {
siteTitle = 'My Simple Site'
}
}

View File

@ -1 +1 @@
// This file was auto-generated by the ssg init command.
// This file was auto-generated by the ssg init command.

View File

@ -0,0 +1,22 @@
// This file was auto-generated by the ssg init command.
import groovy.transform.BaseScript
import com.jessebrault.ssg.buildscript.BuildScriptBase
@BaseScript
BuildScriptBase b
abstractBuild(name: 'mySiteAll', extending: 'default') {
siteSpec {
name = 'My Site'
baseUrl = 'https://mysite.com'
}
}
build(name: 'production', extending: 'mySiteAll') { }
build(name: 'preview', extending: 'mySiteAll') {
siteSpec { base ->
baseUrl = base.baseUrl + '/preview' // https://mysite.com/preview
}
}

View File

@ -1,52 +0,0 @@
package com.jessebrault.ssg
import org.junit.jupiter.api.Disabled
import org.junit.jupiter.api.Test
import static org.junit.jupiter.api.Assertions.assertEquals
import static org.junit.jupiter.api.Assertions.assertTrue
class StaticSiteGeneratorCliIntegrationTests {
@Test
@Disabled('until we figure out how to do the base dir arg')
void defaultConfiguration() {
def partsDir = new File('parts').tap {
mkdir()
deleteOnExit()
}
def specialPagesDir = new File('specialPages').tap {
mkdir()
deleteOnExit()
}
def templatesDir = new File('templatesDir').tap {
mkdir()
deleteOnExit()
}
def textsDir = new File('textsDir').tap {
mkdir()
deleteOnExit()
}
new File(partsDir, 'part.gsp').write('<%= binding.test %>')
new File(specialPagesDir, 'specialPage.gsp').write('<%= parts.part.render([test: "Greetings!"]) %>')
new File(templatesDir, 'template.gsp').write('<%= text %>')
new File(textsDir, 'text.md').write('---\ntemplate: template.gsp\n---\n**Hello, World!**')
StaticSiteGeneratorCli.main('--trace')
def buildDir = new File('build').tap {
deleteOnExit()
}
assertTrue(buildDir.exists())
def textHtml = new File(buildDir, 'text.html')
assertTrue(textHtml.exists())
assertEquals('<p><strong>Hello, World!</strong></p>\n', textHtml.text)
def specialPage = new File(buildDir, 'specialPage.html')
assertTrue(specialPage.exists())
assertEquals('Greetings!', specialPage.text)
}
}

View File

@ -0,0 +1,32 @@
package com.jessebrault.ssg
import org.junit.jupiter.api.Test
import picocli.CommandLine
import static org.junit.jupiter.api.Assertions.assertEquals
final class StaticSiteGeneratorCliTests {
private static void cliSmokeScreen(String... args) {
assertEquals(0, new CommandLine(StaticSiteGeneratorCli).with {
caseInsensitiveEnumValuesAllowed = true
execute(args)
})
}
@Test
void helpSmokeScreen() {
cliSmokeScreen('--help')
}
@Test
void initHelpSmokeScreen() {
cliSmokeScreen('init', '--help')
}
@Test
void buildHelpSmokeScreen() {
cliSmokeScreen('build', '--help')
}
}

View File

@ -3,15 +3,16 @@
<Appenders>
<Console name="standard" target="SYSTEM_OUT">
<PatternLayout>
<MarkerPatternSelector defaultPattern="%highlight{%-5level} %logger{1}: %msg%n%ex">
<PatternMatch key="FLOW" pattern="%highlight{%-5level} %logger{1}: %markerSimpleName %msg%n%ex" />
<MarkerPatternSelector defaultPattern="%highlight{%-5level} %logger: %msg%n%ex">
<PatternMatch key="FLOW" pattern="%highlight{%-5level} %logger: %markerSimpleName %msg%n%ex" />
</MarkerPatternSelector>
</PatternLayout>
</Console>
</Appenders>
<Loggers>
<Root level="trace">
<Root level="info">
<AppenderRef ref="standard" />
</Root>
<Logger name="org.gradle" level="off" />
</Loggers>
</Configuration>
</Configuration>

View File

@ -1,11 +1,110 @@
= com.jessebrault.ssg
Jesse Brault
v0.1.0
v0.2.0
:toc:
:source-highlighter: rouge
*com.jessebrault.ssg* is a static site generator written in Groovy, giving access to the entire JVM ecosystem through its templating system.
== Overview
`ssg` has two sub-commands, `init` and `build`, one of which must be chosen when running from the command line.
NOTE: Previous versions of `ssg` contained a `watch` sub-command; this will be re-introduced in future versions in a more abstracted way that will handle not only the file system changes but also database/external events as well.
=== Sub-command: `init`
`init` is a simple command which creates the expected file and folder structure for `ssg` in the current directory. The resulting directories and file (only `ssgBuilds.groovy`) are empty.
.resulting project structure after running `init`
[plantuml, width=25%, format=svg]
----
@startsalt
{
{T
+ <&folder> (project directory)
++ <&folder> pages
++ <&folder> parts
++ <&folder> templates
++ <&folder> texts
++ <&file> ssgBuilds.groovy
}
}
@endsalt
----
However, with the `--skeleton` option (short form `-s`), a simple text, page, template, and part are generated as well. Additionally, `ssgBuilds.groovy` contains some sample configuration for the site.
.resulting project structure after running `init --skeleton`
[plantuml, width=25%, format=svg]
----
@startsalt
{
{T
+ <&folder> (project directory)
++ <&folder> pages
+++ <&file> page.gsp
++ <&folder> parts
+++ <&file> head.gsp
++ <&folder> templates
+++ <&file> hello.gsp
++ <&folder> texts
+++ <&file> hello.md
++ <&file> ssgBuilds.groovy
}
}
@endsalt
----
=== Sub-command: `build`
`build` encompasses the primary functionality of `ssg`. It accepts two options:
* `-b | --build`: The name of the build to execute. This option can be specified multiple times to specify multiple builds. The default is only one build, the `default` build; if any builds are specified, `default` is ignored (unless it is specified by the user, of course).
* `-s | --script | --buildScript`: The path to the build script file to execute. This may only be specified once. The default is `ssgBuilds.groovy`.
.Examples of using `build`.
[source,shell]
----
ssg build # <1>
ssg build -b production # <2>
ssg build -b production -b preview # <3>
ssg build -s buildScript.groovy -b myBuild # <4>
----
<1> Builds the `default` build using the build script `ssgBuilds.groovy`.
<2> Builds the `production` build using the build script `ssgBuilds.groovy`.
<3> Builds both the `production` and `preview` builds using the build script `ssgBuilds.groovy`.
<4> Builds the build named `myBuild` using the build script named `buildScript.groovy`.
== The `default` Build
If `init` is used to generate the project structure (or the structure is created manually by the user), the project structure matches the expected layout for the `default` build which is automatically included in all available builds. With no further specification, it will generate HTML pages from the given Texts, Pages, Templates, and Parts into the `build` directory.
== Program Execution
When `ssg` is invoked with a build file (such as `ssgBuilds.groovy` in the working directory), the following will occur:
. The build script is evaluated and executed, producing a collection of `Build` domain objects.
. For each `Build` object:
.. TaskFactories are configured using the configuration closures in the build file.
.. TaskFactories produce Tasks, each containing all the information needed to complete the task, except for a `TaskCollection` containing all tasks.
.. Tasks are given a `TaskCollection` and then run in parallel.
== The Build Script
The build file is evaluated as a script whose base class is `BuildScriptBase`. The script instance fields are mutated by its execution, and it (the script) is run exactly once. Each call to `build` in the script produces a new `Build` domain object which is saved by the script. The `Build` object contains all necessary data for executing that particular build:
* `name`: the name of the build, in order to invoke it from the command line.
* `outDir`: the destination directory of the build, such as `build`.
* `siteSpec`: a domain object containing the following properties which are available in all templated documents processed by the build:
** `siteTitle`: a string.
** `baseUrl`: a string, denoting the base url of the whole site, such as `http://example.com`, used in building absolute urls in the various templated documents.
* `globals`: a `Map<String, Object>` containing any user-defined globals for that build.
* `taskFactories: Closure<Void>`: a configuration block for taskFactories.
// TODO: include what the `allBuilds` block does
The `Build` object also contains all the necessary configuration clousres to configure the various instances of `TaskFactory` that are used to produce instances of `Task`.
== Some Examples
.Tag Builder

39
gradle/libs.versions.toml Normal file
View File

@ -0,0 +1,39 @@
[versions]
classgraph = '4.8.179'
commonmark = '0.24.0'
di = '0.1.0'
fp = '0.1.0'
groovy = '4.0.27'
groowt = '0.1.4'
jetbrains-annotations = '26.0.2'
jsoup = '1.20.1'
junit = '5.13.0'
log4j2 = '2.24.3'
mockito = '5.18.0'
picocli = '4.7.7'
slf4j = '2.0.17'
[libraries]
classgraph = { module = 'io.github.classgraph:classgraph', version.ref = 'classgraph' }
commonmark = { module = 'org.commonmark:commonmark', version.ref = 'commonmark' }
commonmark-frontmatter = { module = 'org.commonmark:commonmark-ext-yaml-front-matter', version.ref = 'commonmark' }
di = { module = 'com.jessebrault.di:di', version.ref = 'di' }
fp = { module = 'com.jessebrault.fp:fp', version.ref = 'fp' }
groovy = { module = 'org.apache.groovy:groovy', version.ref = 'groovy' }
groovy-yaml = { module = 'org.apache.groovy:groovy-yaml', version.ref = 'groovy' }
groowt-v = { module = 'groowt:views', version.ref = 'groowt' }
groowt-vc = { module = 'groowt:view-components', version.ref = 'groowt' }
groowt-wvc= { module = 'groowt:web-view-components', version.ref = 'groowt' }
groowt-wvcc = { module = 'groowt:web-view-components-compiler', version.ref = 'groowt' }
groowt-fp = { module = 'groowt:util-fp', version.ref = 'groowt' }
groowt-di = { module = 'groowt:util-di', version.ref = 'groowt' }
jetbrains-anontations = { module = 'org.jetbrains:annotations', version.ref = 'jetbrains-annotations' }
jsoup = { module = 'org.jsoup:jsoup', version.ref = 'jsoup' }
junit-jupiter-api = { module = 'org.junit.jupiter:junit-jupiter-api', version.ref = 'junit' }
log4j2-api = { module = 'org.apache.logging.log4j:log4j-api', version.ref = 'log4j2' }
log4j2-core = { module = 'org.apache.logging.log4j:log4j-core', version.ref = 'log4j2' }
log4j2-slf4j2-impl = { module = 'org.apache.logging.log4j:log4j-slf4j2-impl', version.ref = 'log4j2' }
mockito-core = { module = 'org.mockito:mockito-core', version.ref = 'mockito' }
mockito-junit-jupiter = { module = 'org.mockito:mockito-junit-jupiter', version.ref = 'mockito' }
picocli = { module = 'info.picocli:picocli', version.ref = 'picocli' }
slf4j-api = { module = 'org.slf4j:slf4j-api', version.ref = 'slf4j' }

Binary file not shown.

View File

@ -1,6 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.1-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

33
gradlew vendored
View File

@ -15,6 +15,8 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
@ -55,7 +57,7 @@
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
@ -83,10 +85,8 @@ done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
@ -133,10 +133,13 @@ location of your Java installation."
fi
else
JAVACMD=java
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
@ -144,7 +147,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC3045
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
@ -152,7 +155,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC3045
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
@ -197,11 +200,15 @@ if "$cygwin" || "$msys" ; then
done
fi
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded.
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \

22
gradlew.bat vendored
View File

@ -13,6 +13,8 @@
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@ -43,11 +45,11 @@ set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
@ -57,11 +59,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail

View File

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

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