Added specialPages and some better error handling.

This commit is contained in:
JesseBrault0709 2023-01-05 09:38:31 -06:00
parent b293bb856b
commit e6b603fd69
21 changed files with 352 additions and 61 deletions

View File

@ -3,6 +3,9 @@ package com.jessebrault.ssg
import com.jessebrault.ssg.part.GspPartRenderer
import com.jessebrault.ssg.part.PartFilePartsProvider
import com.jessebrault.ssg.part.PartType
import com.jessebrault.ssg.specialpage.GspSpecialPageRenderer
import com.jessebrault.ssg.specialpage.SpecialPageFileSpecialPagesProvider
import com.jessebrault.ssg.specialpage.SpecialPageType
import com.jessebrault.ssg.template.GspTemplateRenderer
import com.jessebrault.ssg.template.TemplateType
import com.jessebrault.ssg.template.TemplateFileTemplatesProvider
@ -17,19 +20,23 @@ class StaticSiteGeneratorCli {
def markdownText = new TextType(['.md'], new MarkdownTextRenderer(), new MarkdownFrontMatterGetter())
def gspTemplate = new TemplateType(['.gsp'], new GspTemplateRenderer())
def gspPart = new PartType(['.gsp'], new GspPartRenderer())
def gspSpecialPage = new SpecialPageType(['.gsp'], new GspSpecialPageRenderer())
def config = new Config(
textTypes: [markdownText],
templateTypes: [gspTemplate],
partTypes: [gspPart],
specialPageTypes: [gspSpecialPage],
textsDir: new File('texts'),
templatesDir: new File('templates'),
partsDir: new File('parts'),
specialPagesDir: new File('specialPages'),
textsProviderGetter: { Config config -> new TextFileTextsProvider(config.textTypes, config.textsDir) },
templatesProviderGetter: { Config config -> new TemplateFileTemplatesProvider(config.templateTypes, config.templatesDir) },
partsProviderGetter: { Config config -> new PartFilePartsProvider(config.partTypes, config.partsDir) }
partsProviderGetter: { Config config -> new PartFilePartsProvider(config.partTypes, config.partsDir) },
specialPagesProviderGetter: { Config config -> new SpecialPageFileSpecialPagesProvider(config.specialPageTypes, config.specialPagesDir) }
)
def ssg = new SimpleStaticSiteGenerator(config)

View File

@ -2,6 +2,8 @@ package com.jessebrault.ssg
import com.jessebrault.ssg.part.PartType
import com.jessebrault.ssg.part.PartsProvider
import com.jessebrault.ssg.specialpage.SpecialPageType
import com.jessebrault.ssg.specialpage.SpecialPagesProvider
import com.jessebrault.ssg.template.TemplateType
import com.jessebrault.ssg.template.TemplatesProvider
import com.jessebrault.ssg.text.TextType
@ -19,12 +21,15 @@ class Config {
Collection<TextType> textTypes
Collection<TemplateType> templateTypes
Collection<PartType> partTypes
Collection<SpecialPageType> specialPageTypes
File textsDir
File templatesDir
File partsDir
File specialPagesDir
Function<Config, TextsProvider> textsProviderGetter
Function<Config, TemplatesProvider> templatesProviderGetter
Function<Config, PartsProvider> partsProviderGetter
Function<Config, SpecialPagesProvider> specialPagesProviderGetter
}

View File

@ -1,6 +1,7 @@
package com.jessebrault.ssg
import com.jessebrault.ssg.part.PartsProvider
import com.jessebrault.ssg.specialpage.SpecialPagesProvider
import com.jessebrault.ssg.template.TemplatesProvider
import com.jessebrault.ssg.text.TextsProvider
import org.slf4j.Logger
@ -18,24 +19,41 @@ class SimpleStaticSiteGenerator implements StaticSiteGenerator {
private final TextsProvider textsProvider
private final TemplatesProvider templatesProvider
private final PartsProvider partsProvider
private final SpecialPagesProvider specialPagesProvider
SimpleStaticSiteGenerator(Config config) {
this.config = config
this.textsProvider = config.textsProviderGetter.apply(config)
this.templatesProvider = config.templatesProviderGetter.apply(config)
this.partsProvider = config.partsProviderGetter.apply(config)
this.specialPagesProvider = config.specialPagesProviderGetter.apply(config)
}
@Override
void generate(File buildDir) {
logger.trace(enter, 'buildDir: {}', buildDir)
// Get all texts, templates, and parts
// Get all texts, templates, parts, and specialPages
def texts = this.textsProvider.getTextFiles()
def templates = this.templatesProvider.getTemplates()
def parts = this.partsProvider.getParts()
def specialPages = this.specialPagesProvider.getSpecialPages()
logger.debug('texts: {}, templates: {}, parts: {}', texts, templates, parts)
logger.debug('\n\ttexts: {}\n\ttemplates: {}\n\tparts: {}\n\tspecialPages: {}', texts, templates, parts, specialPages)
// Define output function
def outputPage = { String path, String result ->
def outFile = new File(buildDir, path + '.html')
if (outFile.exists()) {
logger.info('outFile {} already exists, deleting', outFile)
outFile.delete()
}
outFile.createParentDirectories()
logger.info('writing result to {}', outFile)
outFile.write(result)
}
// Generate pages from each text
texts.each {
logger.info('processing text: {}', it.path)
@ -53,6 +71,9 @@ class SimpleStaticSiteGenerator implements StaticSiteGenerator {
if (desiredTemplate == null) {
throw new IllegalArgumentException('in text ' + it.path + ' frontMatter.template must not be null')
}
if (desiredTemplate.isEmpty() || desiredTemplate.isBlank()) {
throw new IllegalArgumentException('in text ' + it.path + ' frontMatter.template must not be empty, blank, or missing')
}
def template = templates.find {
it.relativePath == desiredTemplate
}
@ -66,15 +87,20 @@ class SimpleStaticSiteGenerator implements StaticSiteGenerator {
logger.debug('result: {}', result)
// Output the result to the outfile, an .html file
def outFile = new File(buildDir, it.path + '.html')
if (outFile.exists()) {
logger.info('outFile {} already exists, deleting', outFile)
outFile.delete()
}
outFile.createParentDirectories()
logger.info('writing result to {}', outFile)
outFile.write(result)
outputPage(it.path, result)
}
// Generate special pages
specialPages.each {
logger.info('processing specialPage: {}', it)
def result = it.type.renderer.render(it.text, texts, parts)
logger.info('result: {}', result)
// Output result to file
outputPage(it.path, result)
}
logger.trace(exit, '')
}

View File

@ -0,0 +1,14 @@
package com.jessebrault.ssg.part
import groovy.transform.TupleConstructor
@TupleConstructor(includeFields = true, defaults = false)
class EmbeddablePart {
private final Part part
String render(Map binding) {
part.type.renderer.render(part.text, binding)
}
}

View File

@ -1,6 +1,7 @@
package com.jessebrault.ssg.part
import com.jessebrault.ssg.util.FileNameHandler
import groovy.io.FileType
import groovy.transform.NullCheck
import groovy.transform.TupleConstructor
import org.slf4j.Logger
@ -28,14 +29,14 @@ class PartFilePartsProvider implements PartsProvider {
}
def parts = []
partsDir.eachFileRecurse {
if (it.isFile()) {
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)
}
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

View File

@ -1,34 +1,21 @@
package com.jessebrault.ssg.part
import groovy.transform.TupleConstructor
class PartsMap {
@TupleConstructor(includeFields = true, defaults = false)
class RenderingPart {
private final Part part
String render(Map binding) {
part.type.renderer.render(part.text, binding)
}
}
private final Map<String, RenderingPart> partsMap = new LinkedHashMap<>()
private final Map<String, EmbeddablePart> partsMap = [:]
PartsMap(Collection<Part> parts) {
Objects.requireNonNull(parts)
parts.each {
this.partsMap.put(it.name, new RenderingPart(it))
this.partsMap.put(it.name, new EmbeddablePart(it))
}
}
RenderingPart get(String name) {
EmbeddablePart get(String name) {
this.partsMap.get(Objects.requireNonNull(name))
}
RenderingPart getAt(String name) {
EmbeddablePart getAt(String name) {
this.get(Objects.requireNonNull(name))
}

View File

@ -0,0 +1,22 @@
package com.jessebrault.ssg.specialpage
import com.jessebrault.ssg.part.Part
import com.jessebrault.ssg.part.PartsMap
import com.jessebrault.ssg.text.Text
import com.jessebrault.ssg.text.TextsMap
import groovy.text.GStringTemplateEngine
import groovy.text.TemplateEngine
class GspSpecialPageRenderer implements SpecialPageRenderer {
private static final TemplateEngine engine = new GStringTemplateEngine()
@Override
String render(String text, Collection<Text> texts, Collection<Part> parts) {
engine.createTemplate(text).make([
texts: new TextsMap(texts),
parts: new PartsMap(parts)
])
}
}

View File

@ -0,0 +1,14 @@
package com.jessebrault.ssg.specialpage
import groovy.transform.Canonical
import groovy.transform.NullCheck
import groovy.transform.TupleConstructor
@Canonical
@TupleConstructor(defaults = false)
@NullCheck
class SpecialPage {
String text
String path
SpecialPageType type
}

View File

@ -0,0 +1,47 @@
package com.jessebrault.ssg.specialpage
import com.jessebrault.ssg.util.FileNameHandler
import com.jessebrault.ssg.util.RelativePathHandler
import groovy.io.FileType
import groovy.transform.NullCheck
import groovy.transform.TupleConstructor
import org.slf4j.Logger
import org.slf4j.LoggerFactory
@TupleConstructor(includeFields = true, defaults = false)
@NullCheck
class SpecialPageFileSpecialPagesProvider implements SpecialPagesProvider {
private static final Logger logger = LoggerFactory.getLogger(SpecialPageFileSpecialPagesProvider)
private final Collection<SpecialPageType> specialPageTypes
private final File specialPagesDir
private SpecialPageType getSpecialPageType(File file) {
this.specialPageTypes.find {
it.ids.contains(new FileNameHandler(file).getExtension())
}
}
@Override
Collection<SpecialPage> getSpecialPages() {
if (!this.specialPagesDir.isDirectory()) {
throw new IllegalArgumentException('specialPagesDir must be a directory')
}
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
}
}

View File

@ -0,0 +1,8 @@
package com.jessebrault.ssg.specialpage
import com.jessebrault.ssg.part.Part
import com.jessebrault.ssg.text.Text
interface SpecialPageRenderer {
String render(String text, Collection<Text> texts, Collection <Part> parts)
}

View File

@ -0,0 +1,13 @@
package com.jessebrault.ssg.specialpage
import groovy.transform.Canonical
import groovy.transform.NullCheck
import groovy.transform.TupleConstructor
@Canonical
@TupleConstructor(defaults = false)
@NullCheck
class SpecialPageType {
Collection<String> ids
SpecialPageRenderer renderer
}

View File

@ -0,0 +1,5 @@
package com.jessebrault.ssg.specialpage
interface SpecialPagesProvider {
Collection<SpecialPage> getSpecialPages()
}

View File

@ -1,6 +1,7 @@
package com.jessebrault.ssg.template
import com.jessebrault.ssg.util.FileNameHandler
import groovy.io.FileType
import groovy.transform.NullCheck
import groovy.transform.TupleConstructor
import org.slf4j.Logger
@ -27,14 +28,14 @@ class TemplateFileTemplatesProvider implements TemplatesProvider {
throw new IllegalArgumentException('templatesDir must be a directory')
}
def templates = []
this.templatesDir.eachFileRecurse {
if (it.isFile()) {
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)
}
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

View File

@ -0,0 +1,21 @@
package com.jessebrault.ssg.text
import groovy.transform.Memoized
import groovy.transform.TupleConstructor
@TupleConstructor(includeFields = true, defaults = false)
class EmbeddableText {
private final Text text
@Memoized
String render() {
this.text.type.renderer.render(this.text.text)
}
@Memoized
FrontMatter getFrontMatter() {
this.text.type.frontMatterGetter.get(this.text.text)
}
}

View File

@ -3,16 +3,25 @@ package com.jessebrault.ssg.text
import groovy.transform.NullCheck
import groovy.transform.ToString
import groovy.transform.TupleConstructor
import org.slf4j.Logger
import org.slf4j.LoggerFactory
@TupleConstructor(includeFields = true, defaults = false)
@NullCheck
@ToString
@ToString(includeFields = true)
class FrontMatter {
private static final Logger logger = LoggerFactory.getLogger(FrontMatter)
private final Map<String, List<String>> data
String get(String key) {
data[key][0]
if (data[key] != null) {
data[key][0]
} else {
logger.warn('no entry for key in frontMatter, returning empty string: {}', key)
''
}
}
String getAt(String key) {
@ -20,7 +29,12 @@ class FrontMatter {
}
List<String> getList(String key) {
data[key]
if (data[key] != null) {
data[key]
} else {
logger.warn('no entry for key in frontMatter, returning empty list: {}', key)
[]
}
}
}

View File

@ -2,6 +2,7 @@ package com.jessebrault.ssg.text
import com.jessebrault.ssg.util.FileNameHandler
import com.jessebrault.ssg.util.RelativePathHandler
import groovy.io.FileType
import groovy.transform.NullCheck
import groovy.transform.TupleConstructor
import org.slf4j.Logger
@ -29,15 +30,15 @@ class TextFileTextsProvider implements TextsProvider {
}
def textFiles = []
this.textsDir.eachFileRecurse {
if (it.isFile()) {
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)
}
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

View File

@ -0,0 +1,22 @@
package com.jessebrault.ssg.text
class TextsMap {
private final Map<String, EmbeddableText> textsMap = [:]
TextsMap(Collection<Text> texts) {
Objects.requireNonNull(texts)
texts.each {
this.textsMap.put(it.path, new EmbeddableText(it))
}
}
EmbeddableText get(String path) {
this.textsMap.get(Objects.requireNonNull(path))
}
EmbeddableText getAt(String path) {
this.get(Objects.requireNonNull(path))
}
}

View File

@ -10,7 +10,12 @@ class FileNameHandler {
private final File file
String getExtension() {
this.file.name.substring(this.file.name.lastIndexOf('.'))
def lastIndexOfDot = this.file.name.lastIndexOf('.')
if (lastIndexOfDot == -1) {
''
} else {
this.file.name.substring(lastIndexOfDot)
}
}
String getWithoutExtension() {

View File

@ -3,6 +3,9 @@ package com.jessebrault.ssg
import com.jessebrault.ssg.part.GspPartRenderer
import com.jessebrault.ssg.part.PartFilePartsProvider
import com.jessebrault.ssg.part.PartType
import com.jessebrault.ssg.specialpage.GspSpecialPageRenderer
import com.jessebrault.ssg.specialpage.SpecialPageFileSpecialPagesProvider
import com.jessebrault.ssg.specialpage.SpecialPageType
import com.jessebrault.ssg.template.GspTemplateRenderer
import com.jessebrault.ssg.template.TemplateType
import com.jessebrault.ssg.template.TemplateFileTemplatesProvider
@ -21,6 +24,7 @@ class StaticSiteGeneratorTests {
private File partsDir
private File templatesDir
private File textsDir
private File specialPagesDir
private StaticSiteGenerator ssg
@ -29,19 +33,23 @@ class StaticSiteGeneratorTests {
this.textsDir = File.createTempDir()
this.templatesDir = File.createTempDir()
this.partsDir = File.createTempDir()
this.specialPagesDir = File.createTempDir()
def config = new Config(
textTypes: [new TextType(['.md'], new MarkdownTextRenderer(), new MarkdownFrontMatterGetter())],
templateTypes: [new TemplateType(['.gsp'], new GspTemplateRenderer())],
partTypes: [new PartType(['.gsp'], new GspPartRenderer())],
specialPageTypes: [new SpecialPageType(['.gsp'], new GspSpecialPageRenderer())],
textsDir: this.textsDir,
templatesDir: this.templatesDir,
partsDir: this.partsDir,
specialPagesDir: this.specialPagesDir,
textsProviderGetter: { Config config -> new TextFileTextsProvider(config.textTypes, config.textsDir) },
templatesProviderGetter: { Config config -> new TemplateFileTemplatesProvider(config.templateTypes, config.templatesDir) },
partsProviderGetter: { Config config -> new PartFilePartsProvider(config.partTypes, config.partsDir) }
partsProviderGetter: { Config config -> new PartFilePartsProvider(config.partTypes, config.partsDir) },
specialPagesProviderGetter: { Config config -> new SpecialPageFileSpecialPagesProvider(config.specialPageTypes, config.specialPagesDir) }
)
this.ssg = new SimpleStaticSiteGenerator(config)
}
@ -77,4 +85,18 @@ class StaticSiteGeneratorTests {
assertEquals('<p><strong>Hello, World!</strong></p>\n', outFile.text)
}
@Test
void outputsSpecialPage() {
new FileTreeBuilder(this.specialPagesDir).file('special.gsp', $/<%= texts['test'].render() %>/$)
new FileTreeBuilder(this.templatesDir).file('template.gsp', '<%= 1 + 1 %>')
new FileTreeBuilder(this.textsDir).file('test.md', '---\ntemplate: template.gsp\n---\nHello, World!')
def buildDir = File.createTempDir()
this.ssg.generate(buildDir)
def outFile = new File(buildDir, 'special.html')
assertTrue(outFile.exists())
assertEquals('<p>Hello, World!</p>\n', outFile.text)
}
}

View File

@ -0,0 +1,56 @@
package com.jessebrault.ssg.specialpage
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import static org.junit.jupiter.api.Assertions.assertEquals
class SpecialPageFileSpecialPagesProviderTests {
private static final SpecialPageType gspType = new SpecialPageType(['.gsp'], null)
private File specialPagesDir
private SpecialPagesProvider specialPagesProvider
@BeforeEach
void beforeEach() {
this.specialPagesDir = File.createTempDir()
this.specialPagesProvider = new SpecialPageFileSpecialPagesProvider([gspType], this.specialPagesDir)
}
@Test
void findsSpecialPage() {
new FileTreeBuilder(this.specialPagesDir)
.file('test.gsp', '<%= "Hello, World!" %>')
def r = this.specialPagesProvider.getSpecialPages()
assertEquals(1, r.size())
def f0 = r[0]
assertEquals('test', f0.path)
assertEquals('<%= "Hello, World!" %>', f0.text)
assertEquals(gspType, f0.type)
}
@Test
void findsNestedSpecialPage() {
new FileTreeBuilder(this.specialPagesDir).dir('nested') {
file('nested.gsp', '<%= "Hello, World!" %>')
}
def r = this.specialPagesProvider.getSpecialPages()
assertEquals(1, r.size())
def f0 = r[0]
assertEquals('nested/nested', f0.path)
assertEquals('<%= "Hello, World!" %>', f0.text)
assertEquals(gspType, f0.type)
}
@Test
void ignoresUnsupportedFile() {
new FileTreeBuilder(this.specialPagesDir).file('.ignored', 'Ignored!')
def r = this.specialPagesProvider.getSpecialPages()
assertEquals(0, r.size())
}
}

View File

@ -22,7 +22,7 @@ class TextFileTextsProviderTests {
void findsFile() {
new FileTreeBuilder(this.textsDir).file('test.md', '**Hello, World!**')
def r = textsProvider.getTextFiles()
def r = this.textsProvider.getTextFiles()
assertEquals(1, r.size())
def f0 = r[0]
assertEquals('test', f0.path)
@ -36,7 +36,7 @@ class TextFileTextsProviderTests {
file('nested.md', '**Hello!**')
}
def r = textsProvider.getTextFiles()
def r = this.textsProvider.getTextFiles()
assertEquals(1, r.size())
def f0 = r[0]
assertEquals('nested/nested', f0.path)
@ -48,7 +48,7 @@ class TextFileTextsProviderTests {
void ignoresUnsupportedFile() {
new FileTreeBuilder(this.textsDir).file('.ignored', 'Ignored!')
def r = textsProvider.getTextFiles()
def r = this.textsProvider.getTextFiles()
assertEquals(0, r.size())
}