diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..e717505 --- /dev/null +++ b/CHANGELOG.md @@ -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 == '' + + def tagWithBody = tagBuilder.title 'Hello, World!' + assert tagWithBody == 'Hello, World!' + + def tagWithAttributes = tagBuilder.meta name: 'og:title', content: 'Hello, World!' + assert tagWithAttributes == '' + + def tagWithAttributesAndBody = tagBuilder.p([id: 'my-paragraph'], 'Hello, World!') + assert tagWithAttributesAndBody == '

Hello, World!

' + %> + ``` + + 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). diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..45d4d52 --- /dev/null +++ b/TODO.md @@ -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 diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..663921b --- /dev/null +++ b/build.gradle @@ -0,0 +1,11 @@ +plugins { + id 'org.asciidoctor.jvm.convert' version '3.3.2' +} + +repositories { + mavenCentral() +} + +asciidoctor { + sourceDir = 'docs/asciidoc' +} \ No newline at end of file diff --git a/buildSrc/src/main/groovy/ssg.common.gradle b/buildSrc/src/main/groovy/ssg.common.gradle index 901fd91..c89d81f 100644 --- a/buildSrc/src/main/groovy/ssg.common.gradle +++ b/buildSrc/src/main/groovy/ssg.common.gradle @@ -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 { diff --git a/buildSrc/src/main/groovy/ssg.lib.gradle b/buildSrc/src/main/groovy/ssg.lib.gradle deleted file mode 100644 index 443abeb..0000000 --- a/buildSrc/src/main/groovy/ssg.lib.gradle +++ /dev/null @@ -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' -} \ No newline at end of file diff --git a/cli/src/main/groovy/com/jessebrault/ssg/AbstractBuildCommand.groovy b/cli/src/main/groovy/com/jessebrault/ssg/AbstractBuildCommand.groovy index cc087f7..cd483a7 100644 --- a/cli/src/main/groovy/com/jessebrault/ssg/AbstractBuildCommand.groovy +++ b/cli/src/main/groovy/com/jessebrault/ssg/AbstractBuildCommand.groovy @@ -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 executionDiagnostics = [] + def context = new TaskExecutorContext( + it, + tasks, + this.ssg.taskTypes, + { Collection diagnostics -> + executionDiagnostics.addAll(diagnostics) + } + ) + result.get().each { it.execute(context) } + if (!executionDiagnostics.isEmpty()) { + hadDiagnostics = true + executionDiagnostics.each { + logger.error(it.message) + } } } } diff --git a/docs/asciidoc/manual.asciidoc b/docs/asciidoc/manual.asciidoc new file mode 100644 index 0000000..3bd22a1 --- /dev/null +++ b/docs/asciidoc/manual.asciidoc @@ -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 == 'Hello!' +out << a // <2> +---- +<1> Create an tag. +<2> Output the tag in the current script block. \ No newline at end of file diff --git a/lib/build.gradle b/lib/build.gradle index 21b511d..53cb59f 100644 --- a/lib/build.gradle +++ b/lib/build.gradle @@ -1,6 +1,5 @@ plugins { id 'ssg.common' - id 'ssg.lib' } repositories { diff --git a/lib/src/main/groovy/com/jessebrault/ssg/Build.groovy b/lib/src/main/groovy/com/jessebrault/ssg/Build.groovy index c5d611a..d53e40c 100644 --- a/lib/src/main/groovy/com/jessebrault/ssg/Build.groovy +++ b/lib/src/main/groovy/com/jessebrault/ssg/Build.groovy @@ -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 })" } } diff --git a/lib/src/main/groovy/com/jessebrault/ssg/GeneratedPage.groovy b/lib/src/main/groovy/com/jessebrault/ssg/GeneratedPage.groovy deleted file mode 100644 index 6b92787..0000000 --- a/lib/src/main/groovy/com/jessebrault/ssg/GeneratedPage.groovy +++ /dev/null @@ -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 })" - } - -} diff --git a/lib/src/main/groovy/com/jessebrault/ssg/Result.groovy b/lib/src/main/groovy/com/jessebrault/ssg/Result.groovy new file mode 100644 index 0000000..94702bd --- /dev/null +++ b/lib/src/main/groovy/com/jessebrault/ssg/Result.groovy @@ -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 { + + final Collection diagnostics + private final T t + + boolean hasDiagnostics() { + !this.diagnostics.isEmpty() + } + + T get() { + this.t + } + + @Override + String toString() { + "Result(diagnostics: ${ this.diagnostics }, t: ${ this.t })" + } + +} diff --git a/lib/src/main/groovy/com/jessebrault/ssg/SimpleStaticSiteGenerator.groovy b/lib/src/main/groovy/com/jessebrault/ssg/SimpleStaticSiteGenerator.groovy index f2dd9cf..0219f67 100644 --- a/lib/src/main/groovy/com/jessebrault/ssg/SimpleStaticSiteGenerator.groovy +++ b/lib/src/main/groovy/com/jessebrault/ssg/SimpleStaticSiteGenerator.groovy @@ -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> generate(Build build) { + TaskTypeContainer getTaskTypes() { + new TaskTypeContainer([textHtmlFactory.taskType, specialPageHtmlFactory.taskType]) + } + + @Override + Result 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 diagnostics = [] - Collection 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 diff --git a/lib/src/main/groovy/com/jessebrault/ssg/SiteSpec.groovy b/lib/src/main/groovy/com/jessebrault/ssg/SiteSpec.groovy new file mode 100644 index 0000000..37a8835 --- /dev/null +++ b/lib/src/main/groovy/com/jessebrault/ssg/SiteSpec.groovy @@ -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 })" + } + +} diff --git a/lib/src/main/groovy/com/jessebrault/ssg/StaticSiteGenerator.groovy b/lib/src/main/groovy/com/jessebrault/ssg/StaticSiteGenerator.groovy index 8553be4..fe72287 100644 --- a/lib/src/main/groovy/com/jessebrault/ssg/StaticSiteGenerator.groovy +++ b/lib/src/main/groovy/com/jessebrault/ssg/StaticSiteGenerator.groovy @@ -1,5 +1,9 @@ package com.jessebrault.ssg +import com.jessebrault.ssg.task.TaskContainer +import com.jessebrault.ssg.task.TaskTypeContainer + interface StaticSiteGenerator { - Tuple2, Collection> generate(Build build) + TaskTypeContainer getTaskTypes() + Result generate(Build build) } diff --git a/lib/src/main/groovy/com/jessebrault/ssg/buildscript/BuildClosureDelegate.groovy b/lib/src/main/groovy/com/jessebrault/ssg/buildscript/BuildClosureDelegate.groovy index 6572ee3..cc24ef2 100644 --- a/lib/src/main/groovy/com/jessebrault/ssg/buildscript/BuildClosureDelegate.groovy +++ b/lib/src/main/groovy/com/jessebrault/ssg/buildscript/BuildClosureDelegate.groovy @@ -1,26 +1,37 @@ 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 void config( @DelegatesTo(value = ConfigClosureDelegate, strategy = Closure.DELEGATE_FIRST) - Closure configClosure + Closure configClosure ) { configClosure.setDelegate(new ConfigClosureDelegate(this.config)) configClosure.setResolveStrategy(Closure.DELEGATE_FIRST) 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 + Closure globalsClosure ) { def globalsConfigurator = new GlobalsClosureDelegate(this.globals) globalsClosure.setDelegate(globalsConfigurator) diff --git a/lib/src/main/groovy/com/jessebrault/ssg/buildscript/BuildScriptBase.groovy b/lib/src/main/groovy/com/jessebrault/ssg/buildscript/BuildScriptBase.groovy index 10907fa..3c9a84d 100644 --- a/lib/src/main/groovy/com/jessebrault/ssg/buildscript/BuildScriptBase.groovy +++ b/lib/src/main/groovy/com/jessebrault/ssg/buildscript/BuildScriptBase.groovy @@ -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 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++ } diff --git a/lib/src/main/groovy/com/jessebrault/ssg/buildscript/SiteSpecClosureDelegate.groovy b/lib/src/main/groovy/com/jessebrault/ssg/buildscript/SiteSpecClosureDelegate.groovy new file mode 100644 index 0000000..14a4e71 --- /dev/null +++ b/lib/src/main/groovy/com/jessebrault/ssg/buildscript/SiteSpecClosureDelegate.groovy @@ -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 + } + +} diff --git a/lib/src/main/groovy/com/jessebrault/ssg/dsl/StandardDslMap.groovy b/lib/src/main/groovy/com/jessebrault/ssg/dsl/StandardDslMap.groovy new file mode 100644 index 0000000..c57760e --- /dev/null +++ b/lib/src/main/groovy/com/jessebrault/ssg/dsl/StandardDslMap.groovy @@ -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) + } + } + +} diff --git a/lib/src/main/groovy/com/jessebrault/ssg/part/EmbeddablePart.groovy b/lib/src/main/groovy/com/jessebrault/ssg/part/EmbeddablePart.groovy index 9e7d1ab..82ede31 100644 --- a/lib/src/main/groovy/com/jessebrault/ssg/part/EmbeddablePart.groovy +++ b/lib/src/main/groovy/com/jessebrault/ssg/part/EmbeddablePart.groovy @@ -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 })" } } diff --git a/lib/src/main/groovy/com/jessebrault/ssg/part/EmbeddablePartsMap.groovy b/lib/src/main/groovy/com/jessebrault/ssg/part/EmbeddablePartsMap.groovy index 0db363b..26fe807 100644 --- a/lib/src/main/groovy/com/jessebrault/ssg/part/EmbeddablePartsMap.groovy +++ b/lib/src/main/groovy/com/jessebrault/ssg/part/EmbeddablePartsMap.groovy @@ -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 partsMap = [:] - EmbeddablePartsMap(Collection parts, Map globals, Closure onDiagnostics) { + EmbeddablePartsMap( + Collection 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)) } } diff --git a/lib/src/main/groovy/com/jessebrault/ssg/part/GspPartRenderer.groovy b/lib/src/main/groovy/com/jessebrault/ssg/part/GspPartRenderer.groovy index c1f783e..d4360c8 100644 --- a/lib/src/main/groovy/com/jessebrault/ssg/part/GspPartRenderer.groovy +++ b/lib/src/main/groovy/com/jessebrault/ssg/part/GspPartRenderer.groovy @@ -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, String> render(Part part, Map binding, Map globals) { + Tuple2, 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 + )], + '' + ) } } diff --git a/lib/src/main/groovy/com/jessebrault/ssg/part/PartFilePartsProvider.groovy b/lib/src/main/groovy/com/jessebrault/ssg/part/PartFilePartsProvider.groovy index 72e7cc8..f445690 100644 --- a/lib/src/main/groovy/com/jessebrault/ssg/part/PartFilePartsProvider.groovy +++ b/lib/src/main/groovy/com/jessebrault/ssg/part/PartFilePartsProvider.groovy @@ -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 implements PartsProvider { private static final Logger logger = LoggerFactory.getLogger(PartFilePartsProvider) private final Collection partTypes - private final File partsDir - PartFilePartsProvider(Collection partTypes, File partsDir) { - this.partTypes = partTypes - this.partsDir = partsDir - this.watchableDir = this.partsDir + PartFilePartsProvider(File partsDir, Collection 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 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 })" } } diff --git a/lib/src/main/groovy/com/jessebrault/ssg/part/PartRenderer.groovy b/lib/src/main/groovy/com/jessebrault/ssg/part/PartRenderer.groovy index 7675fdd..9d27b39 100644 --- a/lib/src/main/groovy/com/jessebrault/ssg/part/PartRenderer.groovy +++ b/lib/src/main/groovy/com/jessebrault/ssg/part/PartRenderer.groovy @@ -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, String> render(Part part, Map binding, Map globals) + + Tuple2, String> render( + Part part, + Map binding, + RenderContext context, + @Nullable Text text + ) + } diff --git a/lib/src/main/groovy/com/jessebrault/ssg/provider/AbstractFileCollectionProvider.groovy b/lib/src/main/groovy/com/jessebrault/ssg/provider/AbstractFileCollectionProvider.groovy new file mode 100644 index 0000000..56ae2c7 --- /dev/null +++ b/lib/src/main/groovy/com/jessebrault/ssg/provider/AbstractFileCollectionProvider.groovy @@ -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 implements Provider>, 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 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 + } + } + +} diff --git a/lib/src/main/groovy/com/jessebrault/ssg/renderer/RenderContext.groovy b/lib/src/main/groovy/com/jessebrault/ssg/renderer/RenderContext.groovy new file mode 100644 index 0000000..5a4171a --- /dev/null +++ b/lib/src/main/groovy/com/jessebrault/ssg/renderer/RenderContext.groovy @@ -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 texts + final Collection parts + final String sourcePath + final String targetPath + final TaskContainer tasks + final TaskTypeContainer taskTypes +} diff --git a/lib/src/main/groovy/com/jessebrault/ssg/specialpage/GspSpecialPageRenderer.groovy b/lib/src/main/groovy/com/jessebrault/ssg/specialpage/GspSpecialPageRenderer.groovy index cadb675..335651c 100644 --- a/lib/src/main/groovy/com/jessebrault/ssg/specialpage/GspSpecialPageRenderer.groovy +++ b/lib/src/main/groovy/com/jessebrault/ssg/specialpage/GspSpecialPageRenderer.groovy @@ -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, String> render(SpecialPage specialPage, Collection texts, Collection parts, Map globals) { + Tuple2, String> render( + SpecialPage specialPage, + RenderContext context + ) { + def diagnostics = [] try { - Collection diagnostics = [] - def result = engine.createTemplate(specialPage.text).make([ - globals: globals, - parts: new EmbeddablePartsMap(parts, globals, { Collection partDiagnostics -> - diagnostics.addAll(partDiagnostics) - }), - texts: new EmbeddableTextsCollection(texts, globals, { Collection 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 + )], + '' + ) } } diff --git a/lib/src/main/groovy/com/jessebrault/ssg/specialpage/SpecialPage.groovy b/lib/src/main/groovy/com/jessebrault/ssg/specialpage/SpecialPage.groovy index 869a412..d1af35d 100644 --- a/lib/src/main/groovy/com/jessebrault/ssg/specialpage/SpecialPage.groovy +++ b/lib/src/main/groovy/com/jessebrault/ssg/specialpage/SpecialPage.groovy @@ -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() { diff --git a/lib/src/main/groovy/com/jessebrault/ssg/specialpage/SpecialPageFileSpecialPagesProvider.groovy b/lib/src/main/groovy/com/jessebrault/ssg/specialpage/SpecialPageFileSpecialPagesProvider.groovy index a2ef1e3..a8d9f9c 100644 --- a/lib/src/main/groovy/com/jessebrault/ssg/specialpage/SpecialPageFileSpecialPagesProvider.groovy +++ b/lib/src/main/groovy/com/jessebrault/ssg/specialpage/SpecialPageFileSpecialPagesProvider.groovy @@ -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 + implements SpecialPagesProvider { private static final Logger logger = LoggerFactory.getLogger(SpecialPageFileSpecialPagesProvider) private final Collection specialPageTypes - private final File specialPagesDir - SpecialPageFileSpecialPagesProvider(Collection specialPageTypes, File specialPagesDir) { - this.specialPageTypes = specialPageTypes - this.specialPagesDir = specialPagesDir - this.watchableDir = this.specialPagesDir + SpecialPageFileSpecialPagesProvider(File specialPagesDir, Collection 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 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 })" } } diff --git a/lib/src/main/groovy/com/jessebrault/ssg/specialpage/SpecialPageRenderer.groovy b/lib/src/main/groovy/com/jessebrault/ssg/specialpage/SpecialPageRenderer.groovy index a87f57c..44e5363 100644 --- a/lib/src/main/groovy/com/jessebrault/ssg/specialpage/SpecialPageRenderer.groovy +++ b/lib/src/main/groovy/com/jessebrault/ssg/specialpage/SpecialPageRenderer.groovy @@ -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, String> render( SpecialPage specialPage, - Collection texts, - Collection parts, - Map globals + RenderContext context ) } \ No newline at end of file diff --git a/lib/src/main/groovy/com/jessebrault/ssg/tagbuilder/DynamicTagBuilder.groovy b/lib/src/main/groovy/com/jessebrault/ssg/tagbuilder/DynamicTagBuilder.groovy new file mode 100644 index 0000000..9acb4d0 --- /dev/null +++ b/lib/src/main/groovy/com/jessebrault/ssg/tagbuilder/DynamicTagBuilder.groovy @@ -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 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" + } + + @Override + String create(String name, Map 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" + } + + @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) + } + } + +} diff --git a/lib/src/main/groovy/com/jessebrault/ssg/tagbuilder/TagBuilder.groovy b/lib/src/main/groovy/com/jessebrault/ssg/tagbuilder/TagBuilder.groovy new file mode 100644 index 0000000..a92c64a --- /dev/null +++ b/lib/src/main/groovy/com/jessebrault/ssg/tagbuilder/TagBuilder.groovy @@ -0,0 +1,8 @@ +package com.jessebrault.ssg.tagbuilder + +interface TagBuilder { + String create(String name) + String create(String name, Map attributes) + String create(String name, String body) + String create(String name, Map attributes, String body) +} diff --git a/lib/src/main/groovy/com/jessebrault/ssg/task/AbstractTask.groovy b/lib/src/main/groovy/com/jessebrault/ssg/task/AbstractTask.groovy new file mode 100644 index 0000000..bfc3565 --- /dev/null +++ b/lib/src/main/groovy/com/jessebrault/ssg/task/AbstractTask.groovy @@ -0,0 +1,31 @@ +package com.jessebrault.ssg.task + +import groovy.transform.EqualsAndHashCode +import groovy.transform.NullCheck + +@NullCheck +@EqualsAndHashCode +abstract class AbstractTask implements Task { + + final TaskType type + final String name + + AbstractTask(TaskType 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 })" + } + +} diff --git a/lib/src/main/groovy/com/jessebrault/ssg/task/FileInput.groovy b/lib/src/main/groovy/com/jessebrault/ssg/task/FileInput.groovy new file mode 100644 index 0000000..496b2fa --- /dev/null +++ b/lib/src/main/groovy/com/jessebrault/ssg/task/FileInput.groovy @@ -0,0 +1,5 @@ +package com.jessebrault.ssg.task + +interface FileInput extends Input { + File getFile() +} \ No newline at end of file diff --git a/lib/src/main/groovy/com/jessebrault/ssg/task/FileOutput.groovy b/lib/src/main/groovy/com/jessebrault/ssg/task/FileOutput.groovy new file mode 100644 index 0000000..34bd660 --- /dev/null +++ b/lib/src/main/groovy/com/jessebrault/ssg/task/FileOutput.groovy @@ -0,0 +1,6 @@ +package com.jessebrault.ssg.task + +interface FileOutput extends Output { + File getFile() + String getContent() +} diff --git a/lib/src/main/groovy/com/jessebrault/ssg/task/HtmlFileOutput.groovy b/lib/src/main/groovy/com/jessebrault/ssg/task/HtmlFileOutput.groovy new file mode 100644 index 0000000..bf69c28 --- /dev/null +++ b/lib/src/main/groovy/com/jessebrault/ssg/task/HtmlFileOutput.groovy @@ -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 contentClosure + + String getContent(TaskContainer tasks, TaskTypeContainer taskTypes, Closure onDiagnostics) { + this.contentClosure(tasks, taskTypes, onDiagnostics) + } + + @Override + String toString() { + "HtmlFileOutput(file: ${ this.file }, htmlPath: ${ this.htmlPath })" + } + +} diff --git a/lib/src/main/groovy/com/jessebrault/ssg/task/Input.groovy b/lib/src/main/groovy/com/jessebrault/ssg/task/Input.groovy new file mode 100644 index 0000000..ac7ff57 --- /dev/null +++ b/lib/src/main/groovy/com/jessebrault/ssg/task/Input.groovy @@ -0,0 +1,5 @@ +package com.jessebrault.ssg.task + +interface Input { + String getName() +} \ No newline at end of file diff --git a/lib/src/main/groovy/com/jessebrault/ssg/task/Output.groovy b/lib/src/main/groovy/com/jessebrault/ssg/task/Output.groovy new file mode 100644 index 0000000..f03dcda --- /dev/null +++ b/lib/src/main/groovy/com/jessebrault/ssg/task/Output.groovy @@ -0,0 +1,5 @@ +package com.jessebrault.ssg.task + +interface Output { + String getName() +} diff --git a/lib/src/main/groovy/com/jessebrault/ssg/task/SpecialPageToHtmlFileTask.groovy b/lib/src/main/groovy/com/jessebrault/ssg/task/SpecialPageToHtmlFileTask.groovy new file mode 100644 index 0000000..fc4eed6 --- /dev/null +++ b/lib/src/main/groovy/com/jessebrault/ssg/task/SpecialPageToHtmlFileTask.groovy @@ -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 { + + private static final class SpecialPageToHtmlFileTaskExecutor implements TaskExecutor { + + @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 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 })" + } + +} diff --git a/lib/src/main/groovy/com/jessebrault/ssg/task/SpecialPageToHtmlFileTaskFactory.groovy b/lib/src/main/groovy/com/jessebrault/ssg/task/SpecialPageToHtmlFileTaskFactory.groovy new file mode 100644 index 0000000..45017e5 --- /dev/null +++ b/lib/src/main/groovy/com/jessebrault/ssg/task/SpecialPageToHtmlFileTaskFactory.groovy @@ -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 { + + 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 getTaskType() { + SpecialPageToHtmlFileTask.TYPE + } + + @Override + Result> 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(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 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 + } + +} diff --git a/lib/src/main/groovy/com/jessebrault/ssg/task/Task.groovy b/lib/src/main/groovy/com/jessebrault/ssg/task/Task.groovy new file mode 100644 index 0000000..47217d2 --- /dev/null +++ b/lib/src/main/groovy/com/jessebrault/ssg/task/Task.groovy @@ -0,0 +1,7 @@ +package com.jessebrault.ssg.task + +interface Task { + TaskType getType() + String getName() + void execute(TaskExecutorContext context) +} \ No newline at end of file diff --git a/lib/src/main/groovy/com/jessebrault/ssg/task/TaskCollection.groovy b/lib/src/main/groovy/com/jessebrault/ssg/task/TaskCollection.groovy new file mode 100644 index 0000000..50a3f37 --- /dev/null +++ b/lib/src/main/groovy/com/jessebrault/ssg/task/TaskCollection.groovy @@ -0,0 +1,24 @@ +package com.jessebrault.ssg.task + +import static java.util.Objects.requireNonNull + +class TaskCollection { + + @Delegate + private final Collection tasks = new ArrayList() + + TaskCollection(Collection tasks = null) { + if (tasks != null) { + this.tasks.addAll(requireNonNull(tasks)) + } + } + + def TaskCollection findAllByType( + TaskType taskType + ) { + new TaskCollection<>(this.tasks.findResults { + it.type == taskType ? it : null + }) + } + +} diff --git a/lib/src/main/groovy/com/jessebrault/ssg/task/TaskContainer.groovy b/lib/src/main/groovy/com/jessebrault/ssg/task/TaskContainer.groovy new file mode 100644 index 0000000..3661476 --- /dev/null +++ b/lib/src/main/groovy/com/jessebrault/ssg/task/TaskContainer.groovy @@ -0,0 +1,9 @@ +package com.jessebrault.ssg.task + +final class TaskContainer extends TaskCollection { + + TaskContainer(Collection tasks = null) { + super(tasks) + } + +} diff --git a/lib/src/main/groovy/com/jessebrault/ssg/task/TaskExecutor.groovy b/lib/src/main/groovy/com/jessebrault/ssg/task/TaskExecutor.groovy new file mode 100644 index 0000000..35cb769 --- /dev/null +++ b/lib/src/main/groovy/com/jessebrault/ssg/task/TaskExecutor.groovy @@ -0,0 +1,5 @@ +package com.jessebrault.ssg.task + +interface TaskExecutor { + void execute(T task, TaskExecutorContext context) +} \ No newline at end of file diff --git a/lib/src/main/groovy/com/jessebrault/ssg/task/TaskExecutorContext.groovy b/lib/src/main/groovy/com/jessebrault/ssg/task/TaskExecutorContext.groovy new file mode 100644 index 0000000..a1da7b1 --- /dev/null +++ b/lib/src/main/groovy/com/jessebrault/ssg/task/TaskExecutorContext.groovy @@ -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 })" + } + +} diff --git a/lib/src/main/groovy/com/jessebrault/ssg/task/TaskFactory.groovy b/lib/src/main/groovy/com/jessebrault/ssg/task/TaskFactory.groovy new file mode 100644 index 0000000..60b78ff --- /dev/null +++ b/lib/src/main/groovy/com/jessebrault/ssg/task/TaskFactory.groovy @@ -0,0 +1,9 @@ +package com.jessebrault.ssg.task + +import com.jessebrault.ssg.Build +import com.jessebrault.ssg.Result + +interface TaskFactory { + TaskType getTaskType() + Result> getTasks(Build build) +} \ No newline at end of file diff --git a/lib/src/main/groovy/com/jessebrault/ssg/task/TaskType.groovy b/lib/src/main/groovy/com/jessebrault/ssg/task/TaskType.groovy new file mode 100644 index 0000000..2b2652b --- /dev/null +++ b/lib/src/main/groovy/com/jessebrault/ssg/task/TaskType.groovy @@ -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 { + + final String name + final TaskExecutor executor + + @Override + String toString() { + "TaskType(${ this.name }, ${ this.executor })" + } + +} diff --git a/lib/src/main/groovy/com/jessebrault/ssg/task/TaskTypeContainer.groovy b/lib/src/main/groovy/com/jessebrault/ssg/task/TaskTypeContainer.groovy new file mode 100644 index 0000000..b4092cc --- /dev/null +++ b/lib/src/main/groovy/com/jessebrault/ssg/task/TaskTypeContainer.groovy @@ -0,0 +1,31 @@ +package com.jessebrault.ssg.task + +final class TaskTypeContainer { + + @Delegate + private final Set> taskTypes = [] + + TaskTypeContainer(Collection> taskTypes) { + if (taskTypes != null) { + this.taskTypes.addAll(taskTypes) + } + } + + TaskTypeContainer(TaskTypeContainer taskTypeContainer) { + if (taskTypeContainer != null) { + this.taskTypes.addAll(taskTypeContainer) + } + } + + TaskTypeContainer() {} + + @Override + TaskType getProperty(String propertyName) { + def taskType = this.taskTypes.find { it.name == propertyName } + if (!taskType) { + throw new IllegalArgumentException("no such taskType: ${ propertyName }") + } + taskType + } + +} diff --git a/lib/src/main/groovy/com/jessebrault/ssg/task/TextInput.groovy b/lib/src/main/groovy/com/jessebrault/ssg/task/TextInput.groovy new file mode 100644 index 0000000..beed338 --- /dev/null +++ b/lib/src/main/groovy/com/jessebrault/ssg/task/TextInput.groovy @@ -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 +} diff --git a/lib/src/main/groovy/com/jessebrault/ssg/task/TextToHtmlFileTask.groovy b/lib/src/main/groovy/com/jessebrault/ssg/task/TextToHtmlFileTask.groovy new file mode 100644 index 0000000..f89a063 --- /dev/null +++ b/lib/src/main/groovy/com/jessebrault/ssg/task/TextToHtmlFileTask.groovy @@ -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 { + + private static final class TextToHtmlFileTaskExecutor implements TaskExecutor { + + @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 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 })" + } + +} diff --git a/lib/src/main/groovy/com/jessebrault/ssg/task/TextToHtmlFileTaskFactory.groovy b/lib/src/main/groovy/com/jessebrault/ssg/task/TextToHtmlFileTaskFactory.groovy new file mode 100644 index 0000000..0f8d1c0 --- /dev/null +++ b/lib/src/main/groovy/com/jessebrault/ssg/task/TextToHtmlFileTaskFactory.groovy @@ -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 { + + 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 getTaskType() { + TextToHtmlFileTask.TYPE + } + + @Override + Result> 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(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 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 + } + +} diff --git a/lib/src/main/groovy/com/jessebrault/ssg/task/WithInput.groovy b/lib/src/main/groovy/com/jessebrault/ssg/task/WithInput.groovy new file mode 100644 index 0000000..7482c8a --- /dev/null +++ b/lib/src/main/groovy/com/jessebrault/ssg/task/WithInput.groovy @@ -0,0 +1,5 @@ +package com.jessebrault.ssg.task + +interface WithInput { + I getInput() +} \ No newline at end of file diff --git a/lib/src/main/groovy/com/jessebrault/ssg/task/WithOutput.groovy b/lib/src/main/groovy/com/jessebrault/ssg/task/WithOutput.groovy new file mode 100644 index 0000000..b181686 --- /dev/null +++ b/lib/src/main/groovy/com/jessebrault/ssg/task/WithOutput.groovy @@ -0,0 +1,5 @@ +package com.jessebrault.ssg.task + +interface WithOutput { + O getOutput() +} \ No newline at end of file diff --git a/lib/src/main/groovy/com/jessebrault/ssg/template/GspTemplateRenderer.groovy b/lib/src/main/groovy/com/jessebrault/ssg/template/GspTemplateRenderer.groovy index ec538b6..271b240 100644 --- a/lib/src/main/groovy/com/jessebrault/ssg/template/GspTemplateRenderer.groovy +++ b/lib/src/main/groovy/com/jessebrault/ssg/template/GspTemplateRenderer.groovy @@ -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, String> render( Template template, - FrontMatter frontMatter, - String text, - Collection parts, - Map globals + Text text, + RenderContext context ) { + def diagnostics = [] try { - Collection diagnostics = [] - def result = engine.createTemplate(template.text).make([ - frontMatter: frontMatter, - globals: globals, - parts: new EmbeddablePartsMap(parts, globals, { Collection 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 + )], + '' + ) } } diff --git a/lib/src/main/groovy/com/jessebrault/ssg/template/TemplateFileTemplatesProvider.groovy b/lib/src/main/groovy/com/jessebrault/ssg/template/TemplateFileTemplatesProvider.groovy index a656c1e..1a08903 100644 --- a/lib/src/main/groovy/com/jessebrault/ssg/template/TemplateFileTemplatesProvider.groovy +++ b/lib/src/main/groovy/com/jessebrault/ssg/template/TemplateFileTemplatesProvider.groovy @@ -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