Working on new Template
This commit is contained in:
		
							parent
							
								
									2097e280e3
								
							
						
					
					
						commit
						8d37d8883d
					
				| @ -1,4 +0,0 @@ | |||||||
| subprojects { |  | ||||||
|     group = 'com.jessebrault.ssg' |  | ||||||
|     version = '0.0.1' |  | ||||||
| } |  | ||||||
							
								
								
									
										26
									
								
								buildSrc/build.gradle
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								buildSrc/build.gradle
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,26 @@ | |||||||
|  | plugins { | ||||||
|  |     id 'groovy-gradle-plugin' | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | repositories { | ||||||
|  |     maven { | ||||||
|  |         url 'https://archiva.jessebrault.com/repository/internal/' | ||||||
|  | 
 | ||||||
|  |         credentials { | ||||||
|  |             username System.getenv('JBARCHIVA_USERNAME') | ||||||
|  |             password System.getenv('JBARCHIVA_PASSWORD') | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     maven { | ||||||
|  |         url 'https://archiva.jessebrault.com/repository/snapshots/' | ||||||
|  | 
 | ||||||
|  |         credentials { | ||||||
|  |             username System.getenv('JBARCHIVA_USERNAME') | ||||||
|  |             password System.getenv('JBARCHIVA_PASSWORD') | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | dependencies { | ||||||
|  |     implementation 'com.jessebrault.jbarchiva:jbarchiva:0.0.2' | ||||||
|  | } | ||||||
							
								
								
									
										29
									
								
								buildSrc/src/main/groovy/ssg.common.gradle
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								buildSrc/src/main/groovy/ssg.common.gradle
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,29 @@ | |||||||
|  | plugins { | ||||||
|  |     id 'com.jessebrault.jbarchiva' | ||||||
|  |     id 'groovy' | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | group 'com.jessebrault.ssg' | ||||||
|  | version '0.0.1' | ||||||
|  | 
 | ||||||
|  | repositories { | ||||||
|  |     mavenCentral() | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | dependencies { | ||||||
|  |     // https://mvnrepository.com/artifact/org.apache.groovy/groovy | ||||||
|  |     implementation 'org.apache.groovy:groovy:4.0.7' | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * TESTING | ||||||
|  |      */ | ||||||
|  |     // https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter-api | ||||||
|  |     testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.2' | ||||||
|  | 
 | ||||||
|  |     // https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter-engine | ||||||
|  |     testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.2' | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | test { | ||||||
|  |     useJUnitPlatform() | ||||||
|  | } | ||||||
							
								
								
									
										33
									
								
								buildSrc/src/main/groovy/ssg.lib.gradle
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								buildSrc/src/main/groovy/ssg.lib.gradle
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,33 @@ | |||||||
|  | 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' | ||||||
|  | } | ||||||
| @ -1,5 +1,5 @@ | |||||||
| plugins { | plugins { | ||||||
|     id 'groovy' |     id 'ssg.common' | ||||||
|     id 'application' |     id 'application' | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| @ -10,9 +10,6 @@ repositories { | |||||||
| dependencies { | dependencies { | ||||||
|     implementation project(':lib') |     implementation project(':lib') | ||||||
| 
 | 
 | ||||||
|     // https://mvnrepository.com/artifact/org.apache.groovy/groovy |  | ||||||
|     implementation 'org.apache.groovy:groovy:4.0.7' |  | ||||||
| 
 |  | ||||||
|     // https://mvnrepository.com/artifact/info.picocli/picocli |     // https://mvnrepository.com/artifact/info.picocli/picocli | ||||||
|     implementation 'info.picocli:picocli:4.7.0' |     implementation 'info.picocli:picocli:4.7.0' | ||||||
| 
 | 
 | ||||||
| @ -27,15 +24,6 @@ dependencies { | |||||||
| 
 | 
 | ||||||
|     // https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-core |     // https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-core | ||||||
|     implementation 'org.apache.logging.log4j:log4j-core:2.19.0' |     implementation 'org.apache.logging.log4j:log4j-core:2.19.0' | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * TESTING |  | ||||||
|      */ |  | ||||||
|     // https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter-api |  | ||||||
|     testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.2' |  | ||||||
| 
 |  | ||||||
|     // https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter-engine |  | ||||||
|     testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.2' |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| application { | application { | ||||||
| @ -49,10 +37,7 @@ jar { | |||||||
| 
 | 
 | ||||||
| distributions { | distributions { | ||||||
|     main { |     main { | ||||||
|  |         //noinspection GroovyAssignabilityCheck | ||||||
|         distributionBaseName = 'ssg' |         distributionBaseName = 'ssg' | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 |  | ||||||
| test { |  | ||||||
|     useJUnitPlatform() |  | ||||||
| } |  | ||||||
| @ -1,5 +1,6 @@ | |||||||
| plugins { | plugins { | ||||||
|     id 'groovy' |     id 'ssg.common' | ||||||
|  |     id 'ssg.lib' | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| repositories { | repositories { | ||||||
| @ -7,9 +8,6 @@ repositories { | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| dependencies { | dependencies { | ||||||
|     // https://mvnrepository.com/artifact/org.apache.groovy/groovy |  | ||||||
|     implementation 'org.apache.groovy:groovy:4.0.7' |  | ||||||
| 
 |  | ||||||
|     // https://mvnrepository.com/artifact/org.apache.groovy/groovy-templates |     // https://mvnrepository.com/artifact/org.apache.groovy/groovy-templates | ||||||
|     implementation 'org.apache.groovy:groovy-templates:4.0.7' |     implementation 'org.apache.groovy:groovy-templates:4.0.7' | ||||||
| 
 | 
 | ||||||
| @ -18,45 +16,8 @@ dependencies { | |||||||
| 
 | 
 | ||||||
|     // https://mvnrepository.com/artifact/org.commonmark/commonmark-ext-yaml-front-matter |     // https://mvnrepository.com/artifact/org.commonmark/commonmark-ext-yaml-front-matter | ||||||
|     implementation 'org.commonmark:commonmark-ext-yaml-front-matter:0.21.0' |     implementation 'org.commonmark:commonmark-ext-yaml-front-matter:0.21.0' | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Logging |  | ||||||
|      */ |  | ||||||
|     // https://mvnrepository.com/artifact/org.slf4j/slf4j-api |  | ||||||
|     implementation '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.8.2' |  | ||||||
| 
 |  | ||||||
|     // https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter-engine |  | ||||||
|     testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.2' |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * 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' |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * 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' |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| jar { | jar { | ||||||
|     archivesBaseName = 'ssg-lib' |     archivesBaseName = 'ssg-lib' | ||||||
| } | } | ||||||
| 
 |  | ||||||
| test { |  | ||||||
|     useJUnitPlatform() |  | ||||||
| } |  | ||||||
| @ -1,2 +1,2 @@ | |||||||
| rootProject.name = 'ssg' | rootProject.name = 'ssg' | ||||||
| include 'cli', 'lib' | include 'cli', 'lib', 'template' | ||||||
							
								
								
									
										26
									
								
								template/build.gradle
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								template/build.gradle
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,26 @@ | |||||||
|  | plugins { | ||||||
|  |     id 'ssg.common' | ||||||
|  |     id 'ssg.lib' | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | repositories { | ||||||
|  |     mavenCentral() | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | dependencies { | ||||||
|  |     // https://mvnrepository.com/artifact/org.apache.groovy/groovy | ||||||
|  |     implementation 'org.apache.groovy:groovy:4.0.7' | ||||||
|  | 
 | ||||||
|  |     // https://mvnrepository.com/artifact/org.apache.groovy/groovy-templates | ||||||
|  |     implementation 'org.apache.groovy:groovy-templates:4.0.7' | ||||||
|  | 
 | ||||||
|  |     // https://archiva.jessebrault.com/#artifact/com.jessebrault.fsm/lib/0.1.0-SNAPSHOT | ||||||
|  |     implementation 'com.jessebrault.fsm:lib:0.1.0-SNAPSHOT' | ||||||
|  | 
 | ||||||
|  |     // https://archiva.jessebrault.com/#artifact/com.jessebrault.fsm/groovy-extension/0.1.0-SNAPSHOT | ||||||
|  |     implementation 'com.jessebrault.fsm:groovy-extension:0.1.0-SNAPSHOT' | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | jar { | ||||||
|  |     archivesBaseName = 'ssg-template' | ||||||
|  | } | ||||||
| @ -0,0 +1,5 @@ | |||||||
|  | package com.jessebrault.ssg.template.gspe | ||||||
|  | 
 | ||||||
|  | interface Component { | ||||||
|  |     String render(Map<String, ?> attr, String body) | ||||||
|  | } | ||||||
| @ -0,0 +1,5 @@ | |||||||
|  | package com.jessebrault.ssg.template.gspe | ||||||
|  | 
 | ||||||
|  | interface ComponentFactory { | ||||||
|  |     Component get() | ||||||
|  | } | ||||||
| @ -0,0 +1,256 @@ | |||||||
|  | package com.jessebrault.ssg.template.gspe | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | import groovy.transform.PackageScope | ||||||
|  | 
 | ||||||
|  | import static com.jessebrault.ssg.template.gspe.ComponentToken.Type.* | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * NOT thread safe | ||||||
|  |  */ | ||||||
|  | @PackageScope | ||||||
|  | class ComponentParser { | ||||||
|  | 
 | ||||||
|  |     private Queue<ComponentToken> tokens | ||||||
|  |     private StringBuilder b | ||||||
|  | 
 | ||||||
|  |     private String identifier | ||||||
|  | 
 | ||||||
|  |     String parse(List<ComponentToken> tokens) { | ||||||
|  |         this.b = new StringBuilder() | ||||||
|  |         this.tokens = new LinkedList<>(tokens) | ||||||
|  | 
 | ||||||
|  |         this.selfClosingComponent() | ||||||
|  | 
 | ||||||
|  |         this.b.toString() | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     String parse(List<ComponentToken> openingTokens, String bodyClosure, List<ComponentToken> closingTokens) { | ||||||
|  |         this.b = new StringBuilder() | ||||||
|  | 
 | ||||||
|  |         this.tokens = new LinkedList<>(openingTokens) | ||||||
|  |         this.openingComponent() | ||||||
|  | 
 | ||||||
|  |         this.b << "bodyOut << ${ bodyClosure };" | ||||||
|  | 
 | ||||||
|  |         this.tokens = new LinkedList<>(closingTokens) | ||||||
|  |         this.closingComponent() | ||||||
|  | 
 | ||||||
|  |         this.b.toString() | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private static void error(Collection<ComponentToken.Type> expectedTypes, ComponentToken actual) { | ||||||
|  |         throw new RuntimeException("expected ${ expectedTypes.join(' or ') } but got ${ actual ? "'${ actual }'" : 'null' }") | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private void selfClosingComponent() { | ||||||
|  |         this.startOfOpeningOrSelfClosingComponent() | ||||||
|  |         this.keysAndValues() | ||||||
|  |         def t0 = this.tokens.poll() | ||||||
|  |         if (!t0 || t0.type != FORWARD_SLASH) { | ||||||
|  |             error([FORWARD_SLASH], t0) | ||||||
|  |         } else { | ||||||
|  |             def t1 = tokens.poll() | ||||||
|  |             if (!t1 || t1.type != GT) { | ||||||
|  |                 error([GT], t1) | ||||||
|  |             } else { | ||||||
|  |                 this.b << '};' | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private void openingComponent() { | ||||||
|  |         this.startOfOpeningOrSelfClosingComponent() | ||||||
|  |         this.keysAndValues() | ||||||
|  |         def t0 = tokens.poll() | ||||||
|  |         if (!t0 || t0.type != GT) { | ||||||
|  |             error([GT], t0) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private void closingComponent() { | ||||||
|  |         def t0 = this.tokens.poll() | ||||||
|  |         if (!t0 || t0.type != LT) { | ||||||
|  |             error([LT], t0) | ||||||
|  |         } else { | ||||||
|  |             def t1 = this.tokens.poll() | ||||||
|  |             if (!t1 || t1.type != FORWARD_SLASH) { | ||||||
|  |                 error([FORWARD_SLASH], t1) | ||||||
|  |             } else { | ||||||
|  |                 def t2 = this.tokens.poll() | ||||||
|  |                 if (!t2 || t2.type != IDENTIFIER) { | ||||||
|  |                     error([IDENTIFIER], t2) | ||||||
|  |                 } else if (t2.text != identifier) { | ||||||
|  |                     throw new RuntimeException("expected '${ this.identifier }' but got '${ t2.text }'") | ||||||
|  |                 } else { | ||||||
|  |                     def t3 = this.tokens.poll() | ||||||
|  |                     if (!t3 || t3.type != GT) { | ||||||
|  |                         error([GT], t3) | ||||||
|  |                     } else { | ||||||
|  |                         this.b << '};' | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private void startOfOpeningOrSelfClosingComponent() { | ||||||
|  |         def t0 = this.tokens.poll() | ||||||
|  |         if (!t0 || t0.type != LT) { | ||||||
|  |             error([LT], t0) | ||||||
|  |         } else { | ||||||
|  |             def t1 = this.tokens.poll() | ||||||
|  |             if (!t1 || t1.type != IDENTIFIER) { | ||||||
|  |                 error([IDENTIFIER], t1) | ||||||
|  |             } else { | ||||||
|  |                 this.identifier = t1.text | ||||||
|  |                 this.b << "renderComponent('${ this.identifier }') { attr, bodyOut ->\n" | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private void keysAndValues() { | ||||||
|  |         while (true) { | ||||||
|  |             def t0 = this.tokens.peek() | ||||||
|  |             if (!t0 || !t0.type.isAnyOf([KEY, FORWARD_SLASH])) { | ||||||
|  |                 error([KEY, FORWARD_SLASH], t0) | ||||||
|  |             } else if (t0.type == KEY) { | ||||||
|  |                 this.keyAndValue() | ||||||
|  |             } else if (t0.type == FORWARD_SLASH) { | ||||||
|  |                 break | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @PeekBefore(KEY) | ||||||
|  |     private void keyAndValue() { | ||||||
|  |         def t0 = this.tokens.remove() | ||||||
|  |         if (t0.type != KEY) { | ||||||
|  |             throw new RuntimeException('programmer error') | ||||||
|  |         } else { | ||||||
|  |             def t1 = this.tokens.poll() | ||||||
|  |             if (!t1 || t1.type != EQUALS) { | ||||||
|  |                 error([EQUALS], t1) | ||||||
|  |             } else { | ||||||
|  |                 this.b << "attr['${ t0.text }'] = " | ||||||
|  |                 this.value() | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private void value() { | ||||||
|  |         def t0 = this.tokens.peek() | ||||||
|  |         if (!t0 || !t0.type.isAnyOf([DOUBLE_QUOTE, SINGLE_QUOTE, DOLLAR, LT])) { | ||||||
|  |             error([DOUBLE_QUOTE, SINGLE_QUOTE, DOLLAR, LT], t0) | ||||||
|  |         } else if (t0.type == DOUBLE_QUOTE) { | ||||||
|  |             this.doubleQuoteStringValue() | ||||||
|  |         } else if (t0.type == SINGLE_QUOTE) { | ||||||
|  |             this.singleQuoteStringValue() | ||||||
|  |         } else if (t0.type == DOLLAR) { | ||||||
|  |             this.dollarValue() | ||||||
|  |         } else if (t0.type == LT) { | ||||||
|  |             this.scriptletValue() | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @PeekBefore(DOUBLE_QUOTE) | ||||||
|  |     private void doubleQuoteStringValue() { | ||||||
|  |         def t0 = this.tokens.remove() | ||||||
|  |         if (t0.type != DOUBLE_QUOTE) { | ||||||
|  |             throw new RuntimeException('programmer error') | ||||||
|  |         } else { | ||||||
|  |             def t1 = this.tokens.poll() | ||||||
|  |             if (!t1 || t1.type != STRING) { | ||||||
|  |                 error([STRING], t1) | ||||||
|  |             } else { | ||||||
|  |                 def t2 = this.tokens.poll() | ||||||
|  |                 if (!t2 || t2.type != DOUBLE_QUOTE) { | ||||||
|  |                     error([DOUBLE_QUOTE], t2) | ||||||
|  |                 } else { | ||||||
|  |                     this.b << /"${ t1.text }";/ + '\n' | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @PeekBefore(SINGLE_QUOTE) | ||||||
|  |     private void singleQuoteStringValue() { | ||||||
|  |         def t0 = this.tokens.remove() | ||||||
|  |         if (t0.type != SINGLE_QUOTE) { | ||||||
|  |             throw new RuntimeException('programmer error') | ||||||
|  |         } else { | ||||||
|  |             def t1 = tokens.poll() | ||||||
|  |             if (!t1 || t1.type != STRING) { | ||||||
|  |                 error([STRING], t1) | ||||||
|  |             } else { | ||||||
|  |                 def t2 = this.tokens.poll() | ||||||
|  |                 if (!t2 || t2.type != SINGLE_QUOTE) { | ||||||
|  |                     error([SINGLE_QUOTE], t2) | ||||||
|  |                 } else { | ||||||
|  |                     this.b << /'${ t1.text }';/ + '\n' | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @PeekBefore(DOLLAR) | ||||||
|  |     private void dollarValue() { | ||||||
|  |         def t0 = this.tokens.remove() | ||||||
|  |         if (t0.type != DOLLAR) { | ||||||
|  |             throw new RuntimeException('programmer error') | ||||||
|  |         } else { | ||||||
|  |             def t1 = this.tokens.poll() | ||||||
|  |             if (!t1 || t1.type != CURLY_OPEN) { | ||||||
|  |                 error([CURLY_OPEN], t1) | ||||||
|  |             } else { | ||||||
|  |                 def t2 = this.tokens.poll() | ||||||
|  |                 if (!t2 || t2.type != GROOVY) { | ||||||
|  |                     error([GROOVY], t2) | ||||||
|  |                 } else { | ||||||
|  |                     def t3 = this.tokens.poll() | ||||||
|  |                     if (!t3 || t3.type != CURLY_CLOSE) { | ||||||
|  |                         error([CURLY_CLOSE], t3) | ||||||
|  |                     } else { | ||||||
|  |                         this.b << "${ t2.text };\n" | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @PeekBefore(LT) | ||||||
|  |     private void scriptletValue() { | ||||||
|  |         def t0 = this.tokens.remove() | ||||||
|  |         if (t0.type != LT) { | ||||||
|  |             throw new RuntimeException('programmer error') | ||||||
|  |         } else { | ||||||
|  |             def t1 = this.tokens.poll() | ||||||
|  |             if (!t1 || t1.type != PERCENT) { | ||||||
|  |                 error([PERCENT], t1) | ||||||
|  |             } else { | ||||||
|  |                 def t2 = this.tokens.poll() | ||||||
|  |                 if (!t2 || t2.type != EQUALS) { | ||||||
|  |                     error([EQUALS], t2) | ||||||
|  |                 } else { | ||||||
|  |                     def t3 = this.tokens.poll() | ||||||
|  |                     if (!t3 || t3.type != GROOVY) { | ||||||
|  |                         error([GROOVY], t3) | ||||||
|  |                     } else { | ||||||
|  |                         def t4 = this.tokens.poll() | ||||||
|  |                         if (!t4.type || t4.type != PERCENT) { | ||||||
|  |                             error([PERCENT], t4) | ||||||
|  |                         } else { | ||||||
|  |                             def t5 = this.tokens.poll() | ||||||
|  |                             if (!t5 || t5.type != GT) { | ||||||
|  |                                 error([GT], t5) | ||||||
|  |                             } else { | ||||||
|  |                                 this.b << "${ t3.text };\n" | ||||||
|  |                             } | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
| @ -0,0 +1,35 @@ | |||||||
|  | package com.jessebrault.ssg.template.gspe | ||||||
|  | 
 | ||||||
|  | import groovy.transform.TupleConstructor | ||||||
|  | 
 | ||||||
|  | @TupleConstructor(defaults = false) | ||||||
|  | class ComponentToken { | ||||||
|  | 
 | ||||||
|  |     enum Type { | ||||||
|  |         LT, | ||||||
|  |         GT, | ||||||
|  |         IDENTIFIER, | ||||||
|  |         KEY, | ||||||
|  |         EQUALS, | ||||||
|  |         DOUBLE_QUOTE, | ||||||
|  |         SINGLE_QUOTE, | ||||||
|  |         STRING, | ||||||
|  |         DOLLAR, | ||||||
|  |         CURLY_OPEN, | ||||||
|  |         GROOVY, | ||||||
|  |         CURLY_CLOSE, | ||||||
|  |         GROOVY_IDENTIFIER, | ||||||
|  |         PERCENT, | ||||||
|  |         FORWARD_SLASH | ||||||
|  |         ; | ||||||
|  | 
 | ||||||
|  |         boolean isAnyOf(Collection<Type> types) { | ||||||
|  |             types.contains(this) | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     Type type | ||||||
|  |     String text | ||||||
|  | 
 | ||||||
|  | } | ||||||
| @ -0,0 +1,176 @@ | |||||||
|  | package com.jessebrault.ssg.template.gspe | ||||||
|  | 
 | ||||||
|  | import com.jessebrault.fsm.function.FunctionFsmBuilder | ||||||
|  | import com.jessebrault.fsm.function.FunctionFsmBuilderImpl | ||||||
|  | 
 | ||||||
|  | import static ComponentToken.Type | ||||||
|  | 
 | ||||||
|  | class ComponentTokenizer { | ||||||
|  | 
 | ||||||
|  |     private static final PatternFunction lessThan = new PatternFunction(~/^</) | ||||||
|  |     private static final PatternFunction greaterThan = new PatternFunction(~/^>/) | ||||||
|  |     private static final PatternFunction identifier = new PatternFunction(~/^\p{Lu}.*?(?=\s|\/)/) | ||||||
|  |     private static final PatternFunction whitespace = new PatternFunction(~/^\s+/) | ||||||
|  |     private static final PatternFunction key = new PatternFunction(~/^[\p{L}0-9_\-]+/) | ||||||
|  |     private static final PatternFunction equals = new PatternFunction(~/^=/) | ||||||
|  |     private static final PatternFunction doubleQuote = new PatternFunction(~/^"/) | ||||||
|  |     private static final PatternFunction doubleQuoteStringContent = new PatternFunction(~/^(?:[\w\W&&[^\\"]]|\\")+/) | ||||||
|  |     private static final PatternFunction singleQuote = new PatternFunction(~/^'/) | ||||||
|  |     private static final PatternFunction singleQuoteStringContent = new PatternFunction(~/^(?:[\w\W&&[^\\']]|\\')+/) | ||||||
|  | 
 | ||||||
|  |     // https://docs.groovy-lang.org/latest/html/documentation/#_identifiers | ||||||
|  |     //'\u00C0' to '\u00D6' | ||||||
|  |     //'\u00D8' to '\u00F6' | ||||||
|  |     //'\u00F8' to '\u00FF' | ||||||
|  |     //'\u0100' to '\uFFFE' | ||||||
|  |     private static final PatternFunction dollarReference = new PatternFunction(~/^\$[a-zA-Z_$\u00c0-\u00d6\u00d8-\u00f6\u00f8-\u00ff\u0100-\ufff3][a-zA-Z_$0-9\u00c0-\u00d6\u00d8-\u00f6\u00f8-\u00ff\u0100-\ufff3]*(?=[\s\/>])/) | ||||||
|  | 
 | ||||||
|  |     private static final PatternFunction dollarOpen = new PatternFunction(~/^\$\{/) | ||||||
|  |     private static final PatternFunction percent = new PatternFunction(~/^%/) | ||||||
|  |     private static final PatternFunction expressionScriptletGroovy = new PatternFunction(~/^.*?%>/) | ||||||
|  |     private static final PatternFunction forwardSlash = new PatternFunction(~/^\//) | ||||||
|  | 
 | ||||||
|  |     static enum State { | ||||||
|  |         START, | ||||||
|  |         DOUBLE_QUOTE_STRING, | ||||||
|  |         SINGLE_QUOTE_STRING, | ||||||
|  |         DOLLAR_GROOVY, | ||||||
|  |         EXPRESSION_SCRIPTLET_GROOVY, | ||||||
|  |         DONE | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private static FunctionFsmBuilder<String, State, String> getFsmBuilder() { | ||||||
|  |         new FunctionFsmBuilderImpl<>() | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     Queue<ComponentToken> tokenize(String src) { | ||||||
|  |         Queue<ComponentToken> tokens = new LinkedList<>() | ||||||
|  | 
 | ||||||
|  |         def fsm = getFsmBuilder().with { | ||||||
|  |             initialState = State.START | ||||||
|  | 
 | ||||||
|  |             whileIn(State.START) { | ||||||
|  |                 on lessThan exec { | ||||||
|  |                     tokens << new ComponentToken(Type.LT, it) | ||||||
|  |                 } | ||||||
|  |                 on greaterThan shiftTo State.DONE exec { | ||||||
|  |                     tokens << new ComponentToken(Type.GT, it) | ||||||
|  |                 } | ||||||
|  |                 on identifier exec { | ||||||
|  |                     tokens << new ComponentToken(Type.IDENTIFIER, it) | ||||||
|  |                 } | ||||||
|  |                 on whitespace exec { } | ||||||
|  |                 on key exec { | ||||||
|  |                     tokens << new ComponentToken(Type.KEY, it) | ||||||
|  |                 } | ||||||
|  |                 on equals exec { | ||||||
|  |                     tokens << new ComponentToken(Type.EQUALS, it) | ||||||
|  |                 } | ||||||
|  |                 on doubleQuote shiftTo State.DOUBLE_QUOTE_STRING exec { | ||||||
|  |                     tokens << new ComponentToken(Type.DOUBLE_QUOTE, it) | ||||||
|  |                 } | ||||||
|  |                 on singleQuote shiftTo State.SINGLE_QUOTE_STRING exec { | ||||||
|  |                     tokens << new ComponentToken(Type.SINGLE_QUOTE, it) | ||||||
|  |                 } | ||||||
|  |                 on dollarReference exec { String s -> | ||||||
|  |                     tokens << new ComponentToken(Type.DOLLAR, s[0]) | ||||||
|  |                     tokens << new ComponentToken(Type.GROOVY_IDENTIFIER, s.substring(1)) | ||||||
|  |                 } | ||||||
|  |                 on dollarOpen shiftTo State.DOLLAR_GROOVY exec { String s -> | ||||||
|  |                     tokens << new ComponentToken(Type.DOLLAR, s[0]) | ||||||
|  |                     tokens << new ComponentToken(Type.CURLY_OPEN, s[1]) | ||||||
|  |                 } | ||||||
|  |                 on percent shiftTo State.EXPRESSION_SCRIPTLET_GROOVY exec { | ||||||
|  |                     tokens << new ComponentToken(Type.PERCENT, it) | ||||||
|  |                 } | ||||||
|  |                 on forwardSlash exec { | ||||||
|  |                     tokens << new ComponentToken(Type.FORWARD_SLASH, it) | ||||||
|  |                 } | ||||||
|  |                 onNoMatch() exec { | ||||||
|  |                     throw new IllegalArgumentException() | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             whileIn(State.DOUBLE_QUOTE_STRING) { | ||||||
|  |                 on doubleQuoteStringContent exec { | ||||||
|  |                     tokens << new ComponentToken(Type.STRING, it) | ||||||
|  |                 } | ||||||
|  |                 on doubleQuote shiftTo State.START exec { | ||||||
|  |                     tokens << new ComponentToken(Type.DOUBLE_QUOTE, it) | ||||||
|  |                 } | ||||||
|  |                 onNoMatch() exec { | ||||||
|  |                     throw new IllegalArgumentException() | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             whileIn(State.SINGLE_QUOTE_STRING) { | ||||||
|  |                 on singleQuoteStringContent exec { | ||||||
|  |                     tokens << new ComponentToken(Type.STRING, it) | ||||||
|  |                 } | ||||||
|  |                 on singleQuote shiftTo State.START exec { | ||||||
|  |                     tokens << new ComponentToken(Type.SINGLE_QUOTE, it) | ||||||
|  |                 } | ||||||
|  |                 onNoMatch() exec { | ||||||
|  |                     throw new IllegalArgumentException() | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             whileIn(State.DOLLAR_GROOVY) { | ||||||
|  |                 on { String input -> | ||||||
|  |                     def b = new StringBuilder() | ||||||
|  |                     def openCurlyCount = 1 | ||||||
|  |                     def iterator = input.iterator() as Iterator<String> | ||||||
|  |                     while (iterator.hasNext()) { | ||||||
|  |                         def c0 = iterator.next() | ||||||
|  |                         if (c0 == '{') { | ||||||
|  |                             b << c0 | ||||||
|  |                             openCurlyCount++ | ||||||
|  |                         } else if (c0 == '}') { | ||||||
|  |                             b << c0 | ||||||
|  |                             openCurlyCount-- | ||||||
|  |                             if (openCurlyCount == 0) { | ||||||
|  |                                 break | ||||||
|  |                             } | ||||||
|  |                         } else { | ||||||
|  |                             b << c0 | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                     b.toString() | ||||||
|  |                 } shiftTo State.START exec { String s -> | ||||||
|  |                     tokens << new ComponentToken(Type.GROOVY, s.substring(0, s.length() - 1)) | ||||||
|  |                     tokens << new ComponentToken(Type.CURLY_CLOSE, '}') | ||||||
|  |                 } | ||||||
|  |                 onNoMatch() exec { | ||||||
|  |                     throw new IllegalArgumentException() | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             whileIn(State.EXPRESSION_SCRIPTLET_GROOVY) { | ||||||
|  |                 on expressionScriptletGroovy shiftTo State.START exec { String s -> | ||||||
|  |                     tokens << new ComponentToken(Type.GROOVY, s.substring(0, s.length() - 2)) | ||||||
|  |                     tokens << new ComponentToken(Type.PERCENT, '%') | ||||||
|  |                     tokens << new ComponentToken(Type.GT, '>') | ||||||
|  |                 } | ||||||
|  |                 onNoMatch() exec { | ||||||
|  |                     throw new IllegalArgumentException() | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             build() | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         def remaining = src | ||||||
|  | 
 | ||||||
|  |         while (fsm.currentState != State.DONE) { | ||||||
|  |             def output = fsm.apply(remaining) | ||||||
|  |             if (!output) { | ||||||
|  |                 throw new IllegalStateException() | ||||||
|  |             } else { | ||||||
|  |                 remaining = remaining.substring(output.length()) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         tokens | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
| @ -0,0 +1,43 @@ | |||||||
|  | package com.jessebrault.ssg.template.gspe | ||||||
|  | 
 | ||||||
|  | import org.slf4j.Logger | ||||||
|  | import org.slf4j.LoggerFactory | ||||||
|  | import org.slf4j.Marker | ||||||
|  | import org.slf4j.MarkerFactory | ||||||
|  | 
 | ||||||
|  | class ComponentsContainer { | ||||||
|  | 
 | ||||||
|  |     private static final Logger logger = LoggerFactory.getLogger(ComponentsContainer) | ||||||
|  |     private static final Marker enter = MarkerFactory.getMarker('ENTER') | ||||||
|  |     private static final Marker exit = MarkerFactory.getMarker('EXIT') | ||||||
|  | 
 | ||||||
|  |     private final Map<String, Component> componentCache = [:] | ||||||
|  |     private final GroovyClassLoader loader | ||||||
|  | 
 | ||||||
|  |     ComponentsContainer(Collection<URL> componentDirUrls, Collection<Component> components) { | ||||||
|  |         logger.trace(enter, 'componentDirUrls: {}, components: {}', componentDirUrls, components) | ||||||
|  |         this.loader = new GroovyClassLoader() | ||||||
|  |         componentDirUrls.each { this.loader.addURL(it) } | ||||||
|  |         components.each { | ||||||
|  |             this.componentCache[it.class.simpleName] = it | ||||||
|  |         } | ||||||
|  |         logger.debug('this.loader: {}', this.loader) | ||||||
|  |         logger.debug('this.componentCache: {}', this.componentCache) | ||||||
|  |         logger.trace(exit, '') | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     Component get(String name) { | ||||||
|  |         logger.trace('name: {}', name) | ||||||
|  |         def component = this.componentCache.computeIfAbsent(name, { | ||||||
|  |             def componentClass = (Class<? extends Component>) this.loader.loadClass(it) | ||||||
|  |             componentClass.getDeclaredConstructor().newInstance() // must be a default constructor (for now) | ||||||
|  |         }) | ||||||
|  |         logger.trace(exit, 'component: {}', component) | ||||||
|  |         component | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     Component getAt(String name) { | ||||||
|  |         this.get(name) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
| @ -0,0 +1,23 @@ | |||||||
|  | package com.jessebrault.ssg.template.gspe | ||||||
|  | 
 | ||||||
|  | import groovy.transform.PackageScope | ||||||
|  | 
 | ||||||
|  | @PackageScope | ||||||
|  | enum FsmState { | ||||||
|  |     HTML, | ||||||
|  |     SCRIPTLET, | ||||||
|  |     EXPRESSION_SCRIPTLET, | ||||||
|  |     DOLLAR, | ||||||
|  | 
 | ||||||
|  |     COMPONENT, | ||||||
|  | 
 | ||||||
|  |     COMPONENT_IDENTIFIER, | ||||||
|  | 
 | ||||||
|  |     COMPONENT_ATTR_KEY, | ||||||
|  |     COMPONENT_ATTR_VALUE_OPEN, | ||||||
|  | 
 | ||||||
|  |     COMPONENT_ATTR_VALUE_STRING, | ||||||
|  |     COMPONENT_ATTR_VALUE_STRING_CLOSE, | ||||||
|  | 
 | ||||||
|  |     COMPONENT_CLOSE | ||||||
|  | } | ||||||
| @ -0,0 +1,32 @@ | |||||||
|  | package com.jessebrault.ssg.template.gspe | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | import groovy.text.Template | ||||||
|  | 
 | ||||||
|  | class GspeTemplate implements Template { | ||||||
|  | 
 | ||||||
|  |     Closure templateClosure | ||||||
|  |     ComponentsContainer components | ||||||
|  | 
 | ||||||
|  |     String renderComponent(String componentName, Closure configureComponentInstance) { | ||||||
|  |         Map<String, String> attr = [:] | ||||||
|  |         def bodyOut = new StringBuilder() | ||||||
|  |         configureComponentInstance(attr, bodyOut) | ||||||
|  | 
 | ||||||
|  |         def component = this.components[componentName] | ||||||
|  |         component.render(attr, bodyOut.toString()) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     final Writable make() { | ||||||
|  |         this.make([:]) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     final Writable make(Map binding) { | ||||||
|  |         def rehydrated = this.templateClosure.rehydrate(binding, this, this).asWritable() | ||||||
|  |         rehydrated.setResolveStrategy(Closure.DELEGATE_FIRST) | ||||||
|  |         rehydrated | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
| @ -0,0 +1,56 @@ | |||||||
|  | package com.jessebrault.ssg.template.gspe | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | import groovy.text.Template | ||||||
|  | import groovy.text.TemplateEngine | ||||||
|  | import groovy.transform.TupleConstructor | ||||||
|  | import org.codehaus.groovy.control.CompilationFailedException | ||||||
|  | import org.slf4j.Logger | ||||||
|  | import org.slf4j.LoggerFactory | ||||||
|  | 
 | ||||||
|  | import java.util.concurrent.atomic.AtomicInteger | ||||||
|  | import java.util.function.Supplier | ||||||
|  | 
 | ||||||
|  | final class GspeTemplateEngine extends TemplateEngine { | ||||||
|  | 
 | ||||||
|  |     private static final Logger logger = LoggerFactory.getLogger(GspeTemplateEngine) | ||||||
|  | 
 | ||||||
|  |     @TupleConstructor(defaults = false) | ||||||
|  |     static class Configuration { | ||||||
|  |         Supplier<GspeTemplate> ssgTemplateSupplier | ||||||
|  |         Collection<URL> componentDirUrls | ||||||
|  |         Collection<Component> components | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private final Configuration configuration | ||||||
|  |     private final File scriptsDir = File.createTempDir() | ||||||
|  |     private final AtomicInteger templateCount = new AtomicInteger(0) | ||||||
|  |     private final GroovyScriptEngine scriptEngine | ||||||
|  | 
 | ||||||
|  |     GspeTemplateEngine(Configuration configuration) { | ||||||
|  |         this.configuration = configuration | ||||||
|  |         this.scriptEngine = new GroovyScriptEngine([this.scriptsDir.toURI().toURL()] as URL[]) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     Template createTemplate(Reader reader) throws CompilationFailedException, ClassNotFoundException, IOException { | ||||||
|  |         def templateSrc = reader.text | ||||||
|  | 
 | ||||||
|  |         def converter = new TemplateToScriptConverter() | ||||||
|  |         def scriptSrc = converter.convert(templateSrc) | ||||||
|  |         logger.debug('scriptSrc: {}', scriptSrc) | ||||||
|  |         def scriptName = "SsgTemplate${ this.templateCount.getAndIncrement() }.groovy" | ||||||
|  |         new File(this.scriptsDir, scriptName).write(scriptSrc) | ||||||
|  | 
 | ||||||
|  |         def script = this.scriptEngine.createScript(scriptName, new Binding()) | ||||||
|  | 
 | ||||||
|  |         def templateClosure = (Closure) script.invokeMethod('getTemplate', null) | ||||||
|  | 
 | ||||||
|  |         def template = this.configuration.ssgTemplateSupplier.get() | ||||||
|  |         template.templateClosure = templateClosure | ||||||
|  |         template.components = new ComponentsContainer(this.configuration.componentDirUrls, this.configuration.components) | ||||||
|  | 
 | ||||||
|  |         template | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
| @ -0,0 +1,26 @@ | |||||||
|  | package com.jessebrault.ssg.template.gspe | ||||||
|  | 
 | ||||||
|  | import groovy.transform.PackageScope | ||||||
|  | import groovy.transform.TupleConstructor | ||||||
|  | 
 | ||||||
|  | import java.util.function.Function | ||||||
|  | import java.util.regex.Pattern | ||||||
|  | 
 | ||||||
|  | @PackageScope | ||||||
|  | @TupleConstructor(includeFields = true, defaults = false) | ||||||
|  | class PatternFunction implements Function<String, String> { | ||||||
|  | 
 | ||||||
|  |     protected final Pattern pattern | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     String apply(String s) { | ||||||
|  |         def matcher = this.pattern.matcher(s) | ||||||
|  |         matcher.find() ? matcher.group() : null | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     String toString() { | ||||||
|  |         "PatternFunction(pattern: ${ this.pattern })" | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
| @ -0,0 +1,10 @@ | |||||||
|  | package com.jessebrault.ssg.template.gspe | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | import java.lang.annotation.Retention | ||||||
|  | import java.lang.annotation.RetentionPolicy | ||||||
|  | 
 | ||||||
|  | @Retention(RetentionPolicy.SOURCE) | ||||||
|  | @interface PeekBefore { | ||||||
|  |     ComponentToken.Type[] value() | ||||||
|  | } | ||||||
| @ -0,0 +1,164 @@ | |||||||
|  | package com.jessebrault.ssg.template.gspe | ||||||
|  | 
 | ||||||
|  | import com.jessebrault.fsm.stackfunction.StackFunctionFsmBuilder | ||||||
|  | import com.jessebrault.fsm.stackfunction.StackFunctionFsmBuilderImpl | ||||||
|  | import groovy.transform.PackageScope | ||||||
|  | 
 | ||||||
|  | import static FsmState.* | ||||||
|  | 
 | ||||||
|  | @PackageScope | ||||||
|  | class TemplateToScriptConverter { | ||||||
|  | 
 | ||||||
|  |     private static final PatternFunction html = new PatternFunction(~/^(?:[\w\W&&[^<$]]|<(?!%|\p{Lu})|\$(?!\{))+/) | ||||||
|  |     private static final PatternFunction scriptletOpen = new PatternFunction(~/^<%(?!=)/) | ||||||
|  |     private static final PatternFunction expressionScriptletOpen = new PatternFunction(~/^<%=/) | ||||||
|  |     private static final PatternFunction scriptletText = new PatternFunction(~/^.+(?=%>)/) | ||||||
|  |     private static final PatternFunction scriptletClose = new PatternFunction(~/^%>/) | ||||||
|  | 
 | ||||||
|  |     private static final PatternFunction componentOpen = new PatternFunction(~/^<(?=\p{Lu})/) | ||||||
|  |     private static final PatternFunction componentIdentifier = new PatternFunction(~/^\p{Lu}.*?(?=\s|\\/)/) | ||||||
|  |     private static final PatternFunction attrKeyWithValue = new PatternFunction(~/^\s*[\p{Ll}\p{Lu}0-9_\-]+=/) | ||||||
|  |     private static final PatternFunction attrKeyBoolean = new PatternFunction(~/^\s*[\p{Ll}\p{Lu}0-9_\-]++(?!=)/) | ||||||
|  |     private static final PatternFunction componentSelfClose = new PatternFunction(~/^\s*\/>/) | ||||||
|  | 
 | ||||||
|  |     private static final PatternFunction attrValueStringOpen = new PatternFunction(~/^["']/) | ||||||
|  |     private static final PatternFunction attrValueStringContents = new PatternFunction(~/^(?:[\w\W&&[^\\"]]|\\\\|\\")*(?=")/) | ||||||
|  |     private static final PatternFunction attrValueStringClose = new PatternFunction(~/["']/) | ||||||
|  | 
 | ||||||
|  |     private static StackFunctionFsmBuilder<String, FsmState, String> getFsmBuilder() { | ||||||
|  |         new StackFunctionFsmBuilderImpl<>() | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @SuppressWarnings('GrMethodMayBeStatic') | ||||||
|  |     String convert(String src) { | ||||||
|  |         def b = new StringBuilder() | ||||||
|  |         def stringAcc = new StringBuilder() | ||||||
|  | 
 | ||||||
|  |         b << 'def getTemplate() {\nreturn { out ->\n' | ||||||
|  | 
 | ||||||
|  |         def fsm = getFsmBuilder().with { | ||||||
|  |             initialState = HTML | ||||||
|  | 
 | ||||||
|  |             whileIn(HTML) { | ||||||
|  |                 on html exec { | ||||||
|  |                     stringAcc << it | ||||||
|  |                 } | ||||||
|  |                 on scriptletOpen shiftTo SCRIPTLET exec { | ||||||
|  |                     if (stringAcc.length() > 0) { | ||||||
|  |                         b << 'out << """' << stringAcc.toString() << '""";\n' | ||||||
|  |                         stringAcc = new StringBuilder() | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |                 on expressionScriptletOpen shiftTo EXPRESSION_SCRIPTLET exec { | ||||||
|  |                     stringAcc << '${' | ||||||
|  |                 } | ||||||
|  |                 on componentOpen shiftTo COMPONENT_IDENTIFIER exec { | ||||||
|  |                     if (stringAcc.length() > 0) { | ||||||
|  |                         b << 'out << """' << stringAcc.toString() << '""";\n' | ||||||
|  |                         stringAcc = new StringBuilder() | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             whileIn(SCRIPTLET) { | ||||||
|  |                 on scriptletText exec { | ||||||
|  |                     b << it | ||||||
|  |                 } | ||||||
|  |                 on scriptletClose shiftTo HTML exec { | ||||||
|  |                     b << ';\n' | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             whileIn(EXPRESSION_SCRIPTLET) { | ||||||
|  |                 on scriptletText exec { | ||||||
|  |                     stringAcc << it | ||||||
|  |                 } | ||||||
|  |                 on scriptletClose shiftTo HTML exec { | ||||||
|  |                     stringAcc << '}' | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             whileIn(COMPONENT) { | ||||||
|  |                 // tokenize component, figure out body, and tokenize closing component | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             whileIn(COMPONENT_IDENTIFIER) { | ||||||
|  |                 on componentIdentifier shiftTo COMPONENT_ATTR_KEY exec { | ||||||
|  |                     b << "out << renderComponent('${ it }') { attr, bodyOut ->\n" | ||||||
|  |                 } | ||||||
|  |                 onNoMatch() exec { | ||||||
|  |                     throw new RuntimeException('expected a Component Identifier') | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             whileIn(COMPONENT_ATTR_KEY) { | ||||||
|  |                 on attrKeyWithValue shiftTo COMPONENT_ATTR_VALUE_OPEN exec { String s -> | ||||||
|  |                     def trimmed = s.trim() | ||||||
|  |                     def key = trimmed.substring(0, trimmed.length() - 1) | ||||||
|  |                     b << "attr['${ key }'] = " | ||||||
|  |                 } | ||||||
|  |                 on attrKeyBoolean exec { String s -> | ||||||
|  |                     def trimmed = s.trim() | ||||||
|  |                     def key = trimmed.substring(0, trimmed.length() - 1) | ||||||
|  |                     b << "attr['${ key }'] = true" | ||||||
|  |                 } | ||||||
|  |                 on componentSelfClose shiftTo HTML exec { | ||||||
|  |                     b << '};\n' | ||||||
|  |                 } | ||||||
|  |                 onNoMatch() exec { | ||||||
|  |                     throw new RuntimeException('expected either an attr key or a closing />') | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             whileIn(COMPONENT_ATTR_VALUE_OPEN) { | ||||||
|  |                 on attrValueStringOpen shiftTo COMPONENT_ATTR_VALUE_STRING exec { | ||||||
|  |                     b << '"' | ||||||
|  |                 } | ||||||
|  |                 onNoMatch() exec { | ||||||
|  |                     throw new RuntimeException('expected a string opening') | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             whileIn(COMPONENT_ATTR_VALUE_STRING) { | ||||||
|  |                 on attrValueStringContents shiftTo COMPONENT_ATTR_VALUE_STRING_CLOSE exec { | ||||||
|  |                     b << it | ||||||
|  |                 } | ||||||
|  |                 onNoMatch() exec { | ||||||
|  |                     throw new RuntimeException('expected string contents') | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             whileIn(COMPONENT_ATTR_VALUE_STRING_CLOSE) { | ||||||
|  |                 on attrValueStringClose shiftTo COMPONENT_ATTR_KEY exec { | ||||||
|  |                     b << '";\n' | ||||||
|  |                 } | ||||||
|  |                 onNoMatch() exec { | ||||||
|  |                     throw new RuntimeException('expected string close') | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             build() | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         def remaining = src | ||||||
|  |         while (remaining.length() > 0) { | ||||||
|  |             def output = fsm.apply(remaining) | ||||||
|  |             if (output != null) { | ||||||
|  |                 remaining = remaining.substring(output.length()) | ||||||
|  |             } else if (output != null && output.length() == 0) { | ||||||
|  |                 throw new RuntimeException('output length is zero') | ||||||
|  |             } else { | ||||||
|  |                 throw new RuntimeException('output is null') | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if (fsm.currentState == HTML && stringAcc.length() > 0) { | ||||||
|  |             b << 'out << """' << stringAcc.toString() << '""";\n' | ||||||
|  |             stringAcc = new StringBuilder() | ||||||
|  |         } | ||||||
|  |         b << '}}\n' | ||||||
|  | 
 | ||||||
|  |         b.toString() | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
| @ -0,0 +1,107 @@ | |||||||
|  | package com.jessebrault.ssg.template.gspe | ||||||
|  | 
 | ||||||
|  | import org.junit.jupiter.api.Test | ||||||
|  | 
 | ||||||
|  | import static com.jessebrault.ssg.template.gspe.ComponentToken.Type.* | ||||||
|  | 
 | ||||||
|  | import static org.junit.jupiter.api.Assertions.assertEquals | ||||||
|  | 
 | ||||||
|  | class ComponentTokenizerTests { | ||||||
|  | 
 | ||||||
|  |     private final ComponentTokenizer tokenizer = new ComponentTokenizer() | ||||||
|  | 
 | ||||||
|  |     @Test | ||||||
|  |     void selfClosingComponent() { | ||||||
|  |         def r = this.tokenizer.tokenize('<Test />') | ||||||
|  |         assertEquals(4, r.size()) | ||||||
|  |         assertEquals(LT, r[0].type) | ||||||
|  |         assertEquals(IDENTIFIER, r[1].type) | ||||||
|  |         assertEquals('Test', r[1].text) | ||||||
|  |         assertEquals(FORWARD_SLASH, r[2].type) | ||||||
|  |         assertEquals(GT, r[3].type) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Test | ||||||
|  |     void selfClosingComponentWithDoubleQuotedString() { | ||||||
|  |         def r = this.tokenizer.tokenize('<Test key="value" />') | ||||||
|  |         assertEquals(9, r.size()) | ||||||
|  | 
 | ||||||
|  |         assertEquals(LT, r[0].type) | ||||||
|  | 
 | ||||||
|  |         assertEquals(IDENTIFIER, r[1].type) | ||||||
|  |         assertEquals('Test', r[1].text) | ||||||
|  | 
 | ||||||
|  |         assertEquals(KEY, r[2].type) | ||||||
|  |         assertEquals('key', r[2].text) | ||||||
|  | 
 | ||||||
|  |         assertEquals(EQUALS, r[3].type) | ||||||
|  | 
 | ||||||
|  |         assertEquals(DOUBLE_QUOTE, r[4].type) | ||||||
|  | 
 | ||||||
|  |         assertEquals(STRING, r[5].type) | ||||||
|  |         assertEquals('value', r[5].text) | ||||||
|  | 
 | ||||||
|  |         assertEquals(DOUBLE_QUOTE, r[6].type) | ||||||
|  | 
 | ||||||
|  |         assertEquals(FORWARD_SLASH, r[7].type) | ||||||
|  | 
 | ||||||
|  |         assertEquals(GT, r[8].type) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Test | ||||||
|  |     void selfClosingComponentWithSingleQuotedString() { | ||||||
|  |         def r = this.tokenizer.tokenize("<Test key='value' />") | ||||||
|  |         assertEquals(9, r.size()) | ||||||
|  | 
 | ||||||
|  |         assertEquals(LT, r[0].type) | ||||||
|  | 
 | ||||||
|  |         assertEquals(IDENTIFIER, r[1].type) | ||||||
|  |         assertEquals('Test', r[1].text) | ||||||
|  | 
 | ||||||
|  |         assertEquals(KEY, r[2].type) | ||||||
|  |         assertEquals('key', r[2].text) | ||||||
|  | 
 | ||||||
|  |         assertEquals(EQUALS, r[3].type) | ||||||
|  | 
 | ||||||
|  |         assertEquals(SINGLE_QUOTE, r[4].type) | ||||||
|  | 
 | ||||||
|  |         assertEquals(STRING, r[5].type) | ||||||
|  |         assertEquals('value', r[5].text) | ||||||
|  | 
 | ||||||
|  |         assertEquals(SINGLE_QUOTE, r[6].type) | ||||||
|  | 
 | ||||||
|  |         assertEquals(FORWARD_SLASH, r[7].type) | ||||||
|  | 
 | ||||||
|  |         assertEquals(GT, r[8].type) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Test | ||||||
|  |     void componentWithSimpleDollarGroovy() { | ||||||
|  |         def r = this.tokenizer.tokenize('<Test key=${ test } />') | ||||||
|  |         assertEquals(10, r.size()) | ||||||
|  | 
 | ||||||
|  |         assertEquals(LT, r[0].type) | ||||||
|  | 
 | ||||||
|  |         assertEquals(IDENTIFIER, r[1].type) | ||||||
|  |         assertEquals('Test', r[1].text) | ||||||
|  | 
 | ||||||
|  |         assertEquals(KEY, r[2].type) | ||||||
|  |         assertEquals('key', r[2].text) | ||||||
|  | 
 | ||||||
|  |         assertEquals(EQUALS, r[3].type) | ||||||
|  | 
 | ||||||
|  |         assertEquals(DOLLAR, r[4].type) | ||||||
|  | 
 | ||||||
|  |         assertEquals(CURLY_OPEN, r[5].type) | ||||||
|  | 
 | ||||||
|  |         assertEquals(GROOVY, r[6].type) | ||||||
|  |         assertEquals(' test ', r[6].text) | ||||||
|  | 
 | ||||||
|  |         assertEquals(CURLY_CLOSE, r[7].type) | ||||||
|  | 
 | ||||||
|  |         assertEquals(FORWARD_SLASH, r[8].type) | ||||||
|  | 
 | ||||||
|  |         assertEquals(GT, r[9].type) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
| @ -0,0 +1,159 @@ | |||||||
|  | package com.jessebrault.ssg.template.gspe | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | import groovy.text.TemplateEngine | ||||||
|  | import org.junit.jupiter.api.Test | ||||||
|  | 
 | ||||||
|  | import static org.junit.jupiter.api.Assertions.assertEquals | ||||||
|  | 
 | ||||||
|  | class GspeTemplateEngineTests { | ||||||
|  | 
 | ||||||
|  |     private final TemplateEngine engine = new GspeTemplateEngine(new GspeTemplateEngine.Configuration( | ||||||
|  |             { new GspeTemplate() }, | ||||||
|  |             [], | ||||||
|  |             [] | ||||||
|  |     )) | ||||||
|  | 
 | ||||||
|  |     @Test | ||||||
|  |     void doctype() { | ||||||
|  |         def src = '<!DOCTYPE html>' | ||||||
|  |         def r = this.engine.createTemplate(src).make().toString() | ||||||
|  |         assertEquals('<!DOCTYPE html>', r) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Test | ||||||
|  |     void handlesNewlines() { | ||||||
|  |         def src = '<!DOCTYPE html>\n<html>' | ||||||
|  |         def r = this.engine.createTemplate(src).make().toString() | ||||||
|  |         assertEquals(src, r) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Test | ||||||
|  |     void emptyScriptlet() { | ||||||
|  |         def src = '<%%>' | ||||||
|  |         def r = this.engine.createTemplate(src).make().toString() | ||||||
|  |         assertEquals('', r) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Test | ||||||
|  |     void simpleOut() { | ||||||
|  |         def src = '<% out << "Hello, World!" %>' | ||||||
|  |         def r = this.engine.createTemplate(src).make().toString() | ||||||
|  |         assertEquals('Hello, World!', r) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Test | ||||||
|  |     void scriptletInString() { | ||||||
|  |         def src = '<html lang="<% out << "en" %>">' | ||||||
|  |         def r = this.engine.createTemplate(src).make().toString() | ||||||
|  |         assertEquals('<html lang="en">', r) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Test | ||||||
|  |     void expressionScriptlet() { | ||||||
|  |         def src = '<%= 13 %>' | ||||||
|  |         def r = this.engine.createTemplate(src).make().toString() | ||||||
|  |         assertEquals('13', r) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Test | ||||||
|  |     void bindingWorks() { | ||||||
|  |         def src = '<%= greeting %>' | ||||||
|  |         def r = this.engine.createTemplate(src).make([greeting: 'Hello, World!']).toString() | ||||||
|  |         assertEquals('Hello, World!', r) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     static class CustomBaseTemplate extends GspeTemplate { | ||||||
|  | 
 | ||||||
|  |         def greeting = 'Greetings!' | ||||||
|  |         def name = 'Jesse' | ||||||
|  | 
 | ||||||
|  |         @SuppressWarnings('GrMethodMayBeStatic') | ||||||
|  |         def greet() { | ||||||
|  |             'Hello, World!' | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Test | ||||||
|  |     void baseTemplateMethodsPresent() { | ||||||
|  |         def src = '<%= greet() %>' | ||||||
|  |         def configuration = new GspeTemplateEngine.Configuration({ new CustomBaseTemplate() }, [], []) | ||||||
|  |         def engine = new GspeTemplateEngine(configuration) | ||||||
|  |         def r = engine.createTemplate(src).make().toString() | ||||||
|  |         assertEquals('Hello, World!', r) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Test | ||||||
|  |     void baseTemplatePropertiesPresent() { | ||||||
|  |         def src = '<%= this.greeting %>' | ||||||
|  |         def configuration = new GspeTemplateEngine.Configuration({ new CustomBaseTemplate() }, [], []) | ||||||
|  |         def engine = new GspeTemplateEngine(configuration) | ||||||
|  |         def r = engine.createTemplate(src).make().toString() | ||||||
|  |         assertEquals('Greetings!', r) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Test | ||||||
|  |     void bindingOverridesCustomBaseTemplate() { | ||||||
|  |         def src = '<%= greet() %>' | ||||||
|  |         def configuration = new GspeTemplateEngine.Configuration({ new CustomBaseTemplate() }, [], []) | ||||||
|  |         def engine = new GspeTemplateEngine(configuration) | ||||||
|  |         def r = engine.createTemplate(src).make([greet: { "Hello, Person!" }]).toString() | ||||||
|  |         assertEquals('Hello, Person!', r) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     static class Greeter implements Component { | ||||||
|  | 
 | ||||||
|  |         @Override | ||||||
|  |         String render(Map<String, ?> attr, String body) { | ||||||
|  |             "${ attr.greeting }, ${ attr.person }!" | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Test | ||||||
|  |     void selfClosingComponent() { | ||||||
|  |         def src = '<Greeter greeting="Hello" person="World" />' | ||||||
|  |         def configuration = new GspeTemplateEngine.Configuration({ new GspeTemplate() }, [], [new Greeter()]) | ||||||
|  |         def engine = new GspeTemplateEngine(configuration) | ||||||
|  |         def r = engine.createTemplate(src).make().toString() | ||||||
|  |         assertEquals('Hello, World!', r) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Test | ||||||
|  |     void componentWithGStringAttrValue() { | ||||||
|  |         def src = '<Greeter greeting="Hello" person="person number ${ 13 }" />' | ||||||
|  |         def configuration = new GspeTemplateEngine.Configuration({ new GspeTemplate() }, [], [new Greeter()]) | ||||||
|  |         def engine = new GspeTemplateEngine(configuration) | ||||||
|  |         def r = engine.createTemplate(src).make().toString() | ||||||
|  |         assertEquals('Hello, person number 13!', r) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Test | ||||||
|  |     void componentWithGStringAttrValueCanAccessBinding() { | ||||||
|  |         def src = '<Greeter greeting="Hello" person="person named ${ name }" />' | ||||||
|  |         def configuration = new GspeTemplateEngine.Configuration({ new GspeTemplate() }, [], [new Greeter()]) | ||||||
|  |         def engine = new GspeTemplateEngine(configuration) | ||||||
|  |         def r = engine.createTemplate(src).make([name: 'Jesse']).toString() | ||||||
|  |         assertEquals('Hello, person named Jesse!', r) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Test | ||||||
|  |     void componentWithGStringAttrValueCanAccessBaseTemplateMethod() { | ||||||
|  |         def src = '<Greeter greeting="Hello" person="person named ${ getName() }" />' | ||||||
|  |         def configuration = new GspeTemplateEngine.Configuration({ new CustomBaseTemplate() }, [], [new Greeter()]) | ||||||
|  |         def engine = new GspeTemplateEngine(configuration) | ||||||
|  |         def r = engine.createTemplate(src).make().toString() | ||||||
|  |         assertEquals('Hello, person named Jesse!', r) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Test | ||||||
|  |     void componentWithGStringAttrValueCanAccessBaseTemplateProperty() { | ||||||
|  |         def src = '<Greeter greeting="Hello" person="person named ${ this.name }" />' | ||||||
|  |         def configuration = new GspeTemplateEngine.Configuration({ new CustomBaseTemplate() }, [], [new Greeter()]) | ||||||
|  |         def engine = new GspeTemplateEngine(configuration) | ||||||
|  |         def r = engine.createTemplate(src).make().toString() | ||||||
|  |         assertEquals('Hello, person named Jesse!', r) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
							
								
								
									
										12
									
								
								template/src/test/resources/components/Greeting.groovy
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								template/src/test/resources/components/Greeting.groovy
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,12 @@ | |||||||
|  | package components | ||||||
|  | 
 | ||||||
|  | import com.jessebrault.ssg.template.gspe.Component | ||||||
|  | 
 | ||||||
|  | class Greeting implements Component { | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     String render(Map<String, ?> attr, String body) { | ||||||
|  |         "<h1>${ attr.person }, ${ attr.person }!</h1>" | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
							
								
								
									
										16
									
								
								template/src/test/resources/components/Head.groovy
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								template/src/test/resources/components/Head.groovy
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,16 @@ | |||||||
|  | package components | ||||||
|  | 
 | ||||||
|  | import com.jessebrault.ssg.template.gspe.Component | ||||||
|  | 
 | ||||||
|  | class Head implements Component { | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     String render(Map<String, ?> attr, String body) { | ||||||
|  |         def b = new StringBuilder() | ||||||
|  |         b << '<head>\n' | ||||||
|  |         b << "    <title>${ attr.title }</title>\n" | ||||||
|  |         b << '</head>\n' | ||||||
|  |         b.toString() | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
							
								
								
									
										17
									
								
								template/src/test/resources/log4j2.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								template/src/test/resources/log4j2.xml
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,17 @@ | |||||||
|  | <?xml version="1.0" encoding="UTF-8" ?> | ||||||
|  | <Configuration name="ssg" status="WARN"> | ||||||
|  |     <Appenders> | ||||||
|  |         <Console name="standard" target="SYSTEM_OUT"> | ||||||
|  |             <PatternLayout> | ||||||
|  |                 <MarkerPatternSelector defaultPattern="%highlight{%-5level} %logger{1} %M %L: %msg%n%ex"> | ||||||
|  |                     <PatternMatch key="FLOW" pattern="%highlight{%-5level} %logger{1} %M %L: %markerSimpleName %msg%n%ex" /> | ||||||
|  |                 </MarkerPatternSelector> | ||||||
|  |             </PatternLayout> | ||||||
|  |         </Console> | ||||||
|  |     </Appenders> | ||||||
|  |     <Loggers> | ||||||
|  |         <Root level="trace"> | ||||||
|  |             <AppenderRef ref="standard" /> | ||||||
|  |         </Root> | ||||||
|  |     </Loggers> | ||||||
|  | </Configuration> | ||||||
							
								
								
									
										7
									
								
								template/src/test/resources/test.ssg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								template/src/test/resources/test.ssg
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,7 @@ | |||||||
|  | <!DOCTYPE html> | ||||||
|  | <html> | ||||||
|  |     <Head title="Greeting Page" /> | ||||||
|  |     <body> | ||||||
|  |         <Greeting person="World" /> | ||||||
|  |     </body> | ||||||
|  | </html> | ||||||
		Loading…
	
		Reference in New Issue
	
	Block a user
	 JesseBrault0709
						JesseBrault0709