Merge pull request #2 from JesseBrault0709/next

v0.0.2
This commit is contained in:
JesseBrault0709 2023-03-03 15:09:11 -05:00 committed by GitHub
commit f05ee20408
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
86 changed files with 2213 additions and 629 deletions

122
CHANGELOG.md Normal file
View File

@ -0,0 +1,122 @@
# 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).
## Next
### 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).

19
TODO.md Normal file
View File

@ -0,0 +1,19 @@
# TODO
Here will be kept all of the various todos for this project, organized by release.
## Next
### Add
- [ ] Add some kind of `outputs` map to dsl that can be used to retrieve various info about another output of the current build. For example:
```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.*
```
- [ ] Add `extensionUtil` object to dsl.
### Fix

11
build.gradle Normal file
View File

@ -0,0 +1,11 @@
plugins {
id 'org.asciidoctor.jvm.convert' version '3.3.2'
}
repositories {
mavenCentral()
}
asciidoctor {
sourceDir = 'docs/asciidoc'
}

View File

@ -1,6 +1,8 @@
plugins {
id 'com.jessebrault.jbarchiva'
id 'groovy'
id 'java-library'
id 'java-test-fixtures'
}
group 'com.jessebrault.ssg'
@ -12,16 +14,45 @@ repositories {
dependencies {
// https://mvnrepository.com/artifact/org.apache.groovy/groovy
implementation 'org.apache.groovy:groovy:4.0.9'
api 'org.apache.groovy:groovy:4.0.9'
// https://mvnrepository.com/artifact/org.jetbrains/annotations
api 'org.jetbrains:annotations:24.0.0'
/**
* Logging
*/
// https://mvnrepository.com/artifact/org.slf4j/slf4j-api
implementation 'org.slf4j:slf4j-api:1.7.36'
testFixturesImplementation 'org.slf4j:slf4j-api:1.7.36'
/**
* TESTING
*/
// https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter-api
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.9.2'
testFixturesApi 'org.junit.jupiter:junit-jupiter-api:5.9.2'
// https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter-engine
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.9.2'
/**
* Mockito
*/
// https://mvnrepository.com/artifact/org.mockito/mockito-core
testFixturesApi 'org.mockito:mockito-core:5.1.1'
// https://mvnrepository.com/artifact/org.mockito/mockito-junit-jupiter
testFixturesApi 'org.mockito:mockito-junit-jupiter:5.1.1'
/**
* Test Logging
*/
// https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-slf4j-impl
testRuntimeOnly 'org.apache.logging.log4j:log4j-slf4j-impl:2.19.0'
// https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-core
testRuntimeOnly 'org.apache.logging.log4j:log4j-core:2.19.0'
}
test {

View File

@ -1,33 +0,0 @@
plugins {
id 'java-library'
}
repositories {
mavenCentral()
}
dependencies {
/**
* Logging
*/
// https://mvnrepository.com/artifact/org.slf4j/slf4j-api
implementation 'org.slf4j:slf4j-api:1.7.36'
/**
* 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'
/**
* 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'
}

View File

@ -1,12 +1,14 @@
package com.jessebrault.ssg
import com.jessebrault.ssg.buildscript.GroovyBuildScriptRunner
import com.jessebrault.ssg.task.Output
import com.jessebrault.ssg.part.GspPartRenderer
import com.jessebrault.ssg.part.PartFilePartsProvider
import com.jessebrault.ssg.part.PartType
import com.jessebrault.ssg.specialpage.GspSpecialPageRenderer
import com.jessebrault.ssg.specialpage.SpecialPageFileSpecialPagesProvider
import com.jessebrault.ssg.specialpage.SpecialPageType
import com.jessebrault.ssg.task.TaskExecutorContext
import com.jessebrault.ssg.template.GspTemplateRenderer
import com.jessebrault.ssg.template.TemplateFileTemplatesProvider
import com.jessebrault.ssg.template.TemplateType
@ -32,10 +34,10 @@ abstract class AbstractBuildCommand extends AbstractSubCommand {
def gspPart = new PartType(['.gsp'], new GspPartRenderer())
def gspSpecialPage = new SpecialPageType(['.gsp'], new GspSpecialPageRenderer())
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'))
def defaultTextsProvider = new TextFileTextsProvider(new File('texts'), [markdownText])
def defaultTemplatesProvider = new TemplateFileTemplatesProvider(new File('templates'), [gspTemplate])
def defaultPartsProvider = new PartFilePartsProvider(new File('parts'), [gspPart])
def defaultSpecialPagesProvider = new SpecialPageFileSpecialPagesProvider(new File('specialPages'), [gspSpecialPage])
def defaultConfig = new Config(
textProviders: [defaultTextsProvider],
@ -43,6 +45,10 @@ abstract class AbstractBuildCommand extends AbstractSubCommand {
partsProviders: [defaultPartsProvider],
specialPagesProviders: [defaultSpecialPagesProvider]
)
def defaultSiteSpec = new SiteSpec(
name: '',
baseUrl: ''
)
def defaultGlobals = [:]
// Run build script, if applicable
@ -55,7 +61,13 @@ abstract class AbstractBuildCommand extends AbstractSubCommand {
if (this.builds.empty) {
// Add default build
builds << new Build('default', defaultConfig, defaultGlobals, new File('build'))
builds << new Build(
'default',
defaultConfig,
defaultSiteSpec,
defaultGlobals,
new File('build')
)
}
// Get ssg object
@ -69,16 +81,28 @@ abstract class AbstractBuildCommand extends AbstractSubCommand {
// Do each build
this.builds.each {
def result = this.ssg.generate(it)
if (result.v1.size() > 0) {
if (result.hasDiagnostics()) {
hadDiagnostics = true
result.v1.each {
result.diagnostics.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)
def tasks = result.get()
Collection<Diagnostic> executionDiagnostics = []
def context = new TaskExecutorContext(
it,
tasks,
this.ssg.taskTypes,
{ Collection<Diagnostic> diagnostics ->
executionDiagnostics.addAll(diagnostics)
}
)
result.get().each { it.execute(context) }
if (!executionDiagnostics.isEmpty()) {
hadDiagnostics = true
executionDiagnostics.each {
logger.error(it.message)
}
}
}
}

View File

@ -0,0 +1,19 @@
= com.jessebrault.ssg
Jesse Brault
v0.1.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.
== 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,6 +1,5 @@
plugins {
id 'ssg.common'
id 'ssg.lib'
}
repositories {

View File

@ -11,12 +11,14 @@ class Build {
String name
Config config
SiteSpec siteSpec
Map globals
File outDir
@Override
String toString() {
"Build(name: ${ this.name }, config: ${ this.config }, globals: ${ this.globals }, outDir: ${ this.outDir })"
"Build(name: ${ this.name }, config: ${ this.config }, siteSpec: ${ this.siteSpec }, " +
"globals: ${ this.globals }, outDir: ${ this.outDir })"
}
}

View File

@ -1,20 +0,0 @@
package com.jessebrault.ssg
import groovy.transform.EqualsAndHashCode
import groovy.transform.NullCheck
import groovy.transform.TupleConstructor
@TupleConstructor(includeFields = true, defaults = false)
@NullCheck
@EqualsAndHashCode
class GeneratedPage {
String path
String html
@Override
String toString() {
"GeneratedPage(path: ${ this.path })"
}
}

View File

@ -0,0 +1,28 @@
package com.jessebrault.ssg
import groovy.transform.EqualsAndHashCode
import groovy.transform.NullCheck
import groovy.transform.TupleConstructor
@TupleConstructor(defaults = false, includeFields = true)
@NullCheck(includeGenerated = true)
@EqualsAndHashCode
final class Result<T> {
final Collection<Diagnostic> diagnostics
private final T t
boolean hasDiagnostics() {
!this.diagnostics.isEmpty()
}
T get() {
this.t
}
@Override
String toString() {
"Result(diagnostics: ${ this.diagnostics }, t: ${ this.t })"
}
}

View File

@ -1,122 +1,48 @@
package com.jessebrault.ssg
import com.jessebrault.ssg.text.FrontMatter
import groovy.transform.EqualsAndHashCode
import com.jessebrault.ssg.task.SpecialPageToHtmlFileTaskFactory
import com.jessebrault.ssg.task.TaskContainer
import com.jessebrault.ssg.task.TaskTypeContainer
import com.jessebrault.ssg.task.TextToHtmlFileTaskFactory
import groovy.transform.NullCheck
import groovy.transform.TupleConstructor
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.slf4j.Marker
import org.slf4j.MarkerFactory
@TupleConstructor(includeFields = true)
@NullCheck
@EqualsAndHashCode(includeFields = true)
class SimpleStaticSiteGenerator implements StaticSiteGenerator {
private static final Logger logger = LoggerFactory.getLogger(SimpleStaticSiteGenerator)
private static final Marker enter = MarkerFactory.getMarker('ENTER')
private static final Marker exit = MarkerFactory.getMarker('EXIT')
private static final TextToHtmlFileTaskFactory textHtmlFactory = new TextToHtmlFileTaskFactory()
private static final SpecialPageToHtmlFileTaskFactory specialPageHtmlFactory = new SpecialPageToHtmlFileTaskFactory()
@Override
Tuple2<Collection<Diagnostic>, Collection<GeneratedPage>> generate(Build build) {
TaskTypeContainer getTaskTypes() {
new TaskTypeContainer([textHtmlFactory.taskType, specialPageHtmlFactory.taskType])
}
@Override
Result<TaskContainer> generate(Build build) {
logger.trace(enter, 'build: {}', build)
logger.info('processing build with name: {}', build.name)
def config = build.config
def tasks = new TaskContainer()
def diagnostics = []
// Get all texts, templates, parts, and specialPages
def texts = config.textProviders.collectMany { it.provide() }
def templates = config.templatesProviders.collectMany { it.provide() }
def parts = config.partsProviders.collectMany { it.provide() }
def specialPages = config.specialPagesProviders.collectMany { it.provide() }
def textsResult = textHtmlFactory.getTasks(build)
tasks.addAll(textsResult.get())
diagnostics.addAll(textsResult.diagnostics)
logger.debug('\n\ttexts: {}\n\ttemplates: {}\n\tparts: {}\n\tspecialPages: {}', texts, templates, parts, specialPages)
def specialPagesResult = specialPageHtmlFactory.getTasks(build)
tasks.addAll(specialPagesResult.get())
diagnostics.addAll(specialPagesResult.diagnostics)
def globals = build.globals
Collection<Diagnostic> diagnostics = []
Collection<GeneratedPage> generatedPages = []
// Generate pages from each text, but only those that have a 'template' frontMatter field with a valid value
texts.each {
logger.trace(enter, 'text: {}', it)
logger.info('processing text: {}', it.path)
// Extract frontMatter from text
def frontMatterResult = it.type.frontMatterGetter.get(it)
FrontMatter frontMatter
if (frontMatterResult.v1.size() > 0) {
logger.debug('diagnostics for getting frontMatter for {}: {}', it.path, frontMatterResult.v1)
diagnostics.addAll(frontMatterResult.v1)
logger.trace(exit, '')
return
} else {
frontMatter = frontMatterResult.v2
logger.debug('frontMatter: {}', frontMatter)
}
// Find the appropriate template from the frontMatter
def desiredTemplate = frontMatter.find('template')
if (desiredTemplate.isEmpty()) {
logger.info('{} has no \'template\' key in its frontMatter; skipping generation', it)
return
}
def template = templates.find { it.path == desiredTemplate.get() }
if (template == null) {
diagnostics << new Diagnostic('in textFile' + it.path + ' frontMatter.template references an unknown template: ' + desiredTemplate, null)
logger.trace(exit, '')
return
}
logger.debug('found template: {}', template)
// Render the text (i.e., transform text to html)
def textRenderResult = it.type.renderer.render(it, globals)
String renderedText
if (textRenderResult.v1.size() > 0) {
logger.debug('diagnostics for rendering {}: {}', it.path, textRenderResult.v1)
diagnostics.addAll(textRenderResult.v1)
logger.trace(exit, '')
return
} else {
renderedText = textRenderResult.v2
logger.debug('renderedText: {}', renderedText)
}
// Render the template using the result of rendering the text earlier
def templateRenderResult = template.type.renderer.render(template, frontMatter, renderedText, parts, globals)
String renderedTemplate
if (templateRenderResult.v1.size() > 0) {
diagnostics.addAll(templateRenderResult.v1)
logger.trace(exit, '')
return
} else {
renderedTemplate = templateRenderResult.v2
}
// Create a GeneratedPage
generatedPages << new GeneratedPage(it.path, renderedTemplate)
}
// Generate special pages
specialPages.each {
logger.info('processing specialPage: {}', it.path)
def specialPageRenderResult = it.type.renderer.render(it, texts, parts, globals)
String renderedSpecialPage
if (specialPageRenderResult.v1.size() > 0) {
diagnostics.addAll(specialPageRenderResult.v1)
logger.trace(exit, '')
return
} else {
renderedSpecialPage = specialPageRenderResult.v2
}
// Create a GeneratedPage
generatedPages << new GeneratedPage(it.path, renderedSpecialPage)
}
logger.trace(exit, '\n\tdiagnostics: {}\n\tgeneratedPages: {}', diagnostics, generatedPages)
new Tuple2<>(diagnostics, generatedPages)
logger.trace(exit, '\n\tdiagnostics: {}\n\ttasks: {}', diagnostics, tasks)
new Result<>(diagnostics, tasks)
}
@Override

View File

@ -0,0 +1,27 @@
package com.jessebrault.ssg
import groovy.transform.EqualsAndHashCode
import groovy.transform.MapConstructor
import groovy.transform.NullCheck
import groovy.transform.TupleConstructor
@TupleConstructor(force = true, defaults = false)
@MapConstructor
@NullCheck
@EqualsAndHashCode
final class SiteSpec {
String name
String baseUrl
SiteSpec(SiteSpec source) {
this.name = source.name
this.baseUrl = source.baseUrl
}
@Override
String toString() {
"SiteSpec(${ this.name }, ${ this.baseUrl })"
}
}

View File

@ -1,5 +1,9 @@
package com.jessebrault.ssg
import com.jessebrault.ssg.task.TaskContainer
import com.jessebrault.ssg.task.TaskTypeContainer
interface StaticSiteGenerator {
Tuple2<Collection<Diagnostic>, Collection<GeneratedPage>> generate(Build build)
TaskTypeContainer getTaskTypes()
Result<TaskContainer> generate(Build build)
}

View File

@ -1,11 +1,13 @@
package com.jessebrault.ssg.buildscript
import com.jessebrault.ssg.Config
import com.jessebrault.ssg.SiteSpec
class BuildClosureDelegate {
String name
Config config
SiteSpec siteSpec
Map globals
File outDir
@ -18,6 +20,15 @@ class BuildClosureDelegate {
configClosure.run()
}
void siteSpec(
@DelegatesTo(value = SiteSpecClosureDelegate, strategy = Closure.DELEGATE_FIRST)
Closure siteSpecClosure
) {
siteSpecClosure.setDelegate(new SiteSpecClosureDelegate(this.siteSpec))
siteSpecClosure.setResolveStrategy(Closure.DELEGATE_FIRST)
siteSpecClosure.run()
}
void globals(
@DelegatesTo(value = GlobalsClosureDelegate, strategy = Closure.DELEGATE_FIRST)
Closure globalsClosure

View File

@ -2,10 +2,12 @@ package com.jessebrault.ssg.buildscript
import com.jessebrault.ssg.Build
import com.jessebrault.ssg.Config
import com.jessebrault.ssg.SiteSpec
abstract class BuildScriptBase extends Script {
Config defaultConfig
SiteSpec defaultSiteSpec
Map defaultGlobals
Collection<Build> builds = []
@ -20,13 +22,20 @@ abstract class BuildScriptBase extends Script {
// Default values for Build properties
name = 'build' + this.currentBuildNumber
config = new Config(defaultConfig)
siteSpec = new SiteSpec(defaultSiteSpec)
globals = new LinkedHashMap(defaultGlobals)
outDir = new File(name)
}
buildClosure.setDelegate(buildClosureDelegate)
buildClosure.setResolveStrategy(Closure.DELEGATE_FIRST)
buildClosure.run()
this.builds << new Build(buildClosureDelegate.name, buildClosureDelegate.config, buildClosureDelegate.globals, buildClosureDelegate.outDir)
this.builds << new Build(
buildClosureDelegate.name,
buildClosureDelegate.config,
buildClosureDelegate.siteSpec,
buildClosureDelegate.globals,
buildClosureDelegate.outDir
)
this.currentBuildNumber++
}

View File

@ -0,0 +1,14 @@
package com.jessebrault.ssg.buildscript
import com.jessebrault.ssg.SiteSpec
class SiteSpecClosureDelegate {
@Delegate
private final SiteSpec siteSpec
SiteSpecClosureDelegate(SiteSpec siteSpec) {
this.siteSpec = siteSpec
}
}

View File

@ -0,0 +1,84 @@
package com.jessebrault.ssg.dsl
import com.jessebrault.ssg.part.EmbeddablePartsMap
import com.jessebrault.ssg.renderer.RenderContext
import com.jessebrault.ssg.tagbuilder.DynamicTagBuilder
import com.jessebrault.ssg.text.EmbeddableText
import com.jessebrault.ssg.text.EmbeddableTextsCollection
import com.jessebrault.ssg.text.Text
import com.jessebrault.ssg.url.PathBasedUrlBuilder
import groovy.transform.NullCheck
import groovy.transform.stc.ClosureParams
import groovy.transform.stc.SimpleType
import org.slf4j.LoggerFactory
final class StandardDslMap {
@NullCheck(includeGenerated = true)
static final class Builder {
private final Map custom = [:]
String loggerName = ''
Closure onDiagnostics = { }
Text text = null
void putCustom(key, value) {
this.custom.put(key, value)
}
void putAllCustom(Map m) {
this.custom.putAll(m)
}
}
static Map get(
RenderContext context,
@DelegatesTo(value = Builder, strategy = Closure.DELEGATE_FIRST)
@ClosureParams(
value = SimpleType,
options = ['com.jessebrault.ssg.dsl.StandardDslMap.Builder']
)
Closure builderClosure
) {
def b = new Builder()
builderClosure.resolveStrategy = Closure.DELEGATE_FIRST
builderClosure.delegate = b
builderClosure(b)
[:].tap {
it.globals = context.globals
it.logger = LoggerFactory.getLogger(b.loggerName)
it.parts = new EmbeddablePartsMap(
context.parts,
context,
b.onDiagnostics,
b.text
)
it.siteSpec = context.siteSpec
it.sourcePath = context.sourcePath
it.tagBuilder = new DynamicTagBuilder()
it.targetPath = context.targetPath
it.tasks = context.tasks
it.taskTypes = context.taskTypes
it.text = b.text ? new EmbeddableText(
b.text,
context.globals,
b.onDiagnostics
) : null
it.texts = new EmbeddableTextsCollection(
context.texts,
context.globals,
b.onDiagnostics
)
it.urlBuilder = new PathBasedUrlBuilder(
context.targetPath,
context.siteSpec.baseUrl
)
it.putAll(b.custom)
}
}
}

View File

@ -1,20 +1,43 @@
package com.jessebrault.ssg.part
import com.jessebrault.ssg.renderer.RenderContext
import com.jessebrault.ssg.text.Text
import groovy.transform.EqualsAndHashCode
import groovy.transform.NullCheck
import groovy.transform.TupleConstructor
import org.jetbrains.annotations.Nullable
import static java.util.Objects.requireNonNull
@TupleConstructor(includeFields = true, defaults = false)
@NullCheck
@EqualsAndHashCode(includeFields = true)
class EmbeddablePart {
private final Part part
private final Map globals
private final RenderContext context
private final Closure onDiagnostics
@Nullable
private final Text text
EmbeddablePart(
Part part,
RenderContext context,
Closure onDiagnostics,
@Nullable Text text
) {
this.part = requireNonNull(part)
this.context = requireNonNull(context)
this.onDiagnostics = requireNonNull(onDiagnostics)
this.text = text
}
String render(Map binding = [:]) {
def result = part.type.renderer.render(this.part, binding, this.globals)
def result = part.type.renderer.render(
this.part,
binding,
this.context,
this.text
)
if (result.v1.size() > 0) {
this.onDiagnostics.call(result.v1)
''
@ -25,7 +48,7 @@ class EmbeddablePart {
@Override
String toString() {
"EmbeddablePart(part: ${ this.part }, globals: ${ this.globals })"
"EmbeddablePart(part: ${ this.part })"
}
}

View File

@ -1,18 +1,29 @@
package com.jessebrault.ssg.part
import com.jessebrault.ssg.renderer.RenderContext
import com.jessebrault.ssg.text.Text
import groovy.transform.EqualsAndHashCode
import groovy.transform.NullCheck
import org.jetbrains.annotations.Nullable
import static java.util.Objects.requireNonNull
@NullCheck
@EqualsAndHashCode(includeFields = true)
class EmbeddablePartsMap {
@Delegate
private final Map<String, EmbeddablePart> partsMap = [:]
EmbeddablePartsMap(Collection<Part> parts, Map globals, Closure onDiagnostics) {
EmbeddablePartsMap(
Collection<Part> parts,
RenderContext context,
Closure onDiagnostics,
@Nullable Text text = null
) {
requireNonNull(parts)
requireNonNull(context)
requireNonNull(onDiagnostics)
parts.each {
this.put(it.path, new EmbeddablePart(it, globals, onDiagnostics))
this.put(it.path, new EmbeddablePart(it, context, onDiagnostics, text))
}
}

View File

@ -1,9 +1,14 @@
package com.jessebrault.ssg.part
import com.jessebrault.ssg.Diagnostic
import com.jessebrault.ssg.dsl.StandardDslMap
import com.jessebrault.ssg.renderer.RenderContext
import com.jessebrault.ssg.text.EmbeddableText
import com.jessebrault.ssg.text.Text
import groovy.text.GStringTemplateEngine
import groovy.text.TemplateEngine
import groovy.transform.EqualsAndHashCode
import org.jetbrains.annotations.Nullable
@EqualsAndHashCode
class GspPartRenderer implements PartRenderer {
@ -11,15 +16,35 @@ class GspPartRenderer implements PartRenderer {
private static final TemplateEngine engine = new GStringTemplateEngine()
@Override
Tuple2<Collection<Diagnostic>, String> render(Part part, Map binding, Map globals) {
Tuple2<Collection<Diagnostic>, String> render(
Part part,
Map binding,
RenderContext context,
@Nullable Text text
) {
Objects.requireNonNull(part)
Objects.requireNonNull(binding)
Objects.requireNonNull(context)
def diagnostics = []
try {
def result = engine.createTemplate(part.text).make([
binding: binding,
globals: globals
])
new Tuple2<>([], result.toString())
def dslMap = StandardDslMap.get(context) {
it.putCustom('binding', binding)
it.loggerName = "GspPart(${ part.path })"
it.onDiagnostics = diagnostics.&addAll
if (text) {
it.text = text
}
}
def result = engine.createTemplate(part.text).make(dslMap)
new Tuple2<>(diagnostics, result.toString())
} catch (Exception e) {
new Tuple2<>([new Diagnostic("An exception occurred while rendering part ${ part.path }:\n${ e }", e)], '')
new Tuple2<>(
[*diagnostics, new Diagnostic(
"An exception occurred while rendering part ${ part.path }:\n${ e }",
e
)],
''
)
}
}

View File

@ -1,53 +1,38 @@
package com.jessebrault.ssg.part
import com.jessebrault.ssg.provider.WithWatchableDir
import com.jessebrault.ssg.util.FileNameHandler
import groovy.io.FileType
import com.jessebrault.ssg.provider.AbstractFileCollectionProvider
import groovy.transform.EqualsAndHashCode
import groovy.transform.NullCheck
import org.jetbrains.annotations.Nullable
import org.slf4j.Logger
import org.slf4j.LoggerFactory
@NullCheck
@EqualsAndHashCode(includeFields = true)
class PartFilePartsProvider implements PartsProvider, WithWatchableDir {
class PartFilePartsProvider extends AbstractFileCollectionProvider<Part> implements PartsProvider {
private static final Logger logger = LoggerFactory.getLogger(PartFilePartsProvider)
private final Collection<PartType> partTypes
private final File partsDir
PartFilePartsProvider(Collection<PartType> partTypes, File partsDir) {
this.partTypes = partTypes
this.partsDir = partsDir
this.watchableDir = this.partsDir
PartFilePartsProvider(File partsDir, Collection<PartType> partTypes) {
super(partsDir)
this.partTypes = Objects.requireNonNull(partTypes)
}
private PartType getPartType(File file) {
private @Nullable PartType getPartType(String extension) {
this.partTypes.find {
it.ids.contains(new FileNameHandler(file).getExtension())
it.ids.contains(extension)
}
}
@Override
Collection<Part> provide() {
if (!partsDir.isDirectory()) {
logger.warn('partsDir {} does not exist or is not a directory; skipping and providing no Parts', this.partsDir)
[]
} else {
def parts = []
this.partsDir.eachFileRecurse(FileType.FILES) {
def type = this.getPartType(it)
if (type != null) {
def relativePath = this.partsDir.relativePath(it)
logger.debug('found part {}', relativePath)
parts << new Part(relativePath, type, it.text)
} else {
logger.warn('ignoring {} since there is no partType for it', it)
}
}
parts
protected @Nullable Part transformFileToT(File file, String relativePath, String extension) {
def partType = getPartType(extension)
if (!partType) {
logger.warn('there is no PartType for {}, ignoring', relativePath)
}
partType ? new Part(relativePath, partType, file.text) : null
}
@Override
@ -57,7 +42,7 @@ class PartFilePartsProvider implements PartsProvider, WithWatchableDir {
@Override
String toString() {
"PartFilePartsProvider(partsDir: ${ this.partsDir }, partTypes: ${ this.partTypes })"
"PartFilePartsProvider(partsDir: ${ this.dir }, partTypes: ${ this.partTypes })"
}
}

View File

@ -1,7 +1,17 @@
package com.jessebrault.ssg.part
import com.jessebrault.ssg.Diagnostic
import com.jessebrault.ssg.renderer.RenderContext
import com.jessebrault.ssg.text.Text
import org.jetbrains.annotations.Nullable
interface PartRenderer {
Tuple2<Collection<Diagnostic>, String> render(Part part, Map binding, Map globals)
Tuple2<Collection<Diagnostic>, String> render(
Part part,
Map binding,
RenderContext context,
@Nullable Text text
)
}

View File

@ -0,0 +1,40 @@
package com.jessebrault.ssg.provider
import groovy.io.FileType
import org.jetbrains.annotations.Nullable
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import static com.jessebrault.ssg.util.ExtensionsUtil.getExtension
abstract class AbstractFileCollectionProvider<T> implements Provider<Collection<T>>, WithWatchableDir {
private static final Logger logger = LoggerFactory.getLogger(AbstractFileCollectionProvider)
protected final File dir
AbstractFileCollectionProvider(File dir) {
this.dir = Objects.requireNonNull(dir)
this.watchableDir = dir
}
protected abstract @Nullable T transformFileToT(File file, String relativePath, String extension)
@Override
Collection<T> provide() {
if (!this.dir.isDirectory()) {
logger.warn('{} does not exist or is not a directory; skipping', this.dir)
[]
} else {
def ts = []
this.dir.eachFileRecurse(FileType.FILES) {
def t = transformFileToT(it, this.dir.relativePath(it), getExtension(it.path))
if (t) {
ts << t
}
}
ts
}
}
}

View File

@ -0,0 +1,26 @@
package com.jessebrault.ssg.renderer
import com.jessebrault.ssg.Config
import com.jessebrault.ssg.SiteSpec
import com.jessebrault.ssg.part.Part
import com.jessebrault.ssg.task.TaskContainer
import com.jessebrault.ssg.task.TaskTypeContainer
import com.jessebrault.ssg.text.Text
import groovy.transform.EqualsAndHashCode
import groovy.transform.NullCheck
import groovy.transform.TupleConstructor
@TupleConstructor(defaults = false)
@NullCheck(includeGenerated = true)
@EqualsAndHashCode
final class RenderContext {
final Config config
final SiteSpec siteSpec
final Map globals
final Collection<Text> texts
final Collection<Part> parts
final String sourcePath
final String targetPath
final TaskContainer tasks
final TaskTypeContainer taskTypes
}

View File

@ -1,10 +1,8 @@
package com.jessebrault.ssg.specialpage
import com.jessebrault.ssg.Diagnostic
import com.jessebrault.ssg.part.Part
import com.jessebrault.ssg.part.EmbeddablePartsMap
import com.jessebrault.ssg.text.EmbeddableTextsCollection
import com.jessebrault.ssg.text.Text
import com.jessebrault.ssg.dsl.StandardDslMap
import com.jessebrault.ssg.renderer.RenderContext
import groovy.text.GStringTemplateEngine
import groovy.text.TemplateEngine
import groovy.transform.EqualsAndHashCode
@ -17,21 +15,26 @@ class GspSpecialPageRenderer implements SpecialPageRenderer {
private static final TemplateEngine engine = new GStringTemplateEngine()
@Override
Tuple2<Collection<Diagnostic>, String> render(SpecialPage specialPage, Collection<Text> texts, Collection<Part> parts, Map globals) {
Tuple2<Collection<Diagnostic>, String> render(
SpecialPage specialPage,
RenderContext context
) {
def diagnostics = []
try {
Collection<Diagnostic> diagnostics = []
def result = engine.createTemplate(specialPage.text).make([
globals: globals,
parts: new EmbeddablePartsMap(parts, globals, { Collection<Diagnostic> partDiagnostics ->
diagnostics.addAll(partDiagnostics)
}),
texts: new EmbeddableTextsCollection(texts, globals, { Collection<Diagnostic> textDiagnostics ->
diagnostics.addAll(textDiagnostics)
})
])
def dslMap = StandardDslMap.get(context) {
it.loggerName = "GspSpecialPage(${ specialPage.path })"
it.onDiagnostics = diagnostics.&addAll
}
def result = engine.createTemplate(specialPage.text).make(dslMap)
new Tuple2<>(diagnostics, result.toString())
} catch (Exception e) {
new Tuple2<>([new Diagnostic("An exception occurred while rendering specialPage ${ specialPage.path }:\n${ e }", e)], '')
new Tuple2<>(
[*diagnostics, new Diagnostic(
"An exception occurred while rendering specialPage ${ specialPage.path }:\n${ e }",
e
)],
''
)
}
}

View File

@ -5,13 +5,13 @@ import groovy.transform.NullCheck
import groovy.transform.TupleConstructor
@TupleConstructor(defaults = false)
@NullCheck
@NullCheck(includeGenerated = true)
@EqualsAndHashCode
class SpecialPage {
final class SpecialPage {
String text
String path
SpecialPageType type
final String text
final String path
final SpecialPageType type
@Override
String toString() {

View File

@ -1,55 +1,39 @@
package com.jessebrault.ssg.specialpage
import com.jessebrault.ssg.provider.WithWatchableDir
import com.jessebrault.ssg.util.FileNameHandler
import com.jessebrault.ssg.util.RelativePathHandler
import groovy.io.FileType
import com.jessebrault.ssg.provider.AbstractFileCollectionProvider
import groovy.transform.EqualsAndHashCode
import groovy.transform.NullCheck
import org.jetbrains.annotations.Nullable
import org.slf4j.Logger
import org.slf4j.LoggerFactory
@NullCheck
@EqualsAndHashCode(includeFields = true)
class SpecialPageFileSpecialPagesProvider implements SpecialPagesProvider, WithWatchableDir {
class SpecialPageFileSpecialPagesProvider extends AbstractFileCollectionProvider<SpecialPage>
implements SpecialPagesProvider {
private static final Logger logger = LoggerFactory.getLogger(SpecialPageFileSpecialPagesProvider)
private final Collection<SpecialPageType> specialPageTypes
private final File specialPagesDir
SpecialPageFileSpecialPagesProvider(Collection<SpecialPageType> specialPageTypes, File specialPagesDir) {
this.specialPageTypes = specialPageTypes
this.specialPagesDir = specialPagesDir
this.watchableDir = this.specialPagesDir
SpecialPageFileSpecialPagesProvider(File specialPagesDir, Collection<SpecialPageType> specialPageTypes) {
super(specialPagesDir)
this.specialPageTypes = Objects.requireNonNull(specialPageTypes)
}
private SpecialPageType getSpecialPageType(File file) {
private @Nullable SpecialPageType getSpecialPageType(String extension) {
this.specialPageTypes.find {
it.ids.contains(new FileNameHandler(file).getExtension())
it.ids.contains(extension)
}
}
@Override
Collection<SpecialPage> provide() {
if (!this.specialPagesDir.isDirectory()) {
logger.warn('specialPagesDir {} does not exist or is not a directory; skipping and providing no SpecialPages', this.specialPagesDir)
[]
} else {
def specialPages = []
this.specialPagesDir.eachFileRecurse(FileType.FILES) {
def type = this.getSpecialPageType(it)
if (type != null) {
def relativePath = this.specialPagesDir.relativePath(it)
def path = new RelativePathHandler(relativePath).getWithoutExtension()
logger.info('found specialPage {} with type {}', path, type)
specialPages << new SpecialPage(it.text, path, type)
} else {
logger.warn('ignoring {} since there is no specialPageType for it', it)
}
}
specialPages
protected @Nullable SpecialPage transformFileToT(File file, String relativePath, String extension) {
def specialPageType = getSpecialPageType(extension)
if (!specialPageType) {
logger.warn('there is no SpecialPageType for {}, ignoring', relativePath)
}
specialPageType ? new SpecialPage(file.text, relativePath, specialPageType) : null
}
@Override
@ -59,7 +43,8 @@ class SpecialPageFileSpecialPagesProvider implements SpecialPagesProvider, WithW
@Override
String toString() {
"SpecialPageFileSpecialPagesProvider(specialPagesDir: ${ this.specialPagesDir }, specialPageTypes: ${ this.specialPageTypes })"
"SpecialPageFileSpecialPagesProvider(specialPagesDir: ${ this.dir }, " +
"specialPageTypes: ${ this.specialPageTypes })"
}
}

View File

@ -1,16 +1,16 @@
package com.jessebrault.ssg.specialpage
import com.jessebrault.ssg.Diagnostic
import com.jessebrault.ssg.SiteSpec
import com.jessebrault.ssg.part.Part
import com.jessebrault.ssg.renderer.RenderContext
import com.jessebrault.ssg.text.Text
interface SpecialPageRenderer {
Tuple2<Collection<Diagnostic>, String> render(
SpecialPage specialPage,
Collection<Text> texts,
Collection <Part> parts,
Map globals
RenderContext context
)
}

View File

@ -0,0 +1,77 @@
package com.jessebrault.ssg.tagbuilder
import org.codehaus.groovy.runtime.InvokerHelper
class DynamicTagBuilder implements TagBuilder {
@Override
String create(String name) {
"<$name />"
}
@Override
String create(String name, Map<String, ?> attributes) {
def formattedAttributes = attributes.collect {
if (it.value instanceof String) {
it.key + '="' + it.value + '"'
} else if (it.value instanceof Integer) {
it.key + '=' + it.value
} else if (it.value instanceof Boolean && it.value == true) {
it.key
} else {
it.key + '="' + it.value.toString() + '"'
}
}.join(' ')
"<$name $formattedAttributes />"
}
@Override
String create(String name, String body) {
"<$name>$body</$name>"
}
@Override
String create(String name, Map<String, ?> attributes, String body) {
def formattedAttributes = attributes.collect {
if (it.value instanceof String) {
it.key + '="' + it.value + '"'
} else if (it.value instanceof Integer) {
it.key + '=' + it.value
} else if (it.value instanceof Boolean && it.value == true) {
it.key
} else {
it.key + '="' + it.value.toString() + '"'
}
}.join(' ')
"<$name $formattedAttributes>$body</$name>"
}
@Override
Object invokeMethod(String name, Object args) {
def argsList = InvokerHelper.asList(args)
return switch (argsList.size()) {
case 0 -> this.create(name)
case 1 -> {
def arg0 = argsList[0]
if (arg0 instanceof Map) {
this.create(name, arg0)
} else if (arg0 instanceof String) {
this.create(name, arg0)
} else {
throw new MissingMethodException(name, this.class, args, false)
}
}
case 2 -> {
def arg0 = argsList[0]
def arg1 = argsList[1]
if (arg0 instanceof Map && arg1 instanceof String) {
this.create(name, arg0, arg1)
} else {
throw new MissingMethodException(name, this.class, args, false)
}
}
default -> throw new MissingMethodException(name, this.class, args, false)
}
}
}

View File

@ -0,0 +1,8 @@
package com.jessebrault.ssg.tagbuilder
interface TagBuilder {
String create(String name)
String create(String name, Map<String, ?> attributes)
String create(String name, String body)
String create(String name, Map<String, ?> attributes, String body)
}

View File

@ -0,0 +1,31 @@
package com.jessebrault.ssg.task
import groovy.transform.EqualsAndHashCode
import groovy.transform.NullCheck
@NullCheck
@EqualsAndHashCode
abstract class AbstractTask<T extends Task> implements Task {
final TaskType<T> type
final String name
AbstractTask(TaskType<T> type, String name) {
this.type = type
this.name = name
}
protected abstract T getThis()
@Override
void execute(TaskExecutorContext context) {
// I am guessing that if we put this.getThis(), it will think the runtime type is AbstractTask? Not sure.
this.type.executor.execute(getThis(), context)
}
@Override
String toString() {
"AbstractTask(name: ${ this.name }, type: ${ this.type })"
}
}

View File

@ -0,0 +1,5 @@
package com.jessebrault.ssg.task
interface FileInput extends Input {
File getFile()
}

View File

@ -0,0 +1,6 @@
package com.jessebrault.ssg.task
interface FileOutput extends Output {
File getFile()
String getContent()
}

View File

@ -0,0 +1,26 @@
package com.jessebrault.ssg.task
import groovy.transform.EqualsAndHashCode
import groovy.transform.NullCheck
import groovy.transform.TupleConstructor
@TupleConstructor(defaults = false, includeFields = true)
@NullCheck(includeGenerated = true)
@EqualsAndHashCode
final class HtmlFileOutput {
final File file
final String htmlPath
private final Closure<String> contentClosure
String getContent(TaskContainer tasks, TaskTypeContainer taskTypes, Closure onDiagnostics) {
this.contentClosure(tasks, taskTypes, onDiagnostics)
}
@Override
String toString() {
"HtmlFileOutput(file: ${ this.file }, htmlPath: ${ this.htmlPath })"
}
}

View File

@ -0,0 +1,5 @@
package com.jessebrault.ssg.task
interface Input {
String getName()
}

View File

@ -0,0 +1,5 @@
package com.jessebrault.ssg.task
interface Output {
String getName()
}

View File

@ -0,0 +1,51 @@
package com.jessebrault.ssg.task
import com.jessebrault.ssg.specialpage.SpecialPage
import groovy.transform.EqualsAndHashCode
import groovy.transform.NullCheck
@NullCheck
@EqualsAndHashCode
final class SpecialPageToHtmlFileTask extends AbstractTask<SpecialPageToHtmlFileTask> {
private static final class SpecialPageToHtmlFileTaskExecutor implements TaskExecutor<SpecialPageToHtmlFileTask> {
@Override
void execute(SpecialPageToHtmlFileTask task, TaskExecutorContext context) {
task.output.file.createParentDirectories()
task.output.file.write(task.output.getContent(
context.allTasks, context.allTypes, context.onDiagnostics
))
}
@Override
String toString() {
'SpecialPageToHtmlFileTaskExecutor()'
}
}
static final TaskType<SpecialPageToHtmlFileTask> TYPE = new TaskType<>(
'specialPageToHtmlFile', new SpecialPageToHtmlFileTaskExecutor()
)
final SpecialPage input
final HtmlFileOutput output
SpecialPageToHtmlFileTask(String name, SpecialPage input, HtmlFileOutput output) {
super(TYPE, name)
this.input = input
this.output = output
}
@Override
protected SpecialPageToHtmlFileTask getThis() {
this
}
@Override
String toString() {
"SpecialPageToHtmlFileTask(input: ${ this.input }, output: ${ this.output }, super: ${ super })"
}
}

View File

@ -0,0 +1,87 @@
package com.jessebrault.ssg.task
import com.jessebrault.ssg.Build
import com.jessebrault.ssg.Result
import com.jessebrault.ssg.renderer.RenderContext
import com.jessebrault.ssg.util.ExtensionsUtil
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.slf4j.Marker
import org.slf4j.MarkerFactory
final class SpecialPageToHtmlFileTaskFactory implements TaskFactory<SpecialPageToHtmlFileTask> {
private static final Logger logger = LoggerFactory.getLogger(SpecialPageToHtmlFileTaskFactory)
private static final Marker enter = MarkerFactory.getMarker('ENTER')
private static final Marker exit = MarkerFactory.getMarker('EXIT')
@Override
TaskType<SpecialPageToHtmlFileTask> getTaskType() {
SpecialPageToHtmlFileTask.TYPE
}
@Override
Result<TaskCollection<SpecialPageToHtmlFileTask>> getTasks(Build build) {
logger.trace(enter, 'build: {}', build)
logger.info('processing build with name {} for SpecialPageToHtmlFileTasks', build.name)
def config = build.config
def siteSpec = build.siteSpec
def globals = build.globals
def specialPages = config.specialPagesProviders.collectMany { it.provide() }
def templates = config.templatesProviders.collectMany { it.provide() }
def parts = config.partsProviders.collectMany { it.provide() }
def texts = config.textProviders.collectMany { it.provide() }
logger.debug('\n\tspecialPages: {}\n\ttemplates: {}\n\tparts: {}', specialPages, templates, parts)
def tasks = new TaskCollection<SpecialPageToHtmlFileTask>(specialPages.findResults {
logger.trace(enter, 'specialPage: {}', it)
logger.info('processing specialPage with path: {}', it.path)
def htmlPath = ExtensionsUtil.stripExtension(it.path) + '.html'
def renderSpecialPage = { TaskContainer tasks, TaskTypeContainer taskTypes, Closure<Void> onDiagnostics ->
def renderResult = it.type.renderer.render(
it,
new RenderContext(
config,
siteSpec,
globals,
texts,
parts,
it.path,
htmlPath,
tasks,
taskTypes
)
)
if (!renderResult.v1.isEmpty()) {
onDiagnostics(renderResult.v1)
''
} else {
renderResult.v2
}
}
def result = new SpecialPageToHtmlFileTask(
"specialPageToHtmlFileTask:${ it.path }:${ htmlPath }",
it,
new HtmlFileOutput(
new File(build.outDir, htmlPath),
htmlPath,
renderSpecialPage
)
)
logger.trace(exit, 'result: {}', result)
result
})
def result = new Result<>([], tasks)
logger.trace(exit, 'result: {}', result)
result
}
}

View File

@ -0,0 +1,7 @@
package com.jessebrault.ssg.task
interface Task {
TaskType<? extends Task> getType()
String getName()
void execute(TaskExecutorContext context)
}

View File

@ -0,0 +1,24 @@
package com.jessebrault.ssg.task
import static java.util.Objects.requireNonNull
class TaskCollection<T extends Task> {
@Delegate
private final Collection<T> tasks = new ArrayList<T>()
TaskCollection(Collection<? extends T> tasks = null) {
if (tasks != null) {
this.tasks.addAll(requireNonNull(tasks))
}
}
def <U extends T> TaskCollection<U> findAllByType(
TaskType<U> taskType
) {
new TaskCollection<>(this.tasks.findResults {
it.type == taskType ? it : null
})
}
}

View File

@ -0,0 +1,9 @@
package com.jessebrault.ssg.task
final class TaskContainer extends TaskCollection<Task> {
TaskContainer(Collection<? extends Task> tasks = null) {
super(tasks)
}
}

View File

@ -0,0 +1,5 @@
package com.jessebrault.ssg.task
interface TaskExecutor<T extends Task> {
void execute(T task, TaskExecutorContext context)
}

View File

@ -0,0 +1,23 @@
package com.jessebrault.ssg.task
import com.jessebrault.ssg.Build
import groovy.transform.EqualsAndHashCode
import groovy.transform.NullCheck
import groovy.transform.TupleConstructor
@TupleConstructor(defaults = false)
@NullCheck(includeGenerated = true)
@EqualsAndHashCode
final class TaskExecutorContext {
final Build build
final TaskContainer allTasks
final TaskTypeContainer allTypes
final Closure onDiagnostics
@Override
String toString() {
"TaskExecutorContext(build: ${ this.build }, allTasks: ${ this.allTasks }, allTypes: ${ this.allTypes })"
}
}

View File

@ -0,0 +1,9 @@
package com.jessebrault.ssg.task
import com.jessebrault.ssg.Build
import com.jessebrault.ssg.Result
interface TaskFactory<T extends Task> {
TaskType<T> getTaskType()
Result<TaskCollection<T>> getTasks(Build build)
}

View File

@ -0,0 +1,20 @@
package com.jessebrault.ssg.task
import groovy.transform.EqualsAndHashCode
import groovy.transform.NullCheck
import groovy.transform.TupleConstructor
@TupleConstructor(defaults = false)
@NullCheck(includeGenerated = true)
@EqualsAndHashCode
final class TaskType<T extends Task> {
final String name
final TaskExecutor<T> executor
@Override
String toString() {
"TaskType(${ this.name }, ${ this.executor })"
}
}

View File

@ -0,0 +1,31 @@
package com.jessebrault.ssg.task
final class TaskTypeContainer {
@Delegate
private final Set<TaskType<? extends Task>> taskTypes = []
TaskTypeContainer(Collection<TaskType<? extends Task>> taskTypes) {
if (taskTypes != null) {
this.taskTypes.addAll(taskTypes)
}
}
TaskTypeContainer(TaskTypeContainer taskTypeContainer) {
if (taskTypeContainer != null) {
this.taskTypes.addAll(taskTypeContainer)
}
}
TaskTypeContainer() {}
@Override
TaskType<? extends Task> getProperty(String propertyName) {
def taskType = this.taskTypes.find { it.name == propertyName }
if (!taskType) {
throw new IllegalArgumentException("no such taskType: ${ propertyName }")
}
taskType
}
}

View File

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

View File

@ -0,0 +1,51 @@
package com.jessebrault.ssg.task
import com.jessebrault.ssg.text.Text
import groovy.transform.EqualsAndHashCode
import groovy.transform.NullCheck
@NullCheck
@EqualsAndHashCode(callSuper = true)
final class TextToHtmlFileTask extends AbstractTask<TextToHtmlFileTask> {
private static final class TextToHtmlFileTaskExecutor implements TaskExecutor<TextToHtmlFileTask> {
@Override
void execute(TextToHtmlFileTask task, TaskExecutorContext context) {
task.output.file.createParentDirectories()
task.output.file.write(task.output.getContent(
context.allTasks, context.allTypes, context.onDiagnostics
))
}
@Override
String toString() {
'TextToHtmlFileTaskExecutor()'
}
}
static final TaskType<TextToHtmlFileTask> TYPE = new TaskType<>(
'textToHtmlFile', new TextToHtmlFileTaskExecutor()
)
final Text input
final HtmlFileOutput output
TextToHtmlFileTask(String name, Text input, HtmlFileOutput output) {
super(TYPE, name)
this.input = input
this.output = output
}
@Override
protected TextToHtmlFileTask getThis() {
this
}
@Override
String toString() {
"TextToHtmlFileTask(input: ${ this.input }, output: ${ this.output }, super: ${ super })"
}
}

View File

@ -0,0 +1,117 @@
package com.jessebrault.ssg.task
import com.jessebrault.ssg.Build
import com.jessebrault.ssg.Diagnostic
import com.jessebrault.ssg.Result
import com.jessebrault.ssg.renderer.RenderContext
import com.jessebrault.ssg.text.FrontMatter
import com.jessebrault.ssg.util.ExtensionsUtil
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.slf4j.Marker
import org.slf4j.MarkerFactory
final class TextToHtmlFileTaskFactory implements TaskFactory<TextToHtmlFileTask> {
private static final Logger logger = LoggerFactory.getLogger(TextToHtmlFileTaskFactory)
private static final Marker enter = MarkerFactory.getMarker('ENTER')
private static final Marker exit = MarkerFactory.getMarker('EXIT')
@Override
TaskType<TextToHtmlFileTask> getTaskType() {
TextToHtmlFileTask.TYPE
}
@Override
Result<TaskCollection<TextToHtmlFileTask>> getTasks(Build build) {
logger.trace(enter, 'build: {}', build)
logger.info('getting TextToHtmlFileTasks for build with name: {}', build.name)
def config = build.config
def siteSpec = build.siteSpec
def globals = build.globals
def diagnostics = []
// Get all texts, templates, parts, and specialPages
def texts = config.textProviders.collectMany { it.provide() }
def templates = config.templatesProviders.collectMany { it.provide() }
def parts = config.partsProviders.collectMany { it.provide() }
logger.debug('\n\ttexts: {}\n\ttemplates: {}\n\tparts: {}', texts, templates, parts)
def tasks = new TaskCollection<TextToHtmlFileTask>(texts.findResults {
logger.trace(enter, 'text: {}', it)
logger.info('processing text with path: {}', it.path)
def frontMatterResult = it.type.frontMatterGetter.get(it)
FrontMatter frontMatter
if (!frontMatterResult.v1.isEmpty()) {
diagnostics.addAll(frontMatterResult.v1)
logger.trace(exit, 'result: {}', null)
return null
} else {
frontMatter = frontMatterResult.v2
logger.debug('frontMatter: {}', frontMatter)
}
def desiredTemplate = frontMatter.find('template')
if (desiredTemplate.isEmpty()) {
logger.info('text with path {} has no \'template\' key in its frontMatter; skipping', it.path)
logger.trace(exit, 'result: {}', null)
return null
}
def template = templates.find { it.path == desiredTemplate.get() }
if (template == null) {
diagnostics << new Diagnostic("in text with path ${ it.path }, frontMatter.template refers to an unknown template: ${ desiredTemplate.get() }")
logger.trace(exit, 'result: {}', null)
return null
}
logger.debug('found template: {}', template)
def htmlPath = ExtensionsUtil.stripExtension(it.path) + '.html'
def renderTemplate = { TaskContainer tasks, TaskTypeContainer taskTypes, Closure<Void> onDiagnostics ->
def templateRenderResult = template.type.renderer.render(
template,
it,
new RenderContext(
config,
siteSpec,
globals,
texts,
parts,
it.path,
htmlPath,
tasks,
taskTypes
)
)
if (!templateRenderResult.v1.isEmpty()) {
onDiagnostics(templateRenderResult.v1)
''
} else {
templateRenderResult.v2
}
}
def result = new TextToHtmlFileTask(
"textToHtmlFileTask:${ it.path }:${ htmlPath }",
it,
new HtmlFileOutput(
new File(build.outDir, htmlPath),
htmlPath,
renderTemplate
)
)
logger.trace(exit, 'result: {}', result)
result
})
def result = new Result<>(diagnostics, tasks)
logger.trace(exit, 'result: {}', result)
result
}
}

View File

@ -0,0 +1,5 @@
package com.jessebrault.ssg.task
interface WithInput<I extends Input> {
I getInput()
}

View File

@ -0,0 +1,5 @@
package com.jessebrault.ssg.task
interface WithOutput<O extends Output> {
O getOutput()
}

View File

@ -1,9 +1,9 @@
package com.jessebrault.ssg.template
import com.jessebrault.ssg.Diagnostic
import com.jessebrault.ssg.part.EmbeddablePartsMap
import com.jessebrault.ssg.part.Part
import com.jessebrault.ssg.text.FrontMatter
import com.jessebrault.ssg.dsl.StandardDslMap
import com.jessebrault.ssg.renderer.RenderContext
import com.jessebrault.ssg.text.Text
import groovy.text.GStringTemplateEngine
import groovy.text.TemplateEngine
import groovy.transform.EqualsAndHashCode
@ -18,24 +18,26 @@ class GspTemplateRenderer implements TemplateRenderer {
@Override
Tuple2<Collection<Diagnostic>, String> render(
Template template,
FrontMatter frontMatter,
String text,
Collection<Part> parts,
Map globals
Text text,
RenderContext context
) {
def diagnostics = []
try {
Collection<Diagnostic> diagnostics = []
def result = engine.createTemplate(template.text).make([
frontMatter: frontMatter,
globals: globals,
parts: new EmbeddablePartsMap(parts, globals, { Collection<Diagnostic> partDiagnostics ->
diagnostics.addAll(partDiagnostics)
}),
text: text
])
def dslMap = StandardDslMap.get(context) {
it.loggerName = "GspTemplate(${ template.path })"
it.onDiagnostics = diagnostics.&addAll
it.text = text
}
def result = engine.createTemplate(template.text).make(dslMap)
new Tuple2<>(diagnostics, result.toString())
} catch (Exception e) {
new Tuple2<>([new Diagnostic("An exception occurred while rendering Template ${ template.path }:\n${ e }", e)], '')
new Tuple2<>(
[*diagnostics, new Diagnostic(
"An exception occurred while rendering Template ${ template.path }:\n${ e }",
e
)],
''
)
}
}

View File

@ -1,53 +1,38 @@
package com.jessebrault.ssg.template
import com.jessebrault.ssg.provider.WithWatchableDir
import com.jessebrault.ssg.util.FileNameHandler
import groovy.io.FileType
import com.jessebrault.ssg.provider.AbstractFileCollectionProvider
import groovy.transform.EqualsAndHashCode
import groovy.transform.NullCheck
import org.jetbrains.annotations.Nullable
import org.slf4j.Logger
import org.slf4j.LoggerFactory
@NullCheck
@EqualsAndHashCode(includeFields = true)
class TemplateFileTemplatesProvider implements TemplatesProvider, WithWatchableDir {
class TemplateFileTemplatesProvider extends AbstractFileCollectionProvider<Template> implements TemplatesProvider {
private static final Logger logger = LoggerFactory.getLogger(TemplateFileTemplatesProvider)
private final Collection<TemplateType> templateTypes
private final File templatesDir
TemplateFileTemplatesProvider(Collection<TemplateType> templateTypes, File templatesDir) {
this.templateTypes = templateTypes
this.templatesDir = templatesDir
this.watchableDir = this.templatesDir
TemplateFileTemplatesProvider(File templatesDir, Collection<TemplateType> templateTypes) {
super(templatesDir)
this.templateTypes = Objects.requireNonNull(templateTypes)
}
private TemplateType getType(File file) {
private @Nullable TemplateType getType(String extension) {
this.templateTypes.find {
it.ids.contains(new FileNameHandler(file).getExtension())
it.ids.contains(extension)
}
}
@Override
Collection<Template> provide() {
if (!this.templatesDir.isDirectory()) {
logger.warn('templatesDir {} does not exist or is not a directory; skipping and providing no Templates', this.templatesDir)
[]
} else {
def templates = []
this.templatesDir.eachFileRecurse(FileType.FILES) {
def type = this.getType(it)
if (type != null) {
def relativePath = this.templatesDir.relativePath(it)
logger.debug('found template {}', relativePath)
templates << new Template(it.text, relativePath, type)
} else {
logger.warn('ignoring {} because there is no templateType for it', it)
}
}
templates
protected Template transformFileToT(File file, String relativePath, String extension) {
def templateType = getType(extension)
if (templateType == null) {
logger.warn('there is no TemplateType for template {}, ignoring', relativePath)
}
templateType ? new Template(file.text, relativePath, templateType) : null
}
@Override
@ -57,7 +42,7 @@ class TemplateFileTemplatesProvider implements TemplatesProvider, WithWatchableD
@Override
String toString() {
"TemplateFileTemplatesProvider(templatesDir: ${ this.templatesDir }, templateTypes: ${ this.templateTypes })"
"TemplateFileTemplatesProvider(templatesDir: ${ this.dir }, templateTypes: ${ this.templateTypes })"
}
}

View File

@ -1,17 +1,15 @@
package com.jessebrault.ssg.template
import com.jessebrault.ssg.Diagnostic
import com.jessebrault.ssg.part.Part
import com.jessebrault.ssg.text.FrontMatter
import com.jessebrault.ssg.renderer.RenderContext
import com.jessebrault.ssg.text.Text
interface TemplateRenderer {
Tuple2<Collection<Diagnostic>, String> render(
Template template,
FrontMatter frontMatter,
String text,
Collection<Part> parts,
Map globals
Text text,
RenderContext context
)
}

View File

@ -6,7 +6,7 @@ import groovy.transform.NullCheck
import groovy.transform.TupleConstructor
@TupleConstructor(includeFields = true, defaults = false)
@NullCheck
@NullCheck(includeGenerated = true)
@EqualsAndHashCode(includeFields = true)
class EmbeddableText {

View File

@ -5,13 +5,13 @@ import groovy.transform.NullCheck
import groovy.transform.TupleConstructor
@TupleConstructor(defaults = false)
@NullCheck
@NullCheck(includeGenerated = true)
@EqualsAndHashCode
class Text {
final class Text {
String text
String path
TextType type
final String text
final String path
final TextType type
@Override
String toString() {

View File

@ -1,55 +1,38 @@
package com.jessebrault.ssg.text
import com.jessebrault.ssg.provider.WithWatchableDir
import com.jessebrault.ssg.util.FileNameHandler
import com.jessebrault.ssg.util.RelativePathHandler
import groovy.io.FileType
import com.jessebrault.ssg.provider.AbstractFileCollectionProvider
import groovy.transform.EqualsAndHashCode
import groovy.transform.NullCheck
import org.jetbrains.annotations.Nullable
import org.slf4j.Logger
import org.slf4j.LoggerFactory
@NullCheck
@EqualsAndHashCode(includeFields = true)
class TextFileTextsProvider implements TextsProvider, WithWatchableDir {
class TextFileTextsProvider extends AbstractFileCollectionProvider<Text> implements TextsProvider {
private static final Logger logger = LoggerFactory.getLogger(TextFileTextsProvider)
private final Collection<TextType> textTypes
private final File textsDir
TextFileTextsProvider(Collection<TextType> textTypes, File textsDir) {
this.textTypes = textTypes
this.textsDir = textsDir
this.watchableDir = this.textsDir
TextFileTextsProvider(File textsDir, Collection<TextType> textTypes) {
super(textsDir)
this.textTypes = Objects.requireNonNull(textTypes)
}
private TextType getTextType(File file) {
private TextType getTextType(String extension) {
this.textTypes.find {
it.ids.contains(new FileNameHandler(file).getExtension())
it.ids.contains(extension)
}
}
@Override
Collection<Text> provide() {
if (!this.textsDir.isDirectory()) {
logger.warn('textsDir {} does not exist or is not a directory; skipping and providing no Texts', this.textsDir)
[]
} else {
def textFiles = []
this.textsDir.eachFileRecurse(FileType.FILES) {
def type = this.getTextType(it)
if (type != null) {
def relativePath = this.textsDir.relativePath(it)
def path = new RelativePathHandler(relativePath).getWithoutExtension()
logger.debug('found textFile {} with type {}', path, type)
textFiles << new Text(it.text, path, type)
} else {
logger.warn('ignoring {} because there is no textType for it', it)
}
}
textFiles
protected @Nullable Text transformFileToT(File file, String relativePath, String extension) {
def textType = getTextType(extension)
if (!textType) {
logger.warn('no TextType for text {}, ignoring', file.path)
}
textType ? new Text(file.text, relativePath, textType) : null
}
@Override
@ -59,7 +42,7 @@ class TextFileTextsProvider implements TextsProvider, WithWatchableDir {
@Override
String toString() {
"TextFileTextsProvider(textsDir: ${ this.textsDir }, textTypes: ${ this.textTypes })"
"TextFileTextsProvider(textsDir: ${ this.dir }, textTypes: ${ this.textTypes })"
}
}

View File

@ -0,0 +1,37 @@
package com.jessebrault.ssg.url
import java.nio.file.Path
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

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

View File

@ -0,0 +1,24 @@
package com.jessebrault.ssg.util
import java.util.regex.Pattern
class ExtensionsUtil {
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 String getExtension(String path) {
def m = getExtensionPattern.matcher(path)
if (m.matches()) {
m.group(1)
} else {
throw new IllegalArgumentException("cannot get extension for path: ${ path }")
}
}
}

View File

@ -1,32 +0,0 @@
package com.jessebrault.ssg.util
import groovy.transform.EqualsAndHashCode
import groovy.transform.NullCheck
import groovy.transform.TupleConstructor
@TupleConstructor(includeFields = true, defaults = false)
@NullCheck
@EqualsAndHashCode(includeFields = true)
class FileNameHandler {
private final File file
String getExtension() {
def lastIndexOfDot = this.file.name.lastIndexOf('.')
if (lastIndexOfDot == -1) {
''
} else {
this.file.name.substring(lastIndexOfDot)
}
}
String getWithoutExtension() {
this.file.name.substring(0, this.file.name.lastIndexOf('.'))
}
@Override
String toString() {
"FileNameHandler(file: ${ this.file })"
}
}

View File

@ -1,23 +0,0 @@
package com.jessebrault.ssg.util
import groovy.transform.EqualsAndHashCode
import groovy.transform.NullCheck
import groovy.transform.TupleConstructor
@TupleConstructor(includeFields = true, defaults = false)
@NullCheck
@EqualsAndHashCode(includeFields = true)
class RelativePathHandler {
private final String relativePath
String getWithoutExtension() {
this.relativePath.subSequence(0, this.relativePath.lastIndexOf('.'))
}
@Override
String toString() {
"RelativePathHandler(relativePath: ${ this.relativePath })"
}
}

View File

@ -6,6 +6,10 @@ import com.jessebrault.ssg.part.PartType
import com.jessebrault.ssg.specialpage.GspSpecialPageRenderer
import com.jessebrault.ssg.specialpage.SpecialPageFileSpecialPagesProvider
import com.jessebrault.ssg.specialpage.SpecialPageType
import com.jessebrault.ssg.task.SpecialPageToHtmlFileTask
import com.jessebrault.ssg.task.Task
import com.jessebrault.ssg.task.TaskTypeContainer
import com.jessebrault.ssg.task.TextToHtmlFileTask
import com.jessebrault.ssg.template.GspTemplateRenderer
import com.jessebrault.ssg.template.TemplateFileTemplatesProvider
import com.jessebrault.ssg.template.TemplateType
@ -17,6 +21,8 @@ import com.jessebrault.ssg.text.TextType
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import static com.jessebrault.ssg.testutil.DiagnosticsUtil.assertEmptyDiagnostics
import static com.jessebrault.ssg.testutil.DiagnosticsUtil.getDiagnosticsMessageSupplier
import static org.junit.jupiter.api.Assertions.*
class SimpleStaticSiteGeneratorIntegrationTests {
@ -41,10 +47,10 @@ class SimpleStaticSiteGeneratorIntegrationTests {
def gspPartType = new PartType(['.gsp'], new GspPartRenderer())
def gspSpecialPageType = new SpecialPageType(['.gsp'], new GspSpecialPageRenderer())
def textsProvider = new TextFileTextsProvider([markdownTextType], this.textsDir)
def templatesProvider = new TemplateFileTemplatesProvider([gspTemplateType], this.templatesDir)
def partsProvider = new PartFilePartsProvider([gspPartType], this.partsDir)
def specialPagesProvider = new SpecialPageFileSpecialPagesProvider([gspSpecialPageType], this.specialPagesDir)
def textsProvider = new TextFileTextsProvider(this.textsDir, [markdownTextType])
def templatesProvider = new TemplateFileTemplatesProvider(this.templatesDir, [gspTemplateType])
def partsProvider = new PartFilePartsProvider(this.partsDir, [gspPartType])
def specialPagesProvider = new SpecialPageFileSpecialPagesProvider(this.specialPagesDir, [gspSpecialPageType])
def config = new Config(
textProviders: [textsProvider],
@ -52,25 +58,30 @@ class SimpleStaticSiteGeneratorIntegrationTests {
partsProviders: [partsProvider],
specialPagesProviders: [specialPagesProvider]
)
def siteSpec = new SiteSpec('Test Site', 'https://test.com')
def globals = [:]
this.build = new Build('testBuild', config, globals, new File('build'))
this.build = new Build('testBuild', config, siteSpec, globals, new File('build'))
this.ssg = new SimpleStaticSiteGenerator()
}
@Test
void simple() {
new File(this.textsDir, 'test.md').write('---\ntemplate: test.gsp\n---\n**Hello, World!**')
new File(this.templatesDir, 'test.gsp').write('<%= text %>')
new File(this.templatesDir, 'test.gsp').write('<%= text.render() %>')
def result = this.ssg.generate(this.build)
assertTrue(result.v1.size() == 0)
assertTrue(result.v2.size() == 1)
assertEmptyDiagnostics(result)
def tasks = result.get()
assertTrue(tasks.size() == 1)
def p0 = result.v2[0]
assertEquals('test', p0.path)
assertEquals('<p><strong>Hello, World!</strong></p>\n', p0.html)
def t0 = tasks.findAllByType(TextToHtmlFileTask.TYPE)[0]
assertEquals('test.html', t0.output.htmlPath)
def contentResult = t0.output.getContent(tasks, new TaskTypeContainer([TextToHtmlFileTask.TYPE])) { Collection<Diagnostic> diagnostics ->
fail(getDiagnosticsMessageSupplier(diagnostics))
}
assertEquals('<p><strong>Hello, World!</strong></p>\n', contentResult)
}
@Test
@ -81,44 +92,65 @@ class SimpleStaticSiteGeneratorIntegrationTests {
}
}
new File(this.templatesDir, 'nested.gsp').write('<%= text %>')
new File(this.templatesDir, 'nested.gsp').write('<%= text.render() %>')
def result = this.ssg.generate(this.build)
assertTrue(result.v1.size() == 0)
assertTrue(result.v2.size() == 1)
assertEmptyDiagnostics(result)
def tasks = result.get()
assertTrue(tasks.size() == 1)
def p0 = result.v2[0]
assertEquals('nested/nested', p0.path)
assertEquals('<p><strong>Hello, World!</strong></p>\n', p0.html)
def t0 = tasks.findAllByType(TextToHtmlFileTask.TYPE)[0]
assertEquals('nested/nested.html', t0.output.htmlPath)
def contentResult = t0.output.getContent(
tasks,
new TaskTypeContainer([TextToHtmlFileTask.TYPE])
) { Collection<Diagnostic> diagnostics ->
fail(getDiagnosticsMessageSupplier(diagnostics))
}
assertEquals('<p><strong>Hello, World!</strong></p>\n', contentResult)
}
@Test
void outputsSpecialPage() {
new FileTreeBuilder(this.specialPagesDir).file('special.gsp', $/<%= texts.find { it.path == 'test' }.render() %>/$)
new FileTreeBuilder(this.specialPagesDir)
.file('special.gsp', $/<%= texts.find { it.path == 'test.md' }.render() %>/$)
new FileTreeBuilder(this.templatesDir).file('template.gsp', '<%= 1 + 1 %>')
new FileTreeBuilder(this.textsDir).file('test.md', '---\ntemplate: template.gsp\n---\nHello, World!')
def result = this.ssg.generate(this.build)
assertEquals(0, result.v1.size())
assertEquals(2, result.v2.size())
assertEmptyDiagnostics(result)
def tasks = result.get()
assertEquals(2, tasks.size())
def testPage = result.v2.find { it.path == 'test' }
assertNotNull(testPage)
assertEquals('2', testPage.html)
def taskTypes = new TaskTypeContainer([TextToHtmlFileTask.TYPE, SpecialPageToHtmlFileTask.TYPE])
def specialPage = result.v2.find { it.path == 'special' }
assertNotNull(specialPage)
assertEquals('<p>Hello, World!</p>\n', specialPage.html)
def testPageTask = tasks.findAllByType(TextToHtmlFileTask.TYPE).find {
it.output.htmlPath == 'test.html'
}
assertNotNull(testPageTask)
def testPageContent = testPageTask.output.getContent(tasks, taskTypes) { Collection<Diagnostic> diagnostics ->
fail(getDiagnosticsMessageSupplier(diagnostics))
}
assertEquals('2', testPageContent)
def specialPageTask = tasks.findAllByType(SpecialPageToHtmlFileTask.TYPE).find {
it.output.htmlPath == 'special.html'
}
assertNotNull(specialPageTask)
def specialPageContent = specialPageTask.output.getContent(tasks, taskTypes) { Collection<Diagnostic> diagnostics ->
fail(getDiagnosticsMessageSupplier(diagnostics))
}
assertEquals('<p>Hello, World!</p>\n', specialPageContent)
}
@Test
void doesNotGenerateIfNoTemplateInFrontMatter() {
new File(this.textsDir, 'test.md').write('Hello, World!')
def result = this.ssg.generate(this.build)
assertEquals(0, result.v1.size())
assertEquals(0, result.v2.size())
assertEmptyDiagnostics(result)
assertEquals(0, result.get().size())
}
}

View File

@ -1,35 +1,109 @@
package com.jessebrault.ssg.part
import com.jessebrault.ssg.Diagnostic
import com.jessebrault.ssg.dsl.StandardDslConsumerTests
import com.jessebrault.ssg.renderer.RenderContext
import com.jessebrault.ssg.text.Text
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.mockito.junit.jupiter.MockitoExtension
import static com.jessebrault.ssg.testutil.DiagnosticsUtil.assertDiagnosticException
import static com.jessebrault.ssg.testutil.RenderContextUtil.getRenderContext
import static com.jessebrault.ssg.text.TextMocks.renderableText
import static org.junit.jupiter.api.Assertions.assertEquals
import static org.junit.jupiter.api.Assertions.assertTrue
class GspPartRendererTests {
@ExtendWith(MockitoExtension)
class GspPartRendererTests implements StandardDslConsumerTests {
private final PartRenderer renderer = new GspPartRenderer()
@Test
void rendersWithNoBindingOrGlobals() {
def part = new Part('', null, 'Hello, World!')
def r = this.renderer.render(part, [:], [:])
assertTrue(r.v1.size() == 0)
assertEquals('Hello, World!', r.v2)
private Tuple2<Collection<Diagnostic>, String> doRender(
String scriptlet,
RenderContext context,
Map binding = [:],
Text text = null
) {
this.renderer.render(
new Part('', new PartType([], this.renderer), scriptlet),
binding,
context,
text
)
}
@Override
Tuple2<Collection<Diagnostic>, String> render(String scriptlet, RenderContext context) {
this.doRender(scriptlet, context)
}
@Test
void rendersWithBinding() {
def part = new Part('', null, "<%= binding['greeting'] %>")
def r = this.renderer.render(part, [greeting: 'Hello, World!'], [:])
assertTrue(r.v1.size() == 0)
assertEquals('Hello, World!', r.v2)
this.checkResult(
'Hello, World!',
this.doRender('<%= binding.greeting %>', getRenderContext(), [greeting: 'Hello, World!'])
)
}
@Test
void rendersWithGlobals() {
def part = new Part(null, null, "<%= globals['greeting'] %>")
def r = this.renderer.render(part, [:], [greeting: 'Hello, World!'])
assertTrue(r.v1.size() == 0)
void textAvailable() {
this.checkResult('Hello, World!', this.renderer.render(
new Part('', new PartType([], this.renderer), '<%= text.render() %>'),
[:],
getRenderContext(),
renderableText('Hello, World!')
))
}
@Test
void nestedPartDiagnosticBubblesUp() {
def nestedProblemPart = new Part(
'nestedProblem.gsp',
new PartType([], this.renderer),
'<% throw new RuntimeException() %>'
)
def callerPart = new Part(
'caller.gsp',
null,
'<% parts["nestedProblem.gsp"].render() %>'
)
def r = this.renderer.render(
callerPart,
[:],
getRenderContext(parts: [callerPart, nestedProblemPart]),
null
)
assertEquals(1, r.v1.size())
assertDiagnosticException(RuntimeException, r.v1[0])
assertEquals('', r.v2)
}
@Test
void nestedPartIsBlankWhenThrowingExceptionButCallerRendered() {
def nestedProblemPart = new Part(
'nestedProblem.gsp',
new PartType([], this.renderer),
'<% throw new RuntimeException("nested problem exception") %>'
)
def callerPart = new Part(
'caller.gsp',
null,
'Hello, World!<% parts["nestedProblem.gsp"].render() %>'
)
def r = this.renderer.render(
callerPart,
[:],
getRenderContext(parts: [callerPart, nestedProblemPart]),
null
)
assertEquals(1, r.v1.size())
assertDiagnosticException(RuntimeException, r.v1[0]) { e ->
assertEquals('nested problem exception', e.message, {
def w = new StringWriter()
e.printStackTrace(new PrintWriter(w))
w.toString()
})
}
assertEquals('Hello, World!', r.v2)
}

View File

@ -4,10 +4,11 @@ import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import static org.junit.jupiter.api.Assertions.assertEquals
import static org.mockito.Mockito.mock
class PartFilePartsProviderTests {
private static final PartType gspPartType = new PartType(['.gsp'], null)
private static final PartType gspPartType = new PartType(['.gsp'], mock(PartRenderer))
private File partsDir
private PartsProvider partsProvider
@ -15,7 +16,7 @@ class PartFilePartsProviderTests {
@BeforeEach
void beforeEach() {
this.partsDir = File.createTempDir()
partsProvider = new PartFilePartsProvider([gspPartType], this.partsDir)
partsProvider = new PartFilePartsProvider(this.partsDir, [gspPartType])
}
@Test

View File

@ -1,72 +1,22 @@
package com.jessebrault.ssg.specialpage
import com.jessebrault.ssg.part.Part
import com.jessebrault.ssg.part.PartRenderer
import com.jessebrault.ssg.part.PartType
import com.jessebrault.ssg.text.FrontMatterGetter
import com.jessebrault.ssg.text.MarkdownExcerptGetter
import com.jessebrault.ssg.text.Text
import com.jessebrault.ssg.text.TextRenderer
import com.jessebrault.ssg.text.TextType
import org.junit.jupiter.api.Test
import com.jessebrault.ssg.Diagnostic
import com.jessebrault.ssg.dsl.StandardDslConsumerTests
import com.jessebrault.ssg.renderer.RenderContext
import org.junit.jupiter.api.extension.ExtendWith
import org.mockito.Mock
import org.mockito.junit.jupiter.MockitoExtension
import static org.junit.jupiter.api.Assertions.assertEquals
import static org.junit.jupiter.api.Assertions.assertTrue
import static org.mockito.ArgumentMatchers.any
import static org.mockito.ArgumentMatchers.argThat
import static org.mockito.Mockito.when
@ExtendWith(MockitoExtension)
class GspSpecialPageRendererTests {
class GspSpecialPageRendererTests implements StandardDslConsumerTests {
private final SpecialPageRenderer renderer = new GspSpecialPageRenderer()
@Test
void rendersGlobal() {
def specialPage = new SpecialPage("<%= globals['greeting'] %>", null, null)
def globals = [greeting: 'Hello, World!']
def r = this.renderer.render(specialPage, [], [], globals)
assertTrue(r.v1.size() == 0)
assertEquals('Hello, World!', r.v2)
}
@Test
void rendersPartWithNoBinding(@Mock PartRenderer partRenderer) {
when(partRenderer.render(any(), any(), any())).thenReturn(new Tuple2<>([], 'Hello, World!'))
def partType = new PartType([], partRenderer)
def part = new Part('test', partType , '')
def specialPage = new SpecialPage("<%= parts['test'].render() %>", null, null)
def r = this.renderer.render(specialPage, [], [part], [:])
assertTrue(r.v1.size() == 0)
assertEquals('Hello, World!', r.v2)
}
@Test
void rendersPartWithBinding(@Mock PartRenderer partRenderer) {
when(partRenderer.render(any(), argThat { Map m -> m.get('greeting') == 'Hello, World!'}, any())).thenReturn(new Tuple2<>([], 'Hello, World!'))
def partType = new PartType([], partRenderer)
def part = new Part('test', partType, '')
def specialPage = new SpecialPage("<%= parts['test'].render([greeting: 'Hello, World!'])", null, null)
def r = this.renderer.render(specialPage, [], [part], [:])
assertTrue(r.v1.size() == 0)
assertEquals('Hello, World!', r.v2)
}
@Test
void rendersText(@Mock TextRenderer textRenderer, @Mock FrontMatterGetter frontMatterGetter) {
when(textRenderer.render(any(), any())).thenReturn(new Tuple2<>([], '<p><strong>Hello, World!</strong></p>\n'))
def textType = new TextType([], textRenderer, frontMatterGetter, new MarkdownExcerptGetter())
def text = new Text('', 'test', textType)
def specialPage = new SpecialPage("<%= texts.find { it.path == 'test' }.render() %>", null, null)
def r = this.renderer.render(specialPage, [text], [], [:])
assertTrue(r.v1.size() == 0)
assertEquals('<p><strong>Hello, World!</strong></p>\n', r.v2)
@Override
Tuple2<Collection<Diagnostic>, String> render(String scriptlet, RenderContext context) {
this.renderer.render(
new SpecialPage(scriptlet, '', new SpecialPageType([], this.renderer)),
context
)
}
}

View File

@ -4,10 +4,11 @@ import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import static org.junit.jupiter.api.Assertions.assertEquals
import static org.mockito.Mockito.mock
class SpecialPageFileSpecialPagesProviderTests {
private static final SpecialPageType gspType = new SpecialPageType(['.gsp'], null)
private static final SpecialPageType gspType = new SpecialPageType(['.gsp'], mock(SpecialPageRenderer))
private File specialPagesDir
private SpecialPagesProvider specialPagesProvider
@ -15,7 +16,7 @@ class SpecialPageFileSpecialPagesProviderTests {
@BeforeEach
void beforeEach() {
this.specialPagesDir = File.createTempDir()
this.specialPagesProvider = new SpecialPageFileSpecialPagesProvider([gspType], this.specialPagesDir)
this.specialPagesProvider = new SpecialPageFileSpecialPagesProvider(this.specialPagesDir, [gspType])
}
@Test
@ -26,7 +27,7 @@ class SpecialPageFileSpecialPagesProviderTests {
def r = this.specialPagesProvider.provide()
assertEquals(1, r.size())
def f0 = r[0]
assertEquals('test', f0.path)
assertEquals('test.gsp', f0.path)
assertEquals('<%= "Hello, World!" %>', f0.text)
assertEquals(gspType, f0.type)
}
@ -40,7 +41,7 @@ class SpecialPageFileSpecialPagesProviderTests {
def r = this.specialPagesProvider.provide()
assertEquals(1, r.size())
def f0 = r[0]
assertEquals('nested/nested', f0.path)
assertEquals('nested/nested.gsp', f0.path)
assertEquals('<%= "Hello, World!" %>', f0.text)
assertEquals(gspType, f0.type)
}

View File

@ -0,0 +1,54 @@
package com.jessebrault.ssg.tagbuilder
import org.junit.jupiter.api.Test
import static org.junit.jupiter.api.Assertions.assertEquals
class TagBuilderTests {
private final TagBuilder tagBuilder = new DynamicTagBuilder()
@Test
void simple() {
assertEquals('<test />', this.tagBuilder.create('test'))
}
@Test
void withAttributes() {
assertEquals('<test test="abc" />', this.tagBuilder.create('test', [test: 'abc']))
}
@Test
void withBody() {
assertEquals('<test>test</test>', this.tagBuilder.create('test', 'test'))
}
@Test
void withAttributesAndBody() {
assertEquals(
'<test test="abc">test</test>',
this.tagBuilder.create('test', [test: 'abc'], 'test')
)
}
@Test
void dynamicSimple() {
assertEquals('<test />', this.tagBuilder.test())
}
@Test
void dynamicWithAttributes() {
assertEquals('<test test="abc" />', this.tagBuilder.test([test: 'abc']))
}
@Test
void dynamicWithBody() {
assertEquals('<test>test</test>', this.tagBuilder.test('test'))
}
@Test
void dynamicWithAttributesAndBody() {
assertEquals('<test test="abc">test</test>', this.tagBuilder.test([test: 'abc'], 'test'))
}
}

View File

@ -1,80 +1,46 @@
package com.jessebrault.ssg.template
import com.jessebrault.ssg.part.Part
import com.jessebrault.ssg.part.PartRenderer
import com.jessebrault.ssg.part.PartType
import com.jessebrault.ssg.text.FrontMatter
import com.jessebrault.ssg.Diagnostic
import com.jessebrault.ssg.dsl.StandardDslConsumerTests
import com.jessebrault.ssg.renderer.RenderContext
import com.jessebrault.ssg.text.Text
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.mockito.Mock
import org.mockito.junit.jupiter.MockitoExtension
import static com.jessebrault.ssg.testutil.DiagnosticsUtil.getDiagnosticsMessageSupplier
import static com.jessebrault.ssg.testutil.RenderContextUtil.getRenderContext
import static com.jessebrault.ssg.text.TextMocks.blankText
import static com.jessebrault.ssg.text.TextMocks.renderableText
import static org.junit.jupiter.api.Assertions.assertEquals
import static org.junit.jupiter.api.Assertions.assertTrue
import static org.mockito.ArgumentMatchers.any
import static org.mockito.ArgumentMatchers.argThat
import static org.mockito.Mockito.when
@ExtendWith(MockitoExtension)
class GspTemplateRendererTests {
class GspTemplateRendererTests implements StandardDslConsumerTests {
private final TemplateRenderer renderer = new GspTemplateRenderer()
private Tuple2<Collection<Diagnostic>, String> doRender(String scriptlet, Text text, RenderContext context) {
this.renderer.render(new Template(scriptlet, '', null), text, context)
}
@Override
Tuple2<Collection<Diagnostic>, String> render(String scriptlet, RenderContext context) {
this.doRender(scriptlet, blankText(), context)
}
/**
* TODO: refactor this and the super interface methods so that we can re-use rendering logic
*/
@Test
void rendersPartWithNoBinding(@Mock PartRenderer partRenderer) {
def template = new Template(
"<%= parts['test'].render() %>",
null,
null
void textAvailableToRender() {
def template = new Template('<%= text.render() %>', null, null)
def r = this.renderer.render(
template,
renderableText('Hello, World!'),
getRenderContext()
)
when(partRenderer.render(any(), any(), any())).thenReturn(new Tuple2<>([], 'Hello, World!'))
def partType = new PartType([], partRenderer)
def part = new Part('test', partType, null)
def r = this.renderer.render(template, new FrontMatter(null, [:]), '', [part], [:])
assertTrue(r.v1.size() == 0)
assertEquals('Hello, World!', r.v2)
}
@Test
void rendersPartWithBinding(@Mock PartRenderer partRenderer) {
def template = new Template(
"<%= parts['greeting'].render([person: 'World']) %>",
null,
null
)
when(partRenderer.render(any(), argThat { Map m -> m.get('person') == 'World' }, any())).thenReturn(new Tuple2<>([], 'Hello, World!'))
def partType = new PartType([], partRenderer)
def part = new Part('greeting', partType, null)
def r = this.renderer.render(template, new FrontMatter(null, [:]), '', [part], [:])
assertTrue(r.v1.size() == 0)
assertEquals('Hello, World!', r.v2)
}
@Test
void rendersFrontMatter() {
def template = new Template("<%= frontMatter['title'] %>", null, null)
def r = this.renderer.render(template, new FrontMatter(null, [title: ['Hello!']]), '', [], [:])
assertTrue(r.v1.size() == 0)
assertEquals('Hello!', r.v2)
}
@Test
void rendersGlobal() {
def template = new Template("<%= globals['test'] %>", null, null)
def r = this.renderer.render(template, new FrontMatter(null, [:]), '', [], [test: 'Hello, World!'])
assertTrue(r.v1.size() == 0)
assertEquals('Hello, World!', r.v2)
}
@Test
void rendersText() {
def template = new Template('<%= text %>', null, null)
def r = this.renderer.render(template, new FrontMatter(null, [:]), 'Hello, World!', [], [:])
assertTrue(r.v1.size() == 0)
assertTrue(r.v1.isEmpty(), getDiagnosticsMessageSupplier(r.v1))
assertEquals('Hello, World!', r.v2)
}

View File

@ -4,10 +4,11 @@ import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import static org.junit.jupiter.api.Assertions.assertEquals
import static org.mockito.Mockito.mock
class PageTemplatesProviderTests {
class TemplateFileTemplatesProviderTests {
private static final TemplateType gspType = new TemplateType(['.gsp'], null)
private static final TemplateType gspType = new TemplateType(['.gsp'], mock(TemplateRenderer))
private File templatesDir
private TemplatesProvider templatesProvider
@ -15,7 +16,7 @@ class PageTemplatesProviderTests {
@BeforeEach
void beforeEach() {
this.templatesDir = File.createTempDir()
this.templatesProvider = new TemplateFileTemplatesProvider([gspType], this.templatesDir)
this.templatesProvider = new TemplateFileTemplatesProvider(this.templatesDir, [gspType])
}
@Test

View File

@ -2,6 +2,7 @@ package com.jessebrault.ssg.text
import org.junit.jupiter.api.Test
import static com.jessebrault.ssg.text.TextMocks.renderableText
import static org.junit.jupiter.api.Assertions.assertEquals
class ExcerptGetterTests {
@ -10,7 +11,7 @@ class ExcerptGetterTests {
@Test
void takesAllIfTextLessThanLimit() {
def text = new Text('One Two Three Four Five', null, null)
def text = renderableText('One Two Three Four Five')
def result = this.excerptGetter.getExcerpt(text, 10)
assertEquals(0, result.v1.size())
assertEquals('One Two Three Four Five', result.v2)
@ -18,7 +19,7 @@ class ExcerptGetterTests {
@Test
void takesTheLimit() {
def text = new Text('One Two Three Four Five', null, null)
def text = renderableText('One Two Three Four Five')
def result = this.excerptGetter.getExcerpt(text, 2)
assertEquals(0, result.v1.size())
assertEquals('One Two', result.v2)
@ -26,7 +27,7 @@ class ExcerptGetterTests {
@Test
void worksWithHeading() {
def text = new Text('# Heading\nOne Two Three', null, null)
def text = renderableText('# Heading\nOne Two Three')
def result = this.excerptGetter.getExcerpt(text, 1)
assertEquals(0, result.v1.size())
assertEquals('Heading', result.v2)
@ -34,7 +35,7 @@ class ExcerptGetterTests {
@Test
void worksWithFrontMatter() {
def text = new Text('---\ntest: hello\n---\nOne Two Three', null, null)
def text = renderableText('---\ntest: hello\n---\nOne Two Three')
def result = this.excerptGetter.getExcerpt(text, 1)
assertEquals(0, result.v1.size())
assertEquals('One', result.v2)

View File

@ -4,10 +4,16 @@ import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import static org.junit.jupiter.api.Assertions.assertEquals
import static org.mockito.Mockito.mock
class TextFileTextsProviderTests {
private static final TextType markdownType = new TextType(['.md'], null, null, null)
private static final TextType markdownType = new TextType(
['.md'],
mock(TextRenderer),
mock(FrontMatterGetter),
mock(ExcerptGetter)
)
private File textsDir
private TextsProvider textsProvider
@ -15,7 +21,7 @@ class TextFileTextsProviderTests {
@BeforeEach
void beforeEach() {
this.textsDir = File.createTempDir()
this.textsProvider = new TextFileTextsProvider([markdownType], this.textsDir)
this.textsProvider = new TextFileTextsProvider(this.textsDir, [markdownType])
}
@Test
@ -25,7 +31,7 @@ class TextFileTextsProviderTests {
def r = this.textsProvider.provide()
assertEquals(1, r.size())
def f0 = r[0]
assertEquals('test', f0.path)
assertEquals('test.md', f0.path)
assertEquals('**Hello, World!**', f0.text)
assertEquals(markdownType, f0.type)
}
@ -39,7 +45,7 @@ class TextFileTextsProviderTests {
def r = this.textsProvider.provide()
assertEquals(1, r.size())
def f0 = r[0]
assertEquals('nested/nested', f0.path)
assertEquals('nested/nested.md', f0.path)
assertEquals('**Hello!**', f0.text)
assertEquals(markdownType, f0.type)
}

View File

@ -0,0 +1,10 @@
package com.jessebrault.ssg.url
class PathBasedUrlBuilderTests extends AbstractUrlBuilderTests {
@Override
protected UrlBuilder getUrlBuilder(String targetPath, String baseUrl) {
new PathBasedUrlBuilder(targetPath, baseUrl)
}
}

View File

@ -0,0 +1,49 @@
package com.jessebrault.ssg.util
import org.junit.jupiter.api.Test
import static com.jessebrault.ssg.util.ExtensionsUtil.stripExtension
import static com.jessebrault.ssg.util.ExtensionsUtil.getExtension
import static org.junit.jupiter.api.Assertions.assertEquals
class ExtensionsUtilTests {
static class StripExtensionTests {
@Test
void simple() {
assertEquals('test', stripExtension('test.txt'))
}
@Test
void withSlashes() {
assertEquals('test/test', stripExtension('test/test.txt'))
}
@Test
void withMultipleExtensions() {
assertEquals('test.txt', stripExtension('test.txt.html'))
}
}
static class GetExtensionTests {
@Test
void simple() {
assertEquals('.txt', getExtension('test.txt'))
}
@Test
void withSlashes() {
assertEquals('.txt', getExtension('test/test.txt'))
}
@Test
void withMultipleExtensions() {
assertEquals('.txt', getExtension('test.test.txt'))
}
}
}

View File

@ -1,16 +0,0 @@
package com.jessebrault.ssg.util
import org.junit.jupiter.api.Test
import static org.junit.jupiter.api.Assertions.assertEquals
class FileNameHandlerTests {
@Test
void getsCorrectExtension() {
def file = new File('hello.txt')
def extension = new FileNameHandler(file).getExtension()
assertEquals('.txt', extension)
}
}

View File

@ -1,15 +0,0 @@
package com.jessebrault.ssg.util
import org.junit.jupiter.api.Test
import static org.junit.jupiter.api.Assertions.assertEquals
class RelativePathHandlerTests {
@Test
void stripsExtension() {
def stripped = new RelativePathHandler('hello.txt').getWithoutExtension()
assertEquals('hello', stripped)
}
}

View File

@ -0,0 +1,28 @@
package com.jessebrault.ssg.dsl
interface DslScriptletProvider {
String getGlobalsAvailable()
String getRenderGlobal()
String getLoggerAvailable()
String getPartsAvailable()
String getRenderPartFromParts()
String getSiteSpecAvailable()
String getRenderSiteSpecValues()
String getSourcePathAvailable()
String getRenderSourcePathValue()
String getTagBuilderAvailable()
String getTargetPathAvailable()
String getRenderTargetPath()
String getTextsAvailable()
String getRenderTextFromTexts()
String getUrlBuilderAvailable()
String getUrlBuilderCorrectlyConfigured()
}

View File

@ -0,0 +1,240 @@
package com.jessebrault.ssg.dsl
import com.jessebrault.ssg.Diagnostic
import com.jessebrault.ssg.SiteSpec
import com.jessebrault.ssg.part.EmbeddablePartsMap
import com.jessebrault.ssg.part.Part
import com.jessebrault.ssg.part.PartRenderer
import com.jessebrault.ssg.part.PartType
import com.jessebrault.ssg.renderer.RenderContext
import com.jessebrault.ssg.tagbuilder.TagBuilder
import com.jessebrault.ssg.task.HtmlFileOutput
import com.jessebrault.ssg.task.SpecialPageToHtmlFileTask
import com.jessebrault.ssg.task.TaskContainer
import com.jessebrault.ssg.task.TaskTypeContainer
import com.jessebrault.ssg.task.TextToHtmlFileTask
import com.jessebrault.ssg.task.TextToHtmlFileTaskFactory
import com.jessebrault.ssg.text.EmbeddableTextsCollection
import com.jessebrault.ssg.url.UrlBuilder
import net.bytebuddy.implementation.bytecode.Throw
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.mockito.Mock
import org.mockito.MockedStatic
import org.mockito.junit.jupiter.MockitoExtension
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import static com.jessebrault.ssg.task.SpecialPageToHtmlFileTaskMocks.blankSpecialPageToHtmlFileTask
import static com.jessebrault.ssg.task.TextToHtmlFileTaskMocks.blankTextToHtmlFileTask
import static com.jessebrault.ssg.testutil.DiagnosticsUtil.assertEmptyDiagnostics
import static com.jessebrault.ssg.testutil.RenderContextUtil.getRenderContext
import static com.jessebrault.ssg.text.TextMocks.blankText
import static com.jessebrault.ssg.text.TextMocks.renderableTextWithPath
import static org.junit.jupiter.api.Assertions.assertEquals
import static org.junit.jupiter.api.Assertions.fail
import static org.mockito.ArgumentMatchers.any
import static org.mockito.ArgumentMatchers.anyString
import static org.mockito.Mockito.*
@ExtendWith(MockitoExtension)
interface StandardDslConsumerTests {
Tuple2<Collection<Diagnostic>, String> render(String scriptlet, RenderContext context)
default void checkResult(String expected, Tuple2<Collection<Diagnostic>, String> result) {
assertEmptyDiagnostics(result)
assertEquals(expected, result.v2)
}
default void doDslRenderTest(String expected, String scriptlet, RenderContext context = null) {
this.checkResult(expected, this.render(scriptlet, context ?: getRenderContext()))
}
default void doDslAssertionTest(String scriptlet, RenderContext context = null) {
Tuple2<Collection<Diagnostic>, String> result = null
try {
result = this.render(scriptlet, context ?: getRenderContext())
} catch (Throwable e) {
fail(e)
}
assertEmptyDiagnostics(result)
}
@Test
default void rendersGlobal() {
this.doDslRenderTest(
'Hello, World!',
'<%= globals.test %>',
getRenderContext(globals: [test: 'Hello, World!'])
)
}
@Test
default void loggerAvailable(@Mock Logger logger) {
try (MockedStatic<LoggerFactory> loggerFactory = mockStatic(LoggerFactory)) {
loggerFactory.when { LoggerFactory.getLogger(anyString()) }
.thenReturn(logger)
this.doDslAssertionTest('<% assert logger; logger.info("Hello, World!") %>')
verify(logger).info('Hello, World!')
}
}
@Test
default void partsAvailable() {
this.doDslAssertionTest("<% assert parts != null && parts instanceof ${ EmbeddablePartsMap.name } %>")
}
@Test
default void partAvailableAndRenderable(@Mock PartRenderer partRenderer) {
when(partRenderer.render(any(), any(), any(), any()))
.thenReturn(new Tuple2<>([], 'Hello, World!'))
def part = new Part(
'test.gsp',
new PartType([], partRenderer),
'Hello, World!'
)
this.doDslRenderTest(
'Hello, World!',
'<%= parts["test.gsp"].render() %>',
getRenderContext(parts: [part])
)
}
@Test
default void siteSpecAvailable() {
this.doDslAssertionTest(
"<% assert siteSpec && siteSpec instanceof ${ SiteSpec.name } %>"
)
}
@Test
default void siteSpecRendersCorrectValues() {
def siteSpec = new SiteSpec('Test Site', 'https://test.com')
this.doDslRenderTest(
'Test Site https://test.com',
'<%= siteSpec.name + " " + siteSpec.baseUrl %>',
getRenderContext(siteSpec: siteSpec)
)
}
@Test
default void sourcePathAvailable() {
this.doDslAssertionTest(
'<% assert sourcePath && sourcePath instanceof String %>',
getRenderContext(sourcePath: 'test.md')
)
}
@Test
default void sourcePathRendersCorrectValue() {
this.doDslRenderTest(
'test.md',
'<%= sourcePath %>',
getRenderContext(sourcePath: 'test.md')
)
}
@Test
default void tagBuilderAvailable() {
this.doDslAssertionTest("<% assert tagBuilder && tagBuilder instanceof ${ TagBuilder.name } %>")
}
@Test
default void targetPathAvailable() {
this.doDslAssertionTest(
'<% assert targetPath && targetPath instanceof String %>',
getRenderContext(targetPath: 'test/test.html')
)
}
@Test
default void targetPathRendersCorrectValue() {
this.doDslRenderTest(
'test/test.html',
'<%= targetPath %>',
getRenderContext(targetPath: 'test/test.html')
)
}
@Test
default void tasksAvailable() {
this.doDslAssertionTest("<% assert tasks != null && tasks instanceof ${ TaskContainer.name } %>")
}
@Test
default void tasksFind() {
def task = new TextToHtmlFileTask(
'testTask',
blankText(),
new HtmlFileOutput(
new File('test.html'),
'test.html',
{ '' }
)
)
this.doDslRenderTest(
'test.html',
'<%= tasks.find { it.name == "testTask" }.output.htmlPath %>',
getRenderContext(
tasks: new TaskContainer([task]),
taskTypes: new TaskTypeContainer([TextToHtmlFileTask.TYPE])
)
)
}
@Test
default void taskTypesAvailable() {
this.doDslAssertionTest(
"<% assert taskTypes != null && taskTypes instanceof ${ TaskTypeContainer.name } %>"
)
}
@Test
default void taskFindAllByType() {
def t0 = blankTextToHtmlFileTask()
def t1 = blankSpecialPageToHtmlFileTask()
this.doDslAssertionTest(
'<% assert tasks.size() == 2 && ' +
'tasks.findAllByType(taskTypes.textToHtmlFile).size() == 1 &&' +
'tasks.findAllByType(taskTypes.specialPageToHtmlFile).size() == 1 %>',
getRenderContext(
tasks: new TaskContainer([t0, t1]),
taskTypes: new TaskTypeContainer([TextToHtmlFileTask.TYPE, SpecialPageToHtmlFileTask.TYPE])
)
)
}
@Test
default void textsAvailable() {
this.doDslAssertionTest(
"<% assert texts != null && texts instanceof ${ EmbeddableTextsCollection.name } %>"
)
}
@Test
default void textsTextAvailableAndRenderable() {
def testText = renderableTextWithPath('Hello, World!', 'test.md')
this.doDslRenderTest(
'Hello, World!',
'<%= texts.find { it.path == "test.md" }.render() %>',
getRenderContext(texts: [testText])
)
}
@Test
default void urlBuilderAvailable() {
this.doDslAssertionTest("<% assert urlBuilder && urlBuilder instanceof ${ UrlBuilder.name } %>")
}
@Test
default void urlBuilderCorrectlyConfigured() {
this.doDslRenderTest(
'../images/test.jpg',
'<%= urlBuilder.relative("images/test.jpg") %>',
getRenderContext(targetPath: 'test/test.html')
)
}
}

View File

@ -0,0 +1,16 @@
package com.jessebrault.ssg.specialpage
import static org.mockito.Mockito.mock
final class SpecialPageMocks {
static SpecialPage blankSpecialPage() {
def renderer = mock(SpecialPageRenderer)
new SpecialPage(
'',
'',
new SpecialPageType([], renderer)
)
}
}

View File

@ -0,0 +1,19 @@
package com.jessebrault.ssg.task
import static com.jessebrault.ssg.specialpage.SpecialPageMocks.blankSpecialPage
final class SpecialPageToHtmlFileTaskMocks {
static SpecialPageToHtmlFileTask blankSpecialPageToHtmlFileTask() {
new SpecialPageToHtmlFileTask(
'',
blankSpecialPage(),
new HtmlFileOutput(
new File(''),
'',
{ '' }
)
)
}
}

View File

@ -0,0 +1,19 @@
package com.jessebrault.ssg.task
import static com.jessebrault.ssg.text.TextMocks.blankText
final class TextToHtmlFileTaskMocks {
static TextToHtmlFileTask blankTextToHtmlFileTask() {
new TextToHtmlFileTask(
'',
blankText(),
new HtmlFileOutput(
new File(''),
'',
{ '' }
)
)
}
}

View File

@ -0,0 +1,47 @@
package com.jessebrault.ssg.testutil
import com.jessebrault.ssg.Diagnostic
import com.jessebrault.ssg.Result
import com.jessebrault.ssg.text.ExcerptGetter
import groovy.transform.stc.ClosureParams
import groovy.transform.stc.FirstParam
import static org.junit.jupiter.api.Assertions.assertInstanceOf
import static org.junit.jupiter.api.Assertions.assertTrue
class DiagnosticsUtil {
static Closure<String> getDiagnosticsMessageSupplier(Collection<Diagnostic> diagnostics) {
return {
diagnostics.collect {
def writer = new StringWriter()
it.exception.printStackTrace(new PrintWriter(writer))
def stackTrace = writer.toString()
"$it.message\n$stackTrace"
}.join('\n')
}
}
static void assertEmptyDiagnostics(Tuple2<Collection<Diagnostic>, ?> result) {
assertTrue(result.v1.isEmpty(), getDiagnosticsMessageSupplier(result.v1))
}
static void assertEmptyDiagnostics(Result<?> result) {
assertTrue(!result.hasDiagnostics(), getDiagnosticsMessageSupplier(result.diagnostics))
}
static <E extends Exception> void assertDiagnosticException(
Class<E> expectedException,
Diagnostic diagnostic,
@ClosureParams(FirstParam.FirstGenericType)
Closure<Void> additionalAssertions = null
) {
assertInstanceOf(expectedException, diagnostic.exception, {
"Incorrect diagnostic exception class; message: ${ diagnostic.message }"
})
if (additionalAssertions) {
additionalAssertions(diagnostic.exception)
}
}
}

View File

@ -0,0 +1,26 @@
package com.jessebrault.ssg.testutil
import com.jessebrault.ssg.Config
import com.jessebrault.ssg.SiteSpec
import com.jessebrault.ssg.renderer.RenderContext
import com.jessebrault.ssg.task.Task
import com.jessebrault.ssg.task.TaskContainer
import com.jessebrault.ssg.task.TaskTypeContainer
class RenderContextUtil {
static RenderContext getRenderContext(Map args = null) {
new RenderContext(
args?.config as Config ?: new Config(),
args?.siteSpec as SiteSpec ?: new SiteSpec('', ''),
args?.globals as Map ?: [:],
args?.texts as Collection ?: [],
args?.parts as Collection ?: [],
args?.sourcePath as String ?: '',
args?.targetPath as String ?: '',
args?.tasks as TaskContainer ?: new TaskContainer(),
args?.taskTypes as TaskTypeContainer ?: new TaskTypeContainer()
)
}
}

View File

@ -0,0 +1,39 @@
package com.jessebrault.ssg.text
import static org.mockito.ArgumentMatchers.any
import static org.mockito.Mockito.mock
import static org.mockito.Mockito.when
class TextMocks {
static Text blankText() {
def textRenderer = mock(TextRenderer)
def frontMatterGetter = mock(FrontMatterGetter)
def excerptGetter = mock(ExcerptGetter)
new Text('', '', new TextType([], textRenderer, frontMatterGetter, excerptGetter))
}
static Text renderableText(String text) {
def textRenderer = mock(TextRenderer)
when(textRenderer.render(any(), any())).thenReturn(new Tuple2<>([], text))
def frontMatterGetter = mock(FrontMatterGetter)
def excerptGetter = mock(ExcerptGetter)
new Text(text, '', new TextType([], textRenderer, frontMatterGetter, excerptGetter))
}
static Text textWithPath(String path) {
def textRenderer = mock(TextRenderer)
def frontMatterGetter = mock(FrontMatterGetter)
def excerptGetter = mock(ExcerptGetter)
new Text('', path, new TextType([], textRenderer, frontMatterGetter, excerptGetter))
}
static Text renderableTextWithPath(String text, String path) {
def textRenderer = mock(TextRenderer)
when(textRenderer.render(any(), any())).thenReturn(new Tuple2<>([], text))
def frontMatterGetter = mock(FrontMatterGetter)
def excerptGetter = mock(ExcerptGetter)
new Text(text, path, new TextType([], textRenderer, frontMatterGetter, excerptGetter))
}
}

View File

@ -0,0 +1,49 @@
package com.jessebrault.ssg.url
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')
)
}
}