meatyInitAndBuild test written; added Jsoup; notion of baseDir; ResourceUtil.

This commit is contained in:
JesseBrault0709 2023-05-02 17:55:22 +02:00
parent 5a70f9c91c
commit 4c920fb485
21 changed files with 220 additions and 152 deletions

View File

@ -20,9 +20,14 @@ Here will be kept all of the various todos for this project, organized by releas
- [ ] Add a way for CLI to choose a build to do, or multiple builds, defaulting to 'default' if it exists. - [ ] Add a way for CLI to choose a build to do, or multiple builds, defaulting to 'default' if it exists.
- [ ] Write lots of tests for buildscript dsl, etc. - [ ] Write lots of tests for buildscript dsl, etc.
- [ ] Explore `base` in buildScript dsl. - [ ] Explore `base` in buildScript dsl.
- Get rid of `allBuilds` concept, and replace it with composable/concat-able builds. In the dsl we could have a notion of `abstractBuild` which can be 'extended' (i.e., on the left side of a concat operation) but not actually run (since it doesn't have a name).
- `OutputDir` should be concat-able, such that the left is the *base* for the right.
- `OutputDirFunctions.concat` should be concat-able as well, such that both are `BiFunction<OutputDir, Build, OutputDir>`, and the output of the left is the input of the right.
### Fix ### Fix
- [ ] Update CHANGELOG to reflect the gsp-dsl changes. - [ ] Update CHANGELOG to reflect the gsp-dsl changes.
- [ ] `taskTypes` gone, use class name instead
- [ ] introduction of `models`
- [x] Change most instances of `Closure<Void>` to `Closure<?>` to help with IDE expectations. - [x] Change most instances of `Closure<Void>` to `Closure<?>` to help with IDE expectations.
## Finished ## Finished

View File

@ -15,6 +15,9 @@ dependencies {
// https://mvnrepository.com/artifact/org.commonmark/commonmark-ext-yaml-front-matter // https://mvnrepository.com/artifact/org.commonmark/commonmark-ext-yaml-front-matter
implementation 'org.commonmark:commonmark-ext-yaml-front-matter:0.21.0' implementation 'org.commonmark:commonmark-ext-yaml-front-matter:0.21.0'
// https://mvnrepository.com/artifact/org.jsoup/jsoup
implementation 'org.jsoup:jsoup:1.16.1'
} }
jar { jar {

View File

@ -4,6 +4,7 @@ import com.jessebrault.ssg.buildscript.Build
import com.jessebrault.ssg.buildscript.BuildScriptConfiguratorFactory import com.jessebrault.ssg.buildscript.BuildScriptConfiguratorFactory
import com.jessebrault.ssg.buildscript.BuildScriptRunner import com.jessebrault.ssg.buildscript.BuildScriptRunner
import com.jessebrault.ssg.util.Diagnostic import com.jessebrault.ssg.util.Diagnostic
import org.jetbrains.annotations.Nullable
import org.slf4j.Logger import org.slf4j.Logger
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.slf4j.Marker import org.slf4j.Marker
@ -30,7 +31,7 @@ final class BuildScriptBasedStaticSiteGenerator implements StaticSiteGenerator {
BuildScriptBasedStaticSiteGenerator( BuildScriptBasedStaticSiteGenerator(
BuildScriptRunner buildScriptRunner, BuildScriptRunner buildScriptRunner,
BuildScriptConfiguratorFactory configuratorFactory, BuildScriptConfiguratorFactory configuratorFactory,
File buildScript = null, @Nullable File buildScript = null,
Collection<File> buildSrcDirs = [], Collection<File> buildSrcDirs = [],
Map<String, Object> scriptArgs = [:] Map<String, Object> scriptArgs = [:]
) { ) {

View File

@ -12,12 +12,21 @@ import com.jessebrault.ssg.template.TemplateTypes
import com.jessebrault.ssg.template.TemplatesProviders import com.jessebrault.ssg.template.TemplatesProviders
import com.jessebrault.ssg.text.TextTypes import com.jessebrault.ssg.text.TextTypes
import com.jessebrault.ssg.text.TextsProviders import com.jessebrault.ssg.text.TextsProviders
import com.jessebrault.ssg.util.ExtensionUtil
import com.jessebrault.ssg.util.Result import com.jessebrault.ssg.util.Result
import groovy.transform.EqualsAndHashCode
import groovy.transform.NullCheck
import groovy.transform.TupleConstructor
import java.util.function.Consumer import java.util.function.Consumer
@TupleConstructor(includeFields = true, defaults = false)
@NullCheck(includeGenerated = true)
@EqualsAndHashCode(includeFields = true)
final class DefaultBuildScriptConfiguratorFactory implements BuildScriptConfiguratorFactory { final class DefaultBuildScriptConfiguratorFactory implements BuildScriptConfiguratorFactory {
private final File baseDir
@Override @Override
Consumer<BuildScriptBase> get() { Consumer<BuildScriptBase> get() {
return { return {
@ -30,10 +39,10 @@ final class DefaultBuildScriptConfiguratorFactory implements BuildScriptConfigur
} }
providers { types -> providers { types ->
texts(TextsProviders.from(new File('texts'), types.textTypes)) texts(TextsProviders.from(new File(this.baseDir, 'texts'), types.textTypes))
pages(PagesProviders.from(new File('pages'), types.pageTypes)) pages(PagesProviders.from(new File(this.baseDir, 'pages'), types.pageTypes))
templates(TemplatesProviders.from(new File('templates'), types.templateTypes)) templates(TemplatesProviders.from(new File(this.baseDir, 'templates'), types.templateTypes))
parts(PartsProviders.of(new File('parts'), types.partTypes)) parts(PartsProviders.of(new File(this.baseDir, 'parts'), types.partTypes))
} }
taskFactories { sourceProviders -> taskFactories { sourceProviders ->
@ -48,7 +57,11 @@ final class DefaultBuildScriptConfiguratorFactory implements BuildScriptConfigur
def templateValue = frontMatterResult.get().get('template') def templateValue = frontMatterResult.get().get('template')
if (templateValue) { if (templateValue) {
def template = templates.find { it.path == templateValue } def template = templates.find { it.path == templateValue }
return Result.of(new TextToHtmlSpec(it, template, it.path)) return Result.of(new TextToHtmlSpec(
it,
template,
ExtensionUtil.stripExtension(it.path) + '.html'
))
} else { } else {
return null return null
} }
@ -67,7 +80,7 @@ final class DefaultBuildScriptConfiguratorFactory implements BuildScriptConfigur
} }
it.build('default') { it.build('default') {
outputDir = new File('build') outputDir = new File(this.baseDir, 'build')
} }
} }
} }

View File

@ -6,6 +6,7 @@ import com.jessebrault.ssg.util.Diagnostic
import com.jessebrault.ssg.util.Result import com.jessebrault.ssg.util.Result
import groovy.transform.EqualsAndHashCode import groovy.transform.EqualsAndHashCode
import groovy.transform.NullCheck import groovy.transform.NullCheck
import org.jsoup.Jsoup
@NullCheck @NullCheck
@EqualsAndHashCode @EqualsAndHashCode
@ -29,9 +30,12 @@ abstract class AbstractHtmlTask extends AbstractTask implements HtmlTask {
transformResult.diagnostics transformResult.diagnostics
} else { } else {
def content = transformResult.get() def content = transformResult.get()
def document = Jsoup.parse(content)
document.outputSettings().indentAmount(4)
def formatted = document.toString()
def target = new File(this.buildDir, this.path) def target = new File(this.buildDir, this.path)
target.createParentDirectories() target.createParentDirectories()
target.write(content) target.write(formatted)
[] []
} }
} }

View File

@ -0,0 +1,27 @@
package com.jessebrault.ssg.util
final class ResourceUtil {
static void copyResourceToWriter(String name, Writer target) {
ResourceUtil.getClassLoader().getResourceAsStream(name).withReader {
it.transferTo(target)
}
}
static void copyResourceToFile(String name, File target) {
ResourceUtil.getClassLoader().getResourceAsStream(name).withReader { Reader reader ->
target.withWriter { Writer writer ->
reader.transferTo(writer)
}
}
}
static String loadResourceAsString(String name) {
def sw = new StringWriter()
copyResourceToWriter(name, sw)
sw.toString()
}
private ResourceUtil() {}
}

View File

@ -3,16 +3,16 @@ package com.jessebrault.ssg
import com.jessebrault.ssg.buildscript.BuildScriptBase import com.jessebrault.ssg.buildscript.BuildScriptBase
import com.jessebrault.ssg.buildscript.BuildScriptConfiguratorFactory import com.jessebrault.ssg.buildscript.BuildScriptConfiguratorFactory
import com.jessebrault.ssg.buildscript.SimpleBuildScriptRunner import com.jessebrault.ssg.buildscript.SimpleBuildScriptRunner
import com.jessebrault.ssg.util.FileUtil import com.jessebrault.ssg.util.ResourceUtil
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.slf4j.Logger import org.slf4j.Logger
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import java.util.function.Consumer import java.util.function.Consumer
import static com.jessebrault.ssg.util.FileAssertions.assertFileStructureAndContents
import static org.junit.jupiter.api.Assertions.assertTrue import static org.junit.jupiter.api.Assertions.assertTrue
// TODO: everything is working, now to expand this and refactor out common test code.
final class BuildScriptBasedStaticSiteGeneratorTests { final class BuildScriptBasedStaticSiteGeneratorTests {
private static final Logger logger = LoggerFactory.getLogger(BuildScriptBasedStaticSiteGeneratorTests) private static final Logger logger = LoggerFactory.getLogger(BuildScriptBasedStaticSiteGeneratorTests)
@ -22,7 +22,7 @@ final class BuildScriptBasedStaticSiteGeneratorTests {
def sourceDir = File.createTempDir() def sourceDir = File.createTempDir()
def buildScript = new File(sourceDir, 'build.groovy') def buildScript = new File(sourceDir, 'build.groovy')
FileUtil.copyResourceToFile('oneTextAndTemplate.groovy', buildScript) ResourceUtil.copyResourceToFile('oneTextAndTemplate.groovy', buildScript)
new FileTreeBuilder(sourceDir).tap { new FileTreeBuilder(sourceDir).tap {
dir('texts') { dir('texts') {
@ -52,9 +52,11 @@ final class BuildScriptBasedStaticSiteGeneratorTests {
}) })
def expectedBase = File.createTempDir() def expectedBase = File.createTempDir()
new File(expectedBase, 'hello.html').write('<p>Hello, World!</p>\n') new File(expectedBase, 'hello.html').tap {
ResourceUtil.copyResourceToFile('outputs/hello.html', it)
}
FileUtil.assertFileStructureAndContents(expectedBase, new File(sourceDir, 'build')) assertFileStructureAndContents(expectedBase, new File(sourceDir, 'build'))
} }
} }

View File

@ -0,0 +1,26 @@
package com.jessebrault.ssg.util
import org.junit.jupiter.api.Test
import static ResourceUtil.copyResourceToFile
import static ResourceUtil.copyResourceToWriter
import static org.junit.jupiter.api.Assertions.assertEquals
final class ResourceUtilTests {
@Test
void copyResourceToWriterTest() {
def writer = new StringWriter()
copyResourceToWriter('testResource.txt', writer)
assertEquals('Hello, World!', writer.toString())
}
@Test
void copyResourceToTargetFileTest() {
def tempDir = File.createTempDir()
def target = new File(tempDir, 'testResource.txt')
copyResourceToFile('testResource.txt', target)
assertEquals('Hello, World!', target.text)
}
}

View File

@ -0,0 +1,6 @@
<html>
<head></head>
<body>
<p>Hello, World!</p>
</body>
</html>

View File

@ -0,0 +1 @@
Hello, World!

View File

@ -7,9 +7,9 @@ import java.nio.file.Path
import static org.junit.jupiter.api.Assertions.assertEquals import static org.junit.jupiter.api.Assertions.assertEquals
final class FileUtil { final class FileAssertions {
private static final Logger logger = LoggerFactory.getLogger(FileUtil) private static final Logger logger = LoggerFactory.getLogger(ResourceUtil)
private static Map<String, Object> fileToMap(File file) { private static Map<String, Object> fileToMap(File file) {
[ [
@ -46,20 +46,6 @@ final class FileUtil {
} }
} }
static void copyResourceToWriter(String name, Writer target) { private FileAssertions() {}
FileUtil.getClassLoader().getResourceAsStream(name).withReader {
it.transferTo(target)
}
}
static void copyResourceToFile(String name, File target) {
FileUtil.getClassLoader().getResourceAsStream(name).withReader { Reader reader ->
target.withWriter { Writer writer ->
reader.transferTo(writer)
}
}
}
private FileUtil() {}
} }

View File

@ -1,42 +0,0 @@
package com.jessebrault.ssg.util
import org.junit.jupiter.api.Test
import static com.jessebrault.ssg.util.FileUtil.assertFileStructureAndContents
import static com.jessebrault.ssg.util.FileUtil.copyResourceToFile
import static com.jessebrault.ssg.util.FileUtil.copyResourceToWriter
import static org.junit.jupiter.api.Assertions.assertEquals
final class FileUtilTests {
@Test
void sameStructureAndContentsTest() {
def b0 = File.createTempDir()
def b1 = File.createTempDir()
[b0, b1].each {
new FileTreeBuilder(it).tap {
file('testFile', 'test content')
dir('testDir') {
file('testNestedFile', 'test content')
}
}
}
assertFileStructureAndContents(b0, b1)
}
@Test
void copyResourceToWriterTest() {
def writer = new StringWriter()
copyResourceToWriter('testResource.txt', writer)
assertEquals('Hello, World!', writer.toString())
}
@Test
void copyResourceToTargetFileTest() {
def tempDir = File.createTempDir()
def target = new File(tempDir, 'testResource.txt')
copyResourceToFile('testResource.txt', target)
assertEquals('Hello, World!', target.text)
}
}

View File

@ -12,32 +12,38 @@ abstract class AbstractBuildCommand extends AbstractSubCommand {
private static final Logger logger = LogManager.getLogger(AbstractBuildCommand) private static final Logger logger = LogManager.getLogger(AbstractBuildCommand)
@CommandLine.Option( @CommandLine.Option(
names = ['-s', '--script', '--buildScript'], names = '--baseDir',
description = 'The build script file to execute.' description = 'The base directory for all components.'
) )
protected File buildScript = null File baseDir = new File('.')
@CommandLine.Option(
names = ['-s', '--script', '--buildScript'],
description = 'The build script file to execute, relative to the baseDir.'
)
File buildScript = new File('ssgBuilds.groovy')
@CommandLine.Option( @CommandLine.Option(
names = '--scriptArgs', names = '--scriptArgs',
description = 'Named argument(s) to pass directly to the build script.', description = 'Named argument(s) to pass directly to the build script.',
split = ',' split = ','
) )
protected Map<String, String> scriptArgs = [:] Map<String, String> scriptArgs = [:]
@CommandLine.Option( @CommandLine.Option(
names = '--buildSrcDirs', names = '--buildSrcDirs',
description = 'Path(s) to director(ies) containing Groovy classes and scripts which should be visible to the main build script.', description = 'Path(s) to director(ies) containing Groovy classes and scripts which should be visible to the main build script, relative to the baseDir.',
split = ',', split = ',',
paramLabel = 'buildSrcDir' paramLabel = 'buildSrcDir'
) )
protected Collection<File> buildSrcDirs = [new File('buildSrc')] Collection<File> buildSrcDirs = [new File('buildSrc')]
@CommandLine.Option( @CommandLine.Option(
names = ['-b', '--build'], names = ['-b', '--build'],
description = 'The name of a build to execute.', description = 'The name of a build to execute.',
paramLabel = 'buildName' paramLabel = 'buildName'
) )
protected Collection<String> requestedBuilds = ['default'] Collection<String> requestedBuilds = ['default']
protected StaticSiteGenerator staticSiteGenerator = null protected StaticSiteGenerator staticSiteGenerator = null
@ -47,9 +53,11 @@ abstract class AbstractBuildCommand extends AbstractSubCommand {
if (this.staticSiteGenerator == null) { if (this.staticSiteGenerator == null) {
this.staticSiteGenerator = new BuildScriptBasedStaticSiteGenerator( this.staticSiteGenerator = new BuildScriptBasedStaticSiteGenerator(
new SimpleBuildScriptRunner(), new SimpleBuildScriptRunner(),
new DefaultBuildScriptConfiguratorFactory(), new DefaultBuildScriptConfiguratorFactory(this.baseDir),
this.buildScript, this.buildScript == new File('ssgBuilds.groovy') || this.buildScript.exists()
this.buildSrcDirs, ? new File(this.baseDir, this.buildScript.path)
: null,
this.buildSrcDirs.collect { new File(this.baseDir, it.path) },
this.scriptArgs this.scriptArgs
) )
} }

View File

@ -14,7 +14,7 @@ final class SsgBuild extends AbstractBuildCommand {
private static final Logger logger = LogManager.getLogger(SsgBuild) private static final Logger logger = LogManager.getLogger(SsgBuild)
@Override @Override
Integer doSubCommand() { protected Integer doSubCommand() {
logger.traceEntry() logger.traceEntry()
def result = 0 def result = 0
this.requestedBuilds.each { this.requestedBuilds.each {

View File

@ -4,6 +4,8 @@ import org.apache.logging.log4j.LogManager
import org.apache.logging.log4j.Logger import org.apache.logging.log4j.Logger
import picocli.CommandLine import picocli.CommandLine
import static com.jessebrault.ssg.util.ResourceUtil.copyResourceToFile
@CommandLine.Command( @CommandLine.Command(
name = 'init', name = 'init',
mixinStandardHelpOptions = true, mixinStandardHelpOptions = true,
@ -11,44 +13,62 @@ import picocli.CommandLine
) )
final class SsgInit extends AbstractSubCommand { final class SsgInit extends AbstractSubCommand {
static void init(File targetDir, boolean meaty) {
new FileTreeBuilder(targetDir).with {
dir('texts') {
if (meaty) {
file('hello.md').tap {
copyResourceToFile('hello.md', it)
}
}
}
dir('pages') {
if (meaty) {
file('page.gsp').tap {
copyResourceToFile('page.gsp', it)
}
}
}
dir('templates') {
if (meaty) {
file('hello.gsp').tap {
copyResourceToFile('hello.gsp', it)
}
}
}
dir('parts') {
if (meaty) {
file('head.gsp').tap {
copyResourceToFile('head.gsp', it)
}
}
}
if (meaty) {
file('ssgBuilds.groovy').tap {
copyResourceToFile('ssgBuilds.groovy', it)
}
} else {
file('ssgBuilds.groovy').tap {
copyResourceToFile('ssgBuildsBasic.groovy', it)
}
}
}
}
private static final Logger logger = LogManager.getLogger(SsgInit) private static final Logger logger = LogManager.getLogger(SsgInit)
@CommandLine.Option(names = ['-s', '--skeleton'], description = 'Include some basic files in the generated project.') @CommandLine.Option(names = ['-m', '--meaty'], description = 'Include some basic files in the generated project.')
boolean withSkeletonFiles boolean meaty
@CommandLine.Option(names = '--targetDir', description = 'The directory in which to generate the project')
File target = new File('.')
@Override @Override
Integer doSubCommand() { Integer doSubCommand() {
logger.traceEntry() logger.traceEntry()
new FileTreeBuilder().with { init(this.target, this.meaty)
// Generate dirs
dir('texts') {
if (this.withSkeletonFiles) {
file('hello.md', this.getClass().getResource('/hello.md').text)
}
}
dir('templates') {
if (this.withSkeletonFiles) {
file('hello.gsp', this.getClass().getResource('/hello.gsp').text)
}
}
dir('parts') {
if (this.withSkeletonFiles) {
file('head.gsp', this.getClass().getResource('/head.gsp').text)
}
}
dir('specialPages') {
if (this.withSkeletonFiles) {
file('page.gsp', this.getClass().getResource('/page.gsp').text)
}
}
// Generate ssgBuilds.groovy
if (this.withSkeletonFiles) {
file('ssgBuilds.groovy', this.getClass().getResource('/ssgBuilds.groovy').text)
} else {
file('ssgBuilds.groovy', this.getClass().getResource('/ssgBuildsBasic.groovy').text)
}
}
logger.traceExit(0) logger.traceExit(0)
} }
} }

View File

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

View File

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

View File

@ -1,6 +1,6 @@
package com.jessebrault.ssg package com.jessebrault.ssg
import org.junit.jupiter.api.Disabled import com.jessebrault.ssg.util.ResourceUtil
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import static org.junit.jupiter.api.Assertions.assertEquals import static org.junit.jupiter.api.Assertions.assertEquals
@ -9,44 +9,36 @@ import static org.junit.jupiter.api.Assertions.assertTrue
final class StaticSiteGeneratorCliIntegrationTests { final class StaticSiteGeneratorCliIntegrationTests {
@Test @Test
@Disabled('until we figure out how to do the base dir arg') void meatyInitAndBuild() {
void defaultConfiguration() { def tempDir = File.createTempDir()
def partsDir = new File('parts').tap { SsgInit.init(tempDir, true)
mkdir()
deleteOnExit()
}
def specialPagesDir = new File('specialPages').tap {
mkdir()
deleteOnExit()
}
def templatesDir = new File('templatesDir').tap {
mkdir()
deleteOnExit()
}
def textsDir = new File('textsDir').tap {
mkdir()
deleteOnExit()
}
new File(partsDir, 'part.gsp').write('<%= binding.test %>') def ssgBuild = new SsgBuild().tap {
new File(specialPagesDir, 'page.gsp').write('<%= parts.part.render([test: "Greetings!"]) %>') it.cli = new StaticSiteGeneratorCli().tap {
new File(templatesDir, 'template.gsp').write('<%= text %>') it.logLevel = new StaticSiteGeneratorCli.LogLevel().tap {
new File(textsDir, 'text.md').write('---\ntemplate: template.gsp\n---\n**Hello, World!**') it.trace = true
}
StaticSiteGeneratorCli.main('--trace') }
it.baseDir = tempDir
def buildDir = new File('build').tap { it.requestedBuilds = ['production']
deleteOnExit()
} }
assertEquals(0, ssgBuild.call())
def buildDir = new File(tempDir, 'build')
assertTrue(buildDir.exists()) assertTrue(buildDir.exists())
assertTrue(buildDir.directory)
def textHtml = new File(buildDir, 'text.html') def textOutputFile = new File(buildDir, 'hello.html')
assertTrue(textHtml.exists()) assertTrue(textOutputFile.exists())
assertEquals('<p><strong>Hello, World!</strong></p>\n', textHtml.text) assertTrue(textOutputFile.file)
def pageOutputFile = new File(buildDir, 'page.html')
assertTrue(pageOutputFile.exists())
assertTrue(pageOutputFile.file)
def specialPage = new File(buildDir, 'specialPage.html') def expectedText = ResourceUtil.loadResourceAsString('hello.html')
assertTrue(specialPage.exists()) def expectedPage = ResourceUtil.loadResourceAsString('page.html')
assertEquals('Greetings!', specialPage.text) assertEquals(expectedText, textOutputFile.text)
assertEquals(expectedPage, pageOutputFile)
} }
} }

View File

@ -0,0 +1,8 @@
<html>
<head>
<title>My Site: Greeting</title>
</head>
<body>
<h1>Hello, World!</h1>
</body>
</html>

View File

@ -0,0 +1,8 @@
<html>
<head>
<title>My Site: Page</title>
</head>
<body>
<h1>Hello, World!</h1>
</body>
</html>