diff --git a/buildSrc/src/main/groovy/ssg.lib.gradle b/buildSrc/src/main/groovy/ssg.lib.gradle index 443abeb..78f61fa 100644 --- a/buildSrc/src/main/groovy/ssg.lib.gradle +++ b/buildSrc/src/main/groovy/ssg.lib.gradle @@ -7,6 +7,9 @@ repositories { } dependencies { + // https://mvnrepository.com/artifact/org.jetbrains/annotations + api 'org.jetbrains:annotations:24.0.0' + /** * Logging */ diff --git a/lib/src/main/groovy/com/jessebrault/ssg/SimpleStaticSiteGenerator.groovy b/lib/src/main/groovy/com/jessebrault/ssg/SimpleStaticSiteGenerator.groovy index f2dd9cf..04504ed 100644 --- a/lib/src/main/groovy/com/jessebrault/ssg/SimpleStaticSiteGenerator.groovy +++ b/lib/src/main/groovy/com/jessebrault/ssg/SimpleStaticSiteGenerator.groovy @@ -69,21 +69,14 @@ class SimpleStaticSiteGenerator implements StaticSiteGenerator { } 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) + def templateRenderResult = template.type.renderer.render( + template, + frontMatter, + it, + parts, + globals + ) String renderedTemplate if (templateRenderResult.v1.size() > 0) { diagnostics.addAll(templateRenderResult.v1) 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..ceeb31b 100644 --- a/lib/src/main/groovy/com/jessebrault/ssg/part/EmbeddablePart.groovy +++ b/lib/src/main/groovy/com/jessebrault/ssg/part/EmbeddablePart.groovy @@ -1,8 +1,10 @@ package com.jessebrault.ssg.part +import com.jessebrault.ssg.text.EmbeddableText import groovy.transform.EqualsAndHashCode import groovy.transform.NullCheck import groovy.transform.TupleConstructor +import org.jetbrains.annotations.Nullable @TupleConstructor(includeFields = true, defaults = false) @NullCheck @@ -13,8 +15,16 @@ class EmbeddablePart { private final Map globals private final Closure onDiagnostics + @Nullable + private final EmbeddableText 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.globals, + this.text + ) if (result.v1.size() > 0) { this.onDiagnostics.call(result.v1) '' 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..5473bdc 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,26 @@ package com.jessebrault.ssg.part +import com.jessebrault.ssg.text.EmbeddableText import groovy.transform.EqualsAndHashCode -import groovy.transform.NullCheck +import org.jetbrains.annotations.Nullable -@NullCheck @EqualsAndHashCode(includeFields = true) class EmbeddablePartsMap { @Delegate private final Map partsMap = [:] - EmbeddablePartsMap(Collection parts, Map globals, Closure onDiagnostics) { + EmbeddablePartsMap( + Collection parts, + Map globals, + Closure onDiagnostics, + @Nullable EmbeddableText text = null + ) { + Objects.requireNonNull(parts) + Objects.requireNonNull(globals) + Objects.requireNonNull(onDiagnostics) parts.each { - this.put(it.path, new EmbeddablePart(it, globals, onDiagnostics)) + this.put(it.path, new EmbeddablePart(it, globals, 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..84e7812 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,11 @@ package com.jessebrault.ssg.part import com.jessebrault.ssg.Diagnostic +import com.jessebrault.ssg.text.EmbeddableText import groovy.text.GStringTemplateEngine import groovy.text.TemplateEngine import groovy.transform.EqualsAndHashCode +import org.jetbrains.annotations.Nullable @EqualsAndHashCode class GspPartRenderer implements PartRenderer { @@ -11,15 +13,27 @@ 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, + Map globals, + @Nullable EmbeddableText text = null + ) { + Objects.requireNonNull(part) + Objects.requireNonNull(binding) + Objects.requireNonNull(globals) try { def result = engine.createTemplate(part.text).make([ binding: binding, - globals: globals + globals: globals, + text: text ]) new Tuple2<>([], result.toString()) } catch (Exception e) { - new Tuple2<>([new Diagnostic("An exception occurred while rendering part ${ part.path }:\n${ e }", e)], '') + new Tuple2<>( + [new Diagnostic("An exception occurred while rendering part ${ part.path }:\n${ e }", e)], + '' + ) } } 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..2995b19 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,16 @@ package com.jessebrault.ssg.part import com.jessebrault.ssg.Diagnostic +import com.jessebrault.ssg.text.EmbeddableText +import org.jetbrains.annotations.Nullable interface PartRenderer { - Tuple2, String> render(Part part, Map binding, Map globals) + + Tuple2, String> render( + Part part, + Map binding, + Map globals, + @Nullable EmbeddableText text + ) + } 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..fc91f9d 100644 --- a/lib/src/main/groovy/com/jessebrault/ssg/template/GspTemplateRenderer.groovy +++ b/lib/src/main/groovy/com/jessebrault/ssg/template/GspTemplateRenderer.groovy @@ -3,7 +3,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.EmbeddableText import com.jessebrault.ssg.text.FrontMatter +import com.jessebrault.ssg.text.Text import groovy.text.GStringTemplateEngine import groovy.text.TemplateEngine import groovy.transform.EqualsAndHashCode @@ -19,19 +21,21 @@ class GspTemplateRenderer implements TemplateRenderer { Tuple2, String> render( Template template, FrontMatter frontMatter, - String text, + Text text, Collection parts, Map globals ) { try { Collection diagnostics = [] + def onDiagnostics = { Collection partDiagnostics -> + diagnostics.addAll(partDiagnostics) + } + def embeddableText = new EmbeddableText(text, globals, onDiagnostics) def result = engine.createTemplate(template.text).make([ frontMatter: frontMatter, globals: globals, - parts: new EmbeddablePartsMap(parts, globals, { Collection partDiagnostics -> - diagnostics.addAll(partDiagnostics) - }), - text: text + parts: new EmbeddablePartsMap(parts, globals, onDiagnostics, embeddableText), + text: embeddableText ]) new Tuple2<>(diagnostics, result.toString()) } catch (Exception e) { diff --git a/lib/src/main/groovy/com/jessebrault/ssg/template/TemplateRenderer.groovy b/lib/src/main/groovy/com/jessebrault/ssg/template/TemplateRenderer.groovy index 71e2b5a..62d2021 100644 --- a/lib/src/main/groovy/com/jessebrault/ssg/template/TemplateRenderer.groovy +++ b/lib/src/main/groovy/com/jessebrault/ssg/template/TemplateRenderer.groovy @@ -3,13 +3,17 @@ package com.jessebrault.ssg.template import com.jessebrault.ssg.Diagnostic import com.jessebrault.ssg.part.Part import com.jessebrault.ssg.text.FrontMatter +import com.jessebrault.ssg.text.Text interface TemplateRenderer { + /** + * TODO: get rid of frontMatter param since we can obtain it from the text + */ Tuple2, String> render( Template template, FrontMatter frontMatter, - String text, + Text text, Collection parts, Map globals ) diff --git a/lib/src/test/groovy/com/jessebrault/ssg/SimpleStaticSiteGeneratorIntegrationTests.groovy b/lib/src/test/groovy/com/jessebrault/ssg/SimpleStaticSiteGeneratorIntegrationTests.groovy index dee6973..4ca52bc 100644 --- a/lib/src/test/groovy/com/jessebrault/ssg/SimpleStaticSiteGeneratorIntegrationTests.groovy +++ b/lib/src/test/groovy/com/jessebrault/ssg/SimpleStaticSiteGeneratorIntegrationTests.groovy @@ -61,7 +61,7 @@ class SimpleStaticSiteGeneratorIntegrationTests { @Test void simple() { new File(this.textsDir, 'test.md').write('---\ntemplate: test.gsp\n---\n**Hello, World!**') - new File(this.templatesDir, 'test.gsp').write('<%= text %>') + new File(this.templatesDir, 'test.gsp').write('<%= text.render() %>') def result = this.ssg.generate(this.build) @@ -81,7 +81,7 @@ class SimpleStaticSiteGeneratorIntegrationTests { } } - new File(this.templatesDir, 'nested.gsp').write('<%= text %>') + new File(this.templatesDir, 'nested.gsp').write('<%= text.render() %>') def result = this.ssg.generate(this.build) diff --git a/lib/src/test/groovy/com/jessebrault/ssg/part/GspPartRendererTests.groovy b/lib/src/test/groovy/com/jessebrault/ssg/part/GspPartRendererTests.groovy index d905341..724b329 100644 --- a/lib/src/test/groovy/com/jessebrault/ssg/part/GspPartRendererTests.groovy +++ b/lib/src/test/groovy/com/jessebrault/ssg/part/GspPartRendererTests.groovy @@ -1,18 +1,34 @@ package com.jessebrault.ssg.part +import com.jessebrault.ssg.Diagnostic +import com.jessebrault.ssg.text.* import org.junit.jupiter.api.Test import static org.junit.jupiter.api.Assertions.assertEquals import static org.junit.jupiter.api.Assertions.assertTrue +import static org.mockito.ArgumentMatchers.any +import static org.mockito.Mockito.mock +import static org.mockito.Mockito.when class GspPartRendererTests { + /** + * TODO: move to a fixture + */ + private static Text mockRenderableText(String text) { + def textRenderer = mock(TextRenderer) + when(textRenderer.render(any(), any())).thenReturn(new Tuple2<>([], text)) + def frontMatterGetter = mock(FrontMatterGetter) + def excerptGetter = mock(ExcerptGetter) + new Text('', '', new TextType([], textRenderer, frontMatterGetter, excerptGetter)) + } + private final PartRenderer renderer = new GspPartRenderer() @Test void rendersWithNoBindingOrGlobals() { def part = new Part('', null, 'Hello, World!') - def r = this.renderer.render(part, [:], [:]) + def r = this.renderer.render(part, [:], [:], null) assertTrue(r.v1.size() == 0) assertEquals('Hello, World!', r.v2) } @@ -20,7 +36,7 @@ class GspPartRendererTests { @Test void rendersWithBinding() { def part = new Part('', null, "<%= binding['greeting'] %>") - def r = this.renderer.render(part, [greeting: 'Hello, World!'], [:]) + def r = this.renderer.render(part, [greeting: 'Hello, World!'], [:], null) assertTrue(r.v1.size() == 0) assertEquals('Hello, World!', r.v2) } @@ -28,9 +44,28 @@ class GspPartRendererTests { @Test void rendersWithGlobals() { def part = new Part(null, null, "<%= globals['greeting'] %>") - def r = this.renderer.render(part, [:], [greeting: 'Hello, World!']) + def r = this.renderer.render(part, [:], [greeting: 'Hello, World!'], null) assertTrue(r.v1.size() == 0) assertEquals('Hello, World!', r.v2) } + @Test + void textAvailable() { + def part = new Part('', null, '<%= text.render() %>') + def textDiagnostics = [] + def r = this.renderer.render( + part, + [:], + [:], + new EmbeddableText( + mockRenderableText('Hello, World!'), + [:], + { Collection diagnostics -> textDiagnostics.addAll(diagnostics) } + ) + ) + assertTrue(textDiagnostics.isEmpty()) + assertTrue(r.v1.isEmpty()) + assertEquals('Hello, World!', r.v2) + } + } diff --git a/lib/src/test/groovy/com/jessebrault/ssg/specialpage/GspSpecialPageRendererTests.groovy b/lib/src/test/groovy/com/jessebrault/ssg/specialpage/GspSpecialPageRendererTests.groovy index 15c3b20..d949a37 100644 --- a/lib/src/test/groovy/com/jessebrault/ssg/specialpage/GspSpecialPageRendererTests.groovy +++ b/lib/src/test/groovy/com/jessebrault/ssg/specialpage/GspSpecialPageRendererTests.groovy @@ -3,11 +3,7 @@ package com.jessebrault.ssg.specialpage import com.jessebrault.ssg.part.Part import com.jessebrault.ssg.part.PartRenderer import com.jessebrault.ssg.part.PartType -import com.jessebrault.ssg.text.FrontMatterGetter -import com.jessebrault.ssg.text.MarkdownExcerptGetter -import com.jessebrault.ssg.text.Text -import com.jessebrault.ssg.text.TextRenderer -import com.jessebrault.ssg.text.TextType +import com.jessebrault.ssg.text.* import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith import org.mockito.Mock @@ -35,7 +31,7 @@ class GspSpecialPageRendererTests { @Test void rendersPartWithNoBinding(@Mock PartRenderer partRenderer) { - when(partRenderer.render(any(), any(), any())).thenReturn(new Tuple2<>([], 'Hello, World!')) + when(partRenderer.render(any(), any(), any(), any())).thenReturn(new Tuple2<>([], 'Hello, World!')) def partType = new PartType([], partRenderer) def part = new Part('test', partType , '') @@ -47,11 +43,16 @@ class GspSpecialPageRendererTests { @Test void rendersPartWithBinding(@Mock PartRenderer partRenderer) { - when(partRenderer.render(any(), argThat { Map m -> m.get('greeting') == 'Hello, World!'}, any())).thenReturn(new Tuple2<>([], 'Hello, World!')) + when(partRenderer.render(any(), argThat { Map m -> m.get('greeting') == 'Hello, World!'}, any(), any())) + .thenReturn(new Tuple2<>([], 'Hello, World!')) def partType = new PartType([], partRenderer) def part = new Part('test', partType, '') - def specialPage = new SpecialPage("<%= parts['test'].render([greeting: 'Hello, World!'])", null, null) + def specialPage = new SpecialPage( + "<%= parts['test'].render([greeting: 'Hello, World!'])", + null, + null + ) def r = this.renderer.render(specialPage, [], [part], [:]) assertTrue(r.v1.size() == 0) assertEquals('Hello, World!', r.v2) @@ -59,11 +60,16 @@ class GspSpecialPageRendererTests { @Test void rendersText(@Mock TextRenderer textRenderer, @Mock FrontMatterGetter frontMatterGetter) { - when(textRenderer.render(any(), any())).thenReturn(new Tuple2<>([], '

Hello, World!

\n')) + when(textRenderer.render(any(), any())) + .thenReturn(new Tuple2<>([], '

Hello, World!

\n')) def textType = new TextType([], textRenderer, frontMatterGetter, new MarkdownExcerptGetter()) def text = new Text('', 'test', textType) - def specialPage = new SpecialPage("<%= texts.find { it.path == 'test' }.render() %>", null, null) + def specialPage = new SpecialPage( + "<%= texts.find { it.path == 'test' }.render() %>", + null, + null + ) def r = this.renderer.render(specialPage, [text], [], [:]) assertTrue(r.v1.size() == 0) assertEquals('

Hello, World!

\n', r.v2) diff --git a/lib/src/test/groovy/com/jessebrault/ssg/template/GspTemplateRendererTests.groovy b/lib/src/test/groovy/com/jessebrault/ssg/template/GspTemplateRendererTests.groovy index 8fac879..d0c6470 100644 --- a/lib/src/test/groovy/com/jessebrault/ssg/template/GspTemplateRendererTests.groovy +++ b/lib/src/test/groovy/com/jessebrault/ssg/template/GspTemplateRendererTests.groovy @@ -3,7 +3,12 @@ package com.jessebrault.ssg.template import com.jessebrault.ssg.part.Part import com.jessebrault.ssg.part.PartRenderer import com.jessebrault.ssg.part.PartType +import com.jessebrault.ssg.text.ExcerptGetter import com.jessebrault.ssg.text.FrontMatter +import com.jessebrault.ssg.text.FrontMatterGetter +import com.jessebrault.ssg.text.Text +import com.jessebrault.ssg.text.TextRenderer +import com.jessebrault.ssg.text.TextType import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith import org.mockito.Mock @@ -13,11 +18,33 @@ import static org.junit.jupiter.api.Assertions.assertEquals import static org.junit.jupiter.api.Assertions.assertTrue import static org.mockito.ArgumentMatchers.any import static org.mockito.ArgumentMatchers.argThat +import static org.mockito.Mockito.mock import static org.mockito.Mockito.when @ExtendWith(MockitoExtension) class GspTemplateRendererTests { + /** + * TODO: move to a fixture + */ + private static Text mockBlankText() { + def textRenderer = mock(TextRenderer) + def frontMatterGetter = mock(FrontMatterGetter) + def excerptGetter = mock(ExcerptGetter) + new Text('', '', new TextType([], textRenderer, frontMatterGetter, excerptGetter)) + } + + /** + * TODO: move to a fixture + */ + private static Text mockRenderableText(String text) { + def textRenderer = mock(TextRenderer) + when(textRenderer.render(any(), any())).thenReturn(new Tuple2<>([], text)) + def frontMatterGetter = mock(FrontMatterGetter) + def excerptGetter = mock(ExcerptGetter) + new Text('', '', new TextType([], textRenderer, frontMatterGetter, excerptGetter)) + } + private final TemplateRenderer renderer = new GspTemplateRenderer() @Test @@ -28,11 +55,17 @@ class GspTemplateRendererTests { null ) - when(partRenderer.render(any(), any(), any())).thenReturn(new Tuple2<>([], 'Hello, World!')) + when(partRenderer.render(any(), any(), any(), any())).thenReturn(new Tuple2<>([], 'Hello, World!')) def partType = new PartType([], partRenderer) def part = new Part('test', partType, null) - def r = this.renderer.render(template, new FrontMatter(null, [:]), '', [part], [:]) + def r = this.renderer.render( + template, + new FrontMatter(null, [:]), + mockBlankText(), + [part], + [:] + ) assertTrue(r.v1.size() == 0) assertEquals('Hello, World!', r.v2) } @@ -45,11 +78,18 @@ class GspTemplateRendererTests { null ) - when(partRenderer.render(any(), argThat { Map m -> m.get('person') == 'World' }, any())).thenReturn(new Tuple2<>([], 'Hello, World!')) + when(partRenderer.render(any(), argThat { Map m -> m.get('person') == 'World' }, any(), any())) + .thenReturn(new Tuple2<>([], 'Hello, World!')) def partType = new PartType([], partRenderer) def part = new Part('greeting', partType, null) - def r = this.renderer.render(template, new FrontMatter(null, [:]), '', [part], [:]) + def r = this.renderer.render( + template, + new FrontMatter(null, [:]), + mockBlankText(), + [part], + [:] + ) assertTrue(r.v1.size() == 0) assertEquals('Hello, World!', r.v2) } @@ -57,7 +97,13 @@ class GspTemplateRendererTests { @Test void rendersFrontMatter() { def template = new Template("<%= frontMatter['title'] %>", null, null) - def r = this.renderer.render(template, new FrontMatter(null, [title: ['Hello!']]), '', [], [:]) + def r = this.renderer.render( + template, + new FrontMatter(null, [title: ['Hello!']]), + mockBlankText(), + [], + [:] + ) assertTrue(r.v1.size() == 0) assertEquals('Hello!', r.v2) } @@ -65,15 +111,27 @@ class GspTemplateRendererTests { @Test void rendersGlobal() { def template = new Template("<%= globals['test'] %>", null, null) - def r = this.renderer.render(template, new FrontMatter(null, [:]), '', [], [test: 'Hello, World!']) + def r = this.renderer.render( + template, + new FrontMatter(null, [:]), + mockBlankText(), + [], + [test: 'Hello, World!'] + ) assertTrue(r.v1.size() == 0) assertEquals('Hello, World!', r.v2) } @Test - void rendersText() { - def template = new Template('<%= text %>', null, null) - def r = this.renderer.render(template, new FrontMatter(null, [:]), 'Hello, World!', [], [:]) + void textAvailableToRender() { + def template = new Template('<%= text.render() %>', null, null) + def r = this.renderer.render( + template, + new FrontMatter(null, [:]), + mockRenderableText('Hello, World!'), + [], + [:] + ) assertTrue(r.v1.size() == 0) assertEquals('Hello, World!', r.v2) }