Compare commits

..

No commits in common. "master" and "v0.0.1" have entirely different histories.

175 changed files with 2502 additions and 4129 deletions

View File

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

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

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

View File

@ -1,122 +0,0 @@
# Changelog
All notable changes to SSG will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## 0.1.0
### Added
- Templates, SpecialPages, and Parts all have access to two related objects: `tasks` and `taskTypes`. The first is an instance of [`TaskContainer`](lib/src/main/groovy/com/jessebrault/ssg/task/TaskContainer.groovy) and can be used to access all of the [`Task`](lib/src/main/groovy/com/jessebrault/ssg/task/Task.groovy) instances for a given build. The second is in an instance of [`TaskTypeContainer`](lib/src/main/groovy/com/jessebrault/ssg/task/TaskTypeContainer.groovy) and can be used to access the various [`TaskType`](lib/src/main/groovy/com/jessebrault/ssg/task/TaskType.groovy) instances for a given build. For example, one could use these together to obtain the output path of an html file from another task (assume one is in a `.gsp` file):
```groovy
def otherTask = tasks.findAllByType(taskTypes.textToHtmlFile).find { it.input.path == 'someText.md' }
assert otherTask.output.htmlPath == 'someText.html'
```
This is a complicated and experimental feature and may be changed frequently depending on future developments. [92c8108](https://github.com/JesseBrault0709/ssg/commit/92c8108).
- Templates, SpecialPages, and Parts all have access to a `logger` of type `org.slf4j.Logger`. [64f342a](https://github.com/JesseBrault0709/ssg/commit/64f342a).
- There is now the notion of a `siteSpec`, an object of type of [`SiteSpec`](lib/src/main/groovy/com/jessebrault/ssg/SiteSpec.groovy). It is simmilar to the `globals` object in that it contains properties that are available in all Templates, SpecialPages, and Parts. Unlike `globals`, which contains user-defined keys, `siteSpec` contains (for now) the pre-defined keys `baseUrl` and `title`. To configure the `siteSpec`, add the following block to a `build` block in `ssgBuilds.groovy`:
```groovy
siteSpec {
baseUrl = 'https://test.com' // or whatever, defaults to an empty string
title = 'My Great Website' // or whatever, defaults to an empty string
}
```
Then use it in any Template, SpecialPage, or part like so:
```gsp
<%
assert siteSpec.baseUrl == 'https://test.com' && siteSpec.title == 'My Great Website'
%>
```
[111bdea](https://github.com/JesseBrault0709/ssg/commit/111bdea), [ef9e566](https://github.com/JesseBrault0709/ssg/commit/ef9e566).
- Templates, SpecialPages, and Parts all have access to `targetPath` of type `String` representing the path of the 'output' file. For now, this is always a `.html` file.
```gsp
<%
// in a template where the source text is 'foo/bar.md'
assert targetPath == 'foo/bar.html'
// in a special page whose path is 'special.gsp'
assert targetPath == 'special.html'
// in a part with a source text of 'foo/bar/hello.md'
assert targetPath == 'foo/bar/hello.html'
// in a part with a source special page of 'foo/bar/baz/special.gsp'
assert targetParth == 'foo/bar/baz/special.html'
%>
```
[6de83df](https://github.com/JesseBrault0709/ssg/commit/6de83df), [06499a9](https://github.com/JesseBrault0709/ssg/commit/06499a9).
- Templates, SpecialPages, and Parts all have access to `sourcePath` of type `String` representing the path of the 'source' file: either a text file or a special page. In Templates, the 'source' comes from the path of the Text being rendered; in SpecialPages, this comes from the path of the SpecialPage being rendered (i.e., itself); in Parts, this comes from either the Template or SpecialPage which called (i.e., embedded) the Part.
```gsp
<%
// in a template or part when rendering a text at 'home.md'
assert sourcePath == 'home.md'
// in a template or part when rendering a text at 'posts/helloWorld.md'
assert sourcePath == 'posts/helloWorld.md'
// in a special page or part when rendering a special page at 'foo/bar/specialPage.gsp'
assert sourcePath == 'foo/bar/specialPage.gsp'
%>
```
[0371d41](https://github.com/JesseBrault0709/ssg/commit/0371d41), [9983685](https://github.com/JesseBrault0709/ssg/commit/9983685), [076bc9b](https://github.com/JesseBrault0709/ssg/commit/076bc9b), [c5ac810](https://github.com/JesseBrault0709/ssg/commit/c5ac810).
- Templates, SpecialPages, and Parts all have access to a `urlBuilder` of type [`PathBasedUrlBuilder`](lib/src/main/groovy/com/jessebrault/ssg/url/PathBasedUrlBuilder.groovy) (implementing [`UrlBuilder`](lib/src/main/groovy/com/jessebrault/ssg/url/UrlBuilder.groovy)). It can be used to obtain both absolute (using `siteSpec.baseUrl`) and relative urls (to the current `targetPath`). Use it like so:
```gsp
<%
// when targetPath == 'simple.html'
assert urlBuilder.relative('images/test.jpg') == 'images/test.jpg'
// when targetPath == 'nested/post.html'
assert urlBuilder.relative('images/test.jpg') == '../images/test.jpg'
// when baseUrl is 'https://test.com' and targetPath is 'simple.html'
assert urlBuilder.absolute == 'https://test.com/simple.html
// when baseUrl is 'https://test.com' and we want an absolute to another file
assert urlBuilder.absolute('images/test.jpg') == 'https://test.com/images/test.jpg'
%>
```
*Nota bene:* likely will break on Windows since `PathBasedUrlBuilder` currently uses Java's `Path` api and paths would be thusly rendered using Windows-style backslashes. This will be explored in the future. [0371d41](https://github.com/JesseBrault0709/ssg/commit/0371d41), [0762dc6](https://github.com/JesseBrault0709/ssg/commit/0762dc6), [60f4c14](https://github.com/JesseBrault0709/ssg/commit/60f4c14).
- Parts have access to all other parts now via `parts`, an object of type [`EmbeddablePartsMap`](lib/src/main/groovy/com/jessebrault/ssg/part/EmbeddablePartsMap.groovy). For example:
```gsp
<%
// myPart.gsp
out << parts['otherPart.gsp'].render()
%>
```
[0e49414](https://github.com/JesseBrault0709/ssg/commit/0e49414).
- A `tagBuilder` object of type [`DynamicTagBuilder`](lib/src/main/groovy/com/jessebrault/ssg/tagbuilder/DynamicTagBuilder.groovy) (implementing [`TagBuilder`](lib/src/main/groovy/com/jessebrault/ssg/tagbuilder/TagBuilder.groovy)) is available in Templates, SpecialPages, and Parts.
```gsp
<%
def simpleTag = tagBuilder.test()
assert simpleTag == '<test />'
def tagWithBody = tagBuilder.title 'Hello, World!'
assert tagWithBody == '<title>Hello, World!</title>'
def tagWithAttributes = tagBuilder.meta name: 'og:title', content: 'Hello, World!'
assert tagWithAttributes == '<meta name="og:title" content="Hello, World!" />'
def tagWithAttributesAndBody = tagBuilder.p([id: 'my-paragraph'], 'Hello, World!')
assert tagWithAttributesAndBody == '<p id="my-paragraph">Hello, World!</p>'
%>
```
This is likely most useful for building simple, one-line html/xml tags. [93687d](https://github.com/JesseBrault0709/ssg/commit/936587d).
- Parts have a `text` object of type [`EmbeddableText`](lib/src/main/groovy/com/jessebrault/ssg/text/EmbeddableText.groovy). If one is rendering a Part called from anything other than a Template (which has an associated text), this will be `null`. [34d9cd5](https://github.com/JesseBrault0709/ssg/commit/34d9cd5).
### Breaking Changes
- The path of a file was stripped of its extension when previously referring to Texts or SpecialPages; now, the extension is present. For example:
```gsp
<%
// suppose we have a text called 'test.md' and we are in a template, special page, or part
assert texts['test'] == null
assert texts['test.md'] != null
%>
```
[0371d41](https://github.com/JesseBrault0709/ssg/commit/0371d41).
- The `text` object in Templates is now an instance of [`EmbeddableText`](lib/src/main/groovy/com/jessebrault/ssg/text/EmbeddableText.groovy) instead of `String`. Thus, one must use `text.render()` to obtain the rendered text. [34d9cd5](https://github.com/JesseBrault0709/ssg/commit/34d9cd5).
- The `frontMatter` object is no longer available. Use `text.frontMatter` instead. [eafc8cd](https://github.com/JesseBrault0709/ssg/commit/eafc8cd), [c5ac810](https://github.com/JesseBrault0709/ssg/commit/c5ac810).

View File

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

56
TODO.md
View File

@ -1,56 +0,0 @@
# TODO
Here will be kept all of the various todos for this project, organized by release.
N.b. that v0.3.0 was skipped because of such a fundamental change in the usage of the
program with the incorporation of Groowt and Web View Components.
## 0.6.0
- [ ] Plugin system for build scripts
## 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']
assert post instanceof Output // or something
assert post.path == 'blog/post.md'
assert post.targetPath = 'blog/post.html'
// as well as some other information, perhaps such as the Type, extension, *etc.*
```

View File

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

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

View File

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

View File

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

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

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

View File

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

View File

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

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

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

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

View File

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

View File

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

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

View File

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

View File

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

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

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

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

View File

@ -1,23 +0,0 @@
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,28 +0,0 @@
package com.jessebrault.ssg.util
import org.jetbrains.annotations.Nullable
import java.util.regex.Pattern
final class ExtensionUtil {
private static final Pattern stripExtensionPattern = ~/(.+)\..+$/
private static final Pattern getExtensionPattern = ~/.+(\..+)$/
static String stripExtension(String path) {
def m = stripExtensionPattern.matcher(path)
m.matches() ? m.group(1) : path
}
static @Nullable String getExtension(String path) {
def m = getExtensionPattern.matcher(path)
if (m.matches()) {
m.group(1)
} else {
null
}
}
private ExtensionUtil() {}
}

View File

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

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

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

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

View File

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

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

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

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

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

@ -1,47 +0,0 @@
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,10 +0,0 @@
package com.jessebrault.ssg.dsl.urlbuilder
final class PathBasedUrlBuilderTests extends AbstractUrlBuilderTests {
@Override
protected UrlBuilder getUrlBuilder(String targetPath, String baseUrl) {
new PathBasedUrlBuilder(targetPath, baseUrl)
}
}

View File

@ -1,58 +0,0 @@
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,49 +0,0 @@
package com.jessebrault.ssg.dsl.urlbuilder
import org.junit.jupiter.api.Test
import static org.junit.jupiter.api.Assertions.assertEquals
abstract class AbstractUrlBuilderTests {
protected abstract UrlBuilder getUrlBuilder(String targetPath, String baseUrl)
@Test
void upDownDown() {
def builder = this.getUrlBuilder('posts/post.html', '')
assertEquals('../images/test.jpg', builder.relative('images/test.jpg'))
}
@Test
void downDown() {
assertEquals(
'images/test.jpg',
this.getUrlBuilder('test.html', '').relative('images/test.jpg')
)
}
@Test
void upUpDownDown() {
assertEquals(
'../../images/test.jpg',
this.getUrlBuilder('posts/old/test.html', '').relative('images/test.jpg')
)
}
@Test
void absoluteMatchesTargetPath() {
assertEquals(
'https://test.com/test/test.html',
this.getUrlBuilder('test/test.html', 'https://test.com').absolute
)
}
@Test
void absoluteToCorrect() {
assertEquals(
'https://test.com/images/test.jpg',
this.getUrlBuilder('', 'https://test.com').absolute('images/test.jpg')
)
}
}

View File

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

@ -1,17 +1,4 @@
plugins {
id 'org.asciidoctor.jvm.convert' version '3.3.2'
}
repositories {
mavenCentral()
}
asciidoctor {
sourceDir = 'docs/asciidoc'
}
asciidoctorj {
modules {
diagram.use()
}
subprojects {
group = 'com.jessebrault.ssg'
version = '0.0.1'
}

View File

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

@ -1,67 +0,0 @@
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,24 +1,41 @@
plugins {
id 'ssg-common'
id 'groovy'
id 'application'
id 'maven-publish'
}
repositories {
mavenCentral()
maven { url 'https://repo.gradle.org/gradle/libs-releases' }
}
dependencies {
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"
implementation project(':lib')
runtimeOnly libs.log4j2.slf4j2.impl
// https://mvnrepository.com/artifact/org.apache.groovy/groovy
implementation 'org.apache.groovy:groovy:4.0.7'
// https://mvnrepository.com/artifact/info.picocli/picocli
implementation 'info.picocli:picocli:4.7.0'
/**
* 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'
/**
* TESTING
*/
// https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter-api
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.2'
// https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter-engine
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.2'
}
application {
@ -26,30 +43,16 @@ application {
applicationName = 'ssg'
}
java {
withSourcesJar()
}
jar {
archivesBaseName = 'ssg-cli'
}
sourcesJar {
archiveBaseName = 'ssg-cli'
archivesBaseName = "ssg-cli"
}
distributions {
main {
//noinspection GroovyAssignabilityCheck
distributionBaseName = 'ssg'
}
}
publishing {
publications {
create('ssgCli', MavenPublication) {
artifactId = 'cli'
from components.java
}
}
}
test {
useJUnitPlatform()
}

View File

@ -1,146 +1,89 @@
package com.jessebrault.ssg
import com.jessebrault.ssg.gradle.SsgBuildModel
import com.jessebrault.ssg.util.Diagnostic
import com.jessebrault.ssg.util.URLUtil
import com.jessebrault.ssg.buildscript.GroovyBuildScriptRunner
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.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 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)
@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
protected final Collection<Build> builds = []
protected final StaticSiteGenerator ssg
@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
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 = ['-A', '--script-arg'],
description = 'Named args to pass directly to the build script.'
)
Map<String, String> scriptArgs
def defaultTextsProvider = new TextFileTextsProvider([markdownText], new File('texts'))
def defaultTemplatesProvider = new TemplateFileTemplatesProvider([gspTemplate], new File('templates'))
def defaultPartsProvider = new PartFilePartsProvider([gspPart], new File('parts'))
def defaultSpecialPagesProvider = new SpecialPageFileSpecialPagesProvider([gspSpecialPage], new File('specialPages'))
@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
@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
@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
@CommandLine.Option(
names = ['--dry-run'],
description = 'Do a dry run of the build; do not actually output anything.'
)
boolean dryRun
protected StaticSiteGenerator staticSiteGenerator = null
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))
}
}
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))
}
}
}
}
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
)
}
final Collection<Diagnostic> diagnostics = this.staticSiteGenerator.doBuild(
this.commonCliOptions.projectDir,
buildName,
buildName,
this.scriptArgs ?: [:]
def defaultConfig = new Config(
textProviders: [defaultTextsProvider],
templatesProviders: [defaultTemplatesProvider],
partsProviders: [defaultPartsProvider],
specialPagesProviders: [defaultSpecialPagesProvider]
)
def defaultGlobals = [:]
if (!diagnostics.isEmpty()) {
diagnostics.each {
logger.error(it.message)
if (it.exception != null) {
it.exception.printStackTrace()
// 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)
}
if (this.builds.empty) {
// Add default build
builds << new Build('default', defaultConfig, defaultGlobals, new File('build'))
}
// Get ssg object
this.ssg = new SimpleStaticSiteGenerator()
}
protected final Integer doBuild() {
logger.traceEntry('builds: {}, ssg: {}', this.builds, this.ssg)
def hadDiagnostics = false
// Do each build
this.builds.each {
def result = this.ssg.generate(it)
if (result.v1.size() > 0) {
hadDiagnostics = true
result.v1.each {
logger.error(it.message)
}
} else {
result.v2.each { GeneratedPage generatedPage ->
def target = new File(it.outDir, generatedPage.path + '.html')
target.createParentDirectories()
target.write(generatedPage.html)
}
}
logger.traceExit(1)
} else {
logger.traceExit(0)
}
logger.traceExit(hadDiagnostics ? 1 : 0)
}
}

View File

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

View File

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

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

View File

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

View File

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

View File

@ -1,16 +1,24 @@
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.'
)
final class SsgWatch extends AbstractBuildCommand {
class SsgWatch extends AbstractBuildCommand {
private static final Logger logger = LogManager.getLogger(SsgWatch)
@ -18,88 +26,88 @@ final 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,17 +5,30 @@ import picocli.CommandLine
@CommandLine.Command(
name = 'ssg',
mixinStandardHelpOptions = true,
version = '0.4.0',
description = 'A static site generator which can interface with Gradle for high extensibility.',
version = '0.0.1-SNAPSHOT',
description = 'Generates a set of html files from a given configuration.',
subcommands = [SsgInit, SsgBuild, SsgWatch]
)
final class StaticSiteGeneratorCli {
class StaticSiteGeneratorCli {
static void main(String[] args) {
System.exit(new CommandLine(StaticSiteGeneratorCli).with {
caseInsensitiveEnumValuesAllowed = true
execute(args)
})
System.exit(new CommandLine(StaticSiteGeneratorCli).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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

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

@ -1,118 +0,0 @@
= com.jessebrault.ssg
Jesse Brault
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
[source,groovy]
----
def a = tagBuilder.a(href: 'hello.html', 'Hello!') // <1>
assert a == '<a href="hello.html">Hello!</a>'
out << a // <2>
----
<1> Create an <a> tag.
<2> Output the tag in the current script block.

View File

@ -1,39 +0,0 @@
[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,7 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.1-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

33
gradlew vendored
View File

@ -15,8 +15,6 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
@ -57,7 +55,7 @@
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
@ -85,8 +83,10 @@ done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# 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
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"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
@ -133,13 +133,10 @@ location of your Java installation."
fi
else
JAVACMD=java
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.
which java >/dev/null 2>&1 || 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.
@ -147,7 +144,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=SC2039,SC3045
# shellcheck disable=SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
@ -155,7 +152,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=SC2039,SC3045
# shellcheck disable=SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
@ -200,15 +197,11 @@ if "$cygwin" || "$msys" ; then
done
fi
# 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.
# 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.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \

22
gradlew.bat vendored
View File

@ -13,8 +13,6 @@
@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 ##########################################################################
@ -45,11 +43,11 @@ set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
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
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.
goto fail
@ -59,11 +57,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
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
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.
goto fail

62
lib/build.gradle Normal file
View File

@ -0,0 +1,62 @@
plugins {
id 'groovy'
}
repositories {
mavenCentral()
}
dependencies {
// https://mvnrepository.com/artifact/org.apache.groovy/groovy
implementation 'org.apache.groovy:groovy:4.0.7'
// https://mvnrepository.com/artifact/org.apache.groovy/groovy-templates
implementation 'org.apache.groovy:groovy-templates:4.0.7'
// 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'
/**
* Logging
*/
// https://mvnrepository.com/artifact/org.slf4j/slf4j-api
implementation 'org.slf4j:slf4j-api:1.7.36'
/**
* TESTING
*/
// https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter-api
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.2'
// https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter-engine
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.2'
/**
* Mockito
*/
// https://mvnrepository.com/artifact/org.mockito/mockito-core
testImplementation 'org.mockito:mockito-core:4.11.0'
// https://mvnrepository.com/artifact/org.mockito/mockito-junit-jupiter
testImplementation 'org.mockito:mockito-junit-jupiter:4.11.0'
/**
* 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'
}
jar {
archivesBaseName = 'ssg-lib'
}
test {
useJUnitPlatform()
}

View File

@ -0,0 +1,22 @@
package com.jessebrault.ssg
import groovy.transform.EqualsAndHashCode
import groovy.transform.NullCheck
import groovy.transform.TupleConstructor
@TupleConstructor(defaults = false)
@NullCheck(includeGenerated = true)
@EqualsAndHashCode
class Build {
String name
Config config
Map globals
File outDir
@Override
String toString() {
"Build(name: ${ this.name }, config: ${ this.config }, globals: ${ this.globals }, outDir: ${ this.outDir })"
}
}

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