Templates/Parts now have access to an EmbeddableText object.

This commit is contained in:
Jesse Brault 2023-02-11 11:24:14 +01:00
parent a1f1871d1a
commit 34d9cd52b0
12 changed files with 197 additions and 53 deletions

View File

@ -7,6 +7,9 @@ repositories {
} }
dependencies { dependencies {
// https://mvnrepository.com/artifact/org.jetbrains/annotations
api 'org.jetbrains:annotations:24.0.0'
/** /**
* Logging * Logging
*/ */

View File

@ -69,21 +69,14 @@ class SimpleStaticSiteGenerator implements StaticSiteGenerator {
} }
logger.debug('found template: {}', template) 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 // 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 String renderedTemplate
if (templateRenderResult.v1.size() > 0) { if (templateRenderResult.v1.size() > 0) {
diagnostics.addAll(templateRenderResult.v1) diagnostics.addAll(templateRenderResult.v1)

View File

@ -1,8 +1,10 @@
package com.jessebrault.ssg.part package com.jessebrault.ssg.part
import com.jessebrault.ssg.text.EmbeddableText
import groovy.transform.EqualsAndHashCode import groovy.transform.EqualsAndHashCode
import groovy.transform.NullCheck import groovy.transform.NullCheck
import groovy.transform.TupleConstructor import groovy.transform.TupleConstructor
import org.jetbrains.annotations.Nullable
@TupleConstructor(includeFields = true, defaults = false) @TupleConstructor(includeFields = true, defaults = false)
@NullCheck @NullCheck
@ -13,8 +15,16 @@ class EmbeddablePart {
private final Map globals private final Map globals
private final Closure onDiagnostics private final Closure onDiagnostics
@Nullable
private final EmbeddableText text
String render(Map binding = [:]) { 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) { if (result.v1.size() > 0) {
this.onDiagnostics.call(result.v1) this.onDiagnostics.call(result.v1)
'' ''

View File

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

View File

@ -1,9 +1,11 @@
package com.jessebrault.ssg.part package com.jessebrault.ssg.part
import com.jessebrault.ssg.Diagnostic import com.jessebrault.ssg.Diagnostic
import com.jessebrault.ssg.text.EmbeddableText
import groovy.text.GStringTemplateEngine import groovy.text.GStringTemplateEngine
import groovy.text.TemplateEngine import groovy.text.TemplateEngine
import groovy.transform.EqualsAndHashCode import groovy.transform.EqualsAndHashCode
import org.jetbrains.annotations.Nullable
@EqualsAndHashCode @EqualsAndHashCode
class GspPartRenderer implements PartRenderer { class GspPartRenderer implements PartRenderer {
@ -11,15 +13,27 @@ class GspPartRenderer implements PartRenderer {
private static final TemplateEngine engine = new GStringTemplateEngine() private static final TemplateEngine engine = new GStringTemplateEngine()
@Override @Override
Tuple2<Collection<Diagnostic>, String> render(Part part, Map binding, Map globals) { Tuple2<Collection<Diagnostic>, String> render(
Part part,
Map binding,
Map globals,
@Nullable EmbeddableText text = null
) {
Objects.requireNonNull(part)
Objects.requireNonNull(binding)
Objects.requireNonNull(globals)
try { try {
def result = engine.createTemplate(part.text).make([ def result = engine.createTemplate(part.text).make([
binding: binding, binding: binding,
globals: globals globals: globals,
text: text
]) ])
new Tuple2<>([], result.toString()) new Tuple2<>([], result.toString())
} catch (Exception e) { } 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)],
''
)
} }
} }

View File

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

View File

@ -3,7 +3,9 @@ package com.jessebrault.ssg.template
import com.jessebrault.ssg.Diagnostic import com.jessebrault.ssg.Diagnostic
import com.jessebrault.ssg.part.EmbeddablePartsMap import com.jessebrault.ssg.part.EmbeddablePartsMap
import com.jessebrault.ssg.part.Part import com.jessebrault.ssg.part.Part
import com.jessebrault.ssg.text.EmbeddableText
import com.jessebrault.ssg.text.FrontMatter import com.jessebrault.ssg.text.FrontMatter
import com.jessebrault.ssg.text.Text
import groovy.text.GStringTemplateEngine import groovy.text.GStringTemplateEngine
import groovy.text.TemplateEngine import groovy.text.TemplateEngine
import groovy.transform.EqualsAndHashCode import groovy.transform.EqualsAndHashCode
@ -19,19 +21,21 @@ class GspTemplateRenderer implements TemplateRenderer {
Tuple2<Collection<Diagnostic>, String> render( Tuple2<Collection<Diagnostic>, String> render(
Template template, Template template,
FrontMatter frontMatter, FrontMatter frontMatter,
String text, Text text,
Collection<Part> parts, Collection<Part> parts,
Map globals Map globals
) { ) {
try { try {
Collection<Diagnostic> diagnostics = [] Collection<Diagnostic> diagnostics = []
def onDiagnostics = { Collection<Diagnostic> partDiagnostics ->
diagnostics.addAll(partDiagnostics)
}
def embeddableText = new EmbeddableText(text, globals, onDiagnostics)
def result = engine.createTemplate(template.text).make([ def result = engine.createTemplate(template.text).make([
frontMatter: frontMatter, frontMatter: frontMatter,
globals: globals, globals: globals,
parts: new EmbeddablePartsMap(parts, globals, { Collection<Diagnostic> partDiagnostics -> parts: new EmbeddablePartsMap(parts, globals, onDiagnostics, embeddableText),
diagnostics.addAll(partDiagnostics) text: embeddableText
}),
text: text
]) ])
new Tuple2<>(diagnostics, result.toString()) new Tuple2<>(diagnostics, result.toString())
} catch (Exception e) { } catch (Exception e) {

View File

@ -3,13 +3,17 @@ package com.jessebrault.ssg.template
import com.jessebrault.ssg.Diagnostic import com.jessebrault.ssg.Diagnostic
import com.jessebrault.ssg.part.Part import com.jessebrault.ssg.part.Part
import com.jessebrault.ssg.text.FrontMatter import com.jessebrault.ssg.text.FrontMatter
import com.jessebrault.ssg.text.Text
interface TemplateRenderer { interface TemplateRenderer {
/**
* TODO: get rid of frontMatter param since we can obtain it from the text
*/
Tuple2<Collection<Diagnostic>, String> render( Tuple2<Collection<Diagnostic>, String> render(
Template template, Template template,
FrontMatter frontMatter, FrontMatter frontMatter,
String text, Text text,
Collection<Part> parts, Collection<Part> parts,
Map globals Map globals
) )

View File

@ -61,7 +61,7 @@ class SimpleStaticSiteGeneratorIntegrationTests {
@Test @Test
void simple() { void simple() {
new File(this.textsDir, 'test.md').write('---\ntemplate: test.gsp\n---\n**Hello, World!**') 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) 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) def result = this.ssg.generate(this.build)

View File

@ -1,18 +1,34 @@
package com.jessebrault.ssg.part package com.jessebrault.ssg.part
import com.jessebrault.ssg.Diagnostic
import com.jessebrault.ssg.text.*
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import static org.junit.jupiter.api.Assertions.assertEquals import static org.junit.jupiter.api.Assertions.assertEquals
import static org.junit.jupiter.api.Assertions.assertTrue 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 { 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() private final PartRenderer renderer = new GspPartRenderer()
@Test @Test
void rendersWithNoBindingOrGlobals() { void rendersWithNoBindingOrGlobals() {
def part = new Part('', null, 'Hello, World!') 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) assertTrue(r.v1.size() == 0)
assertEquals('Hello, World!', r.v2) assertEquals('Hello, World!', r.v2)
} }
@ -20,7 +36,7 @@ class GspPartRendererTests {
@Test @Test
void rendersWithBinding() { void rendersWithBinding() {
def part = new Part('', null, "<%= binding['greeting'] %>") 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) assertTrue(r.v1.size() == 0)
assertEquals('Hello, World!', r.v2) assertEquals('Hello, World!', r.v2)
} }
@ -28,9 +44,28 @@ class GspPartRendererTests {
@Test @Test
void rendersWithGlobals() { void rendersWithGlobals() {
def part = new Part(null, null, "<%= globals['greeting'] %>") 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) assertTrue(r.v1.size() == 0)
assertEquals('Hello, World!', r.v2) 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<Diagnostic> diagnostics -> textDiagnostics.addAll(diagnostics) }
)
)
assertTrue(textDiagnostics.isEmpty())
assertTrue(r.v1.isEmpty())
assertEquals('Hello, World!', r.v2)
}
} }

View File

@ -3,11 +3,7 @@ package com.jessebrault.ssg.specialpage
import com.jessebrault.ssg.part.Part import com.jessebrault.ssg.part.Part
import com.jessebrault.ssg.part.PartRenderer import com.jessebrault.ssg.part.PartRenderer
import com.jessebrault.ssg.part.PartType import com.jessebrault.ssg.part.PartType
import com.jessebrault.ssg.text.FrontMatterGetter import com.jessebrault.ssg.text.*
import com.jessebrault.ssg.text.MarkdownExcerptGetter
import com.jessebrault.ssg.text.Text
import com.jessebrault.ssg.text.TextRenderer
import com.jessebrault.ssg.text.TextType
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith import org.junit.jupiter.api.extension.ExtendWith
import org.mockito.Mock import org.mockito.Mock
@ -35,7 +31,7 @@ class GspSpecialPageRendererTests {
@Test @Test
void rendersPartWithNoBinding(@Mock PartRenderer partRenderer) { 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 partType = new PartType([], partRenderer)
def part = new Part('test', partType , '') def part = new Part('test', partType , '')
@ -47,11 +43,16 @@ class GspSpecialPageRendererTests {
@Test @Test
void rendersPartWithBinding(@Mock PartRenderer partRenderer) { 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 partType = new PartType([], partRenderer)
def part = new Part('test', partType, '') 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], [:]) def r = this.renderer.render(specialPage, [], [part], [:])
assertTrue(r.v1.size() == 0) assertTrue(r.v1.size() == 0)
assertEquals('Hello, World!', r.v2) assertEquals('Hello, World!', r.v2)
@ -59,11 +60,16 @@ class GspSpecialPageRendererTests {
@Test @Test
void rendersText(@Mock TextRenderer textRenderer, @Mock FrontMatterGetter frontMatterGetter) { void rendersText(@Mock TextRenderer textRenderer, @Mock FrontMatterGetter frontMatterGetter) {
when(textRenderer.render(any(), any())).thenReturn(new Tuple2<>([], '<p><strong>Hello, World!</strong></p>\n')) when(textRenderer.render(any(), any()))
.thenReturn(new Tuple2<>([], '<p><strong>Hello, World!</strong></p>\n'))
def textType = new TextType([], textRenderer, frontMatterGetter, new MarkdownExcerptGetter()) def textType = new TextType([], textRenderer, frontMatterGetter, new MarkdownExcerptGetter())
def text = new Text('', 'test', textType) 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], [], [:]) def r = this.renderer.render(specialPage, [text], [], [:])
assertTrue(r.v1.size() == 0) assertTrue(r.v1.size() == 0)
assertEquals('<p><strong>Hello, World!</strong></p>\n', r.v2) assertEquals('<p><strong>Hello, World!</strong></p>\n', r.v2)

View File

@ -3,7 +3,12 @@ package com.jessebrault.ssg.template
import com.jessebrault.ssg.part.Part import com.jessebrault.ssg.part.Part
import com.jessebrault.ssg.part.PartRenderer import com.jessebrault.ssg.part.PartRenderer
import com.jessebrault.ssg.part.PartType import com.jessebrault.ssg.part.PartType
import com.jessebrault.ssg.text.ExcerptGetter
import com.jessebrault.ssg.text.FrontMatter 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.Test
import org.junit.jupiter.api.extension.ExtendWith import org.junit.jupiter.api.extension.ExtendWith
import org.mockito.Mock 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.junit.jupiter.api.Assertions.assertTrue
import static org.mockito.ArgumentMatchers.any import static org.mockito.ArgumentMatchers.any
import static org.mockito.ArgumentMatchers.argThat import static org.mockito.ArgumentMatchers.argThat
import static org.mockito.Mockito.mock
import static org.mockito.Mockito.when import static org.mockito.Mockito.when
@ExtendWith(MockitoExtension) @ExtendWith(MockitoExtension)
class GspTemplateRendererTests { 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() private final TemplateRenderer renderer = new GspTemplateRenderer()
@Test @Test
@ -28,11 +55,17 @@ class GspTemplateRendererTests {
null 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 partType = new PartType([], partRenderer)
def part = new Part('test', partType, null) 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) assertTrue(r.v1.size() == 0)
assertEquals('Hello, World!', r.v2) assertEquals('Hello, World!', r.v2)
} }
@ -45,11 +78,18 @@ class GspTemplateRendererTests {
null 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 partType = new PartType([], partRenderer)
def part = new Part('greeting', partType, null) 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) assertTrue(r.v1.size() == 0)
assertEquals('Hello, World!', r.v2) assertEquals('Hello, World!', r.v2)
} }
@ -57,7 +97,13 @@ class GspTemplateRendererTests {
@Test @Test
void rendersFrontMatter() { void rendersFrontMatter() {
def template = new Template("<%= frontMatter['title'] %>", null, null) 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) assertTrue(r.v1.size() == 0)
assertEquals('Hello!', r.v2) assertEquals('Hello!', r.v2)
} }
@ -65,15 +111,27 @@ class GspTemplateRendererTests {
@Test @Test
void rendersGlobal() { void rendersGlobal() {
def template = new Template("<%= globals['test'] %>", null, null) 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) assertTrue(r.v1.size() == 0)
assertEquals('Hello, World!', r.v2) assertEquals('Hello, World!', r.v2)
} }
@Test @Test
void rendersText() { void textAvailableToRender() {
def template = new Template('<%= text %>', null, null) def template = new Template('<%= text.render() %>', null, null)
def r = this.renderer.render(template, new FrontMatter(null, [:]), 'Hello, World!', [], [:]) def r = this.renderer.render(
template,
new FrontMatter(null, [:]),
mockRenderableText('Hello, World!'),
[],
[:]
)
assertTrue(r.v1.size() == 0) assertTrue(r.v1.size() == 0)
assertEquals('Hello, World!', r.v2) assertEquals('Hello, World!', r.v2)
} }