diff --git a/src/css/tailwind.config.js b/src/css/tailwind.config.js index f4aeede..e3fe237 100644 --- a/src/css/tailwind.config.js +++ b/src/css/tailwind.config.js @@ -1,7 +1,8 @@ module.exports = { purge: { content: [ - '../main/resources/views/**/*.twig' + '../main/resources/views/**/*.twig', + '../main/kotlin/starter/PebbleModule.kt', ] }, theme: {}, diff --git a/src/main/kotlin/starter/KotlinStarter.kt b/src/main/kotlin/starter/KotlinStarter.kt index 4885bd2..b0eab04 100644 --- a/src/main/kotlin/starter/KotlinStarter.kt +++ b/src/main/kotlin/starter/KotlinStarter.kt @@ -1,5 +1,7 @@ package starter +import org.http4k.core.then +import org.http4k.filter.ServerFilters import org.http4k.routing.ResourceLoader import org.http4k.routing.RoutingHttpHandler import org.http4k.routing.routes @@ -25,9 +27,8 @@ val mainModule = module { } onClose { it?.stop() } single { Config().load() } - single { PebbleModule().engine() } single { LoggerFactory.getLogger("Starter") } - single { Views(get()) } + single { Views(get(), get()) } single { ProjectZip(getAll()) } } @@ -39,18 +40,19 @@ val templateModule = module { } val routesModule = module { - single { IndexRouteSupplier(get(), get()) } bind RouteSupplier::class + single { IndexRouteSupplier(get()) } bind RouteSupplier::class single { ZipRouteSupplier(get(), get()) } bind RouteSupplier::class single { - routes( - static(ResourceLoader.Classpath("/assets")), - getAll().toRouter() - ) + ServerFilters.CatchAll().then( + routes( + static(ResourceLoader.Classpath("/assets")), + getAll().toRouter() + )) } } fun main() { startKoin { - modules(mainModule, templateModule, routesModule) + modules(mainModule, pebbleModule, templateModule, routesModule) } } diff --git a/src/main/kotlin/starter/PebbleModule.kt b/src/main/kotlin/starter/PebbleModule.kt index 5c3bc98..b7851f2 100644 --- a/src/main/kotlin/starter/PebbleModule.kt +++ b/src/main/kotlin/starter/PebbleModule.kt @@ -1,15 +1,57 @@ package starter -import com.mitchellbosecke.pebble.PebbleEngine -import com.mitchellbosecke.pebble.loader.ClasspathLoader +import com.mitchellbosecke.pebble.extension.escaper.SafeString +import com.mitchellbosecke.pebble.template.EvaluationContext +import com.mitchellbosecke.pebble.template.PebbleTemplate +import org.intellij.lang.annotations.Language +import org.koin.dsl.bind +import org.koin.dsl.module +import starter.utils.PebbleEngineBuilder +import starter.utils.PebbleEngineBuilder.CacheType.ConcurrentMap +import starter.utils.PebbleFunction -class PebbleModule { - fun engine(): PebbleEngine { - val loader = ClasspathLoader() - loader.suffix = ".twig" - return PebbleEngine.Builder() - .loader(loader) - .cacheActive(true) - .build() +class DepAsXmlPebbleFunction : PebbleFunction { + override val name = "depAsXml" + override fun getArgumentNames() = listOf("dependency") + override fun execute(args: Map, self: PebbleTemplate, context: EvaluationContext, lineNumber: Int): Any { + val dep = args["dependency"] as Dependency + + fun startTag(name: String): String { + @Language("html") @Suppress("UnnecessaryVariable") + val result = """<$name>""" + return result + } + + fun endTag(name: String): String { + @Language("html") @Suppress("UnnecessaryVariable") + val result = """</$name>""" + return result + } + + fun tag(name: String, content: String): String { + return """${startTag(name)}$content${endTag(name)}""" + } + + val result = """ + |${startTag("dependency")} + | ${tag("groupId", dep.groupId)} + | ${tag("artifactId", dep.artifactId)} + | ${tag("version", dep.version)} + |${endTag("dependency")} + """.trimMargin() + + return SafeString(result) + } +} + +val pebbleModule = module { + single { DepAsXmlPebbleFunction() } bind PebbleFunction::class + + single { + PebbleEngineBuilder { + cache = ConcurrentMap + functions(getAll()) + classPath { suffix = ".twig" } + } } } diff --git a/src/main/kotlin/starter/Views.kt b/src/main/kotlin/starter/Views.kt index ff45b1b..5e20bdc 100644 --- a/src/main/kotlin/starter/Views.kt +++ b/src/main/kotlin/starter/Views.kt @@ -3,12 +3,11 @@ package starter import com.mitchellbosecke.pebble.PebbleEngine import starter.utils.render -class Views(private val engine: PebbleEngine) { - fun index(dependencies: List, inputs: List): String { - val dependenciesByCategory = dependencies.groupBy { it.category }.toSortedMap() - return engine.render( - "views/index", - mapOf("dependencies" to dependenciesByCategory, "inputs" to inputs) - ) - } +class Views(private val engine: PebbleEngine, config: StarterConfig) { + private val args = mapOf( + "dependencies" to config.dependencies.groupBy { it.category }.toSortedMap(), + "inputs" to config.inputs + ) + + fun index() = engine.render("views/index", args) } diff --git a/src/main/kotlin/starter/routes/IndexRouteSupplier.kt b/src/main/kotlin/starter/routes/IndexRouteSupplier.kt index 2f3a504..a498584 100644 --- a/src/main/kotlin/starter/routes/IndexRouteSupplier.kt +++ b/src/main/kotlin/starter/routes/IndexRouteSupplier.kt @@ -7,13 +7,10 @@ import org.http4k.routing.bind import starter.StarterConfig import starter.Views -class IndexRouteSupplier( - private val views: Views, - private val conf: StarterConfig, -) : RouteSupplier { +class IndexRouteSupplier(private val views: Views) : RouteSupplier { override fun get() = "/" bind Method.GET to { Response(Status.OK) - .body(views.index(conf.dependencies, conf.inputs)) + .body(views.index()) .header("Content-Type", "text/html") } } diff --git a/src/main/kotlin/starter/utils/PebbleUtils.kt b/src/main/kotlin/starter/utils/PebbleUtils.kt index e24ae0a..1b68e50 100644 --- a/src/main/kotlin/starter/utils/PebbleUtils.kt +++ b/src/main/kotlin/starter/utils/PebbleUtils.kt @@ -1,11 +1,87 @@ package starter.utils import com.mitchellbosecke.pebble.PebbleEngine +import com.mitchellbosecke.pebble.cache.tag.CaffeineTagCache +import com.mitchellbosecke.pebble.cache.tag.ConcurrentMapTagCache +import com.mitchellbosecke.pebble.cache.tag.NoOpTagCache +import com.mitchellbosecke.pebble.cache.template.CaffeineTemplateCache +import com.mitchellbosecke.pebble.cache.template.ConcurrentMapTemplateCache +import com.mitchellbosecke.pebble.cache.template.NoOpTemplateCache +import com.mitchellbosecke.pebble.extension.AbstractExtension +import com.mitchellbosecke.pebble.extension.Filter +import com.mitchellbosecke.pebble.extension.Function +import com.mitchellbosecke.pebble.loader.ClasspathLoader import java.io.StringWriter -fun PebbleEngine.render(name: String, args: Map = mapOf()): String { - val template = getTemplate(name) - val writer = StringWriter() - template.evaluate(writer, args) - return writer.toString() +fun PebbleEngine.render(name: String, args: Map = mapOf()) = + getTemplate(name).let { StringWriter().apply { it.evaluate(this, args) }.toString() } + +fun PebbleEngine.render(name: String, vararg args: Pair) = + render(name, mapOf(*args)) + +interface PebbleFunction : Function { + val name: String +} + +interface PebbleFilter : Filter { + val name: String +} + +class PebbleEngineBuilder { + enum class CacheType { + None, Caffeine, ConcurrentMap + } + + private var loader = ClasspathLoader() + var cache = CacheType.ConcurrentMap + + private val functions = mutableListOf() + private val filters = mutableListOf() + + fun function(function: PebbleFunction) { + functions.add(function) + } + + fun functions(functions: Iterable) { + this.functions.addAll(functions) + } + + fun filter(filter: PebbleFilter) { + filters.add(filter) + } + + fun filters(filters: Iterable) { + this.filters.addAll(filters) + } + + fun classPath(block: ClasspathLoader.() -> Unit) { + loader = ClasspathLoader().apply(block) + } + + companion object { + operator fun invoke(block: PebbleEngineBuilder.() -> Unit): PebbleEngine = + PebbleEngineBuilder().apply(block).let { builder -> + PebbleEngine.Builder() + .loader(builder.loader) + .cacheActive(builder.cache != CacheType.None) + .templateCache(when (builder.cache) { + CacheType.None -> NoOpTemplateCache() + CacheType.Caffeine -> CaffeineTemplateCache() + CacheType.ConcurrentMap -> ConcurrentMapTemplateCache() + }) + .tagCache(when (builder.cache) { + CacheType.None -> NoOpTagCache() + CacheType.Caffeine -> CaffeineTagCache() + CacheType.ConcurrentMap -> ConcurrentMapTagCache() + }) + .extension(object : AbstractExtension() { + override fun getFunctions(): Map? = + builder.functions.associateBy { it.name }.ifEmpty { null } + + override fun getFilters(): Map? = + builder.filters.associateBy { it.name }.ifEmpty { null } + }) + .build() + } + } } diff --git a/src/main/resources/assets/styles.css b/src/main/resources/assets/styles.css index 1717fd4..44cb45d 100644 --- a/src/main/resources/assets/styles.css +++ b/src/main/resources/assets/styles.css @@ -1 +1 @@ -html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0}main{display:block}h1{font-size:2em;margin:.67em 0}hr{box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace,monospace;font-size:1em}a{background-color:transparent}abbr[title]{border-bottom:none;text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}img{border-style:none}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}[type=button],[type=submit],button{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}[type=button]:-moz-focusring,[type=submit]:-moz-focusring,button:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:.35em .75em .625em}legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{vertical-align:baseline}textarea{overflow:auto}[type=checkbox]{box-sizing:border-box;padding:0}details{display:block}summary{display:list-item}template{display:none}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}button{background-color:transparent;background-image:none}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}fieldset,ol,ul{margin:0;padding:0}ol,ul{list-style:none}html{font-family:system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;line-height:1.5}*,:after,:before{box-sizing:border-box;border:0 solid #e2e8f0}hr{border-top-width:1px}img{border-style:solid}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{color:#a0aec0}input:-ms-input-placeholder,textarea:-ms-input-placeholder{color:#a0aec0}input::placeholder,textarea::placeholder{color:#a0aec0}button{cursor:pointer}table{border-collapse:collapse}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}button,input,optgroup,select,textarea{padding:0;line-height:inherit;color:inherit}code,kbd,pre,samp{font-family:Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}.container{width:100%}@media (min-width:640px){.container{max-width:640px}}@media (min-width:768px){.container{max-width:768px}}@media (min-width:1024px){.container{max-width:1024px}}@media (min-width:1280px){.container{max-width:1280px}}.btn{font-weight:600;padding:.5rem 1rem;border-radius:.25rem}.btn:focus{outline:0;box-shadow:0 0 0 3px rgba(66,153,225,.5)}.btn:hover{font-weight:800}.btn-purple{--bg-opacity:1;background-color:#b794f4;background-color:rgba(183,148,244,var(--bg-opacity));--text-opacity:1;color:#2d3748;color:rgba(45,55,72,var(--text-opacity))}.btn-purple:focus,.btn-purple:hover{--bg-opacity:1;background-color:#805ad5;background-color:rgba(128,90,213,var(--bg-opacity))}.category{font-size:1.125rem;display:inline-block;text-align:center;font-weight:600;color:#2d3748;color:rgba(45,55,72,var(--text-opacity));background-color:#9ae6b4;background-color:rgba(154,230,180,var(--bg-opacity));border-radius:9999px;padding:.25rem .75rem;margin-top:.5rem;margin-bottom:.5rem}.category,.input{--text-opacity:1;--bg-opacity:1}.input{background-color:#edf2f7;background-color:rgba(237,242,247,var(--bg-opacity));-webkit-appearance:none;-moz-appearance:none;appearance:none;border-width:2px;--border-opacity:1;border-color:#edf2f7;border-color:rgba(237,242,247,var(--border-opacity));border-radius:.25rem;width:100%;padding:.5rem 1rem;color:#4a5568;color:rgba(74,85,104,var(--text-opacity));line-height:1.25}.input:focus{outline:0;--bg-opacity:1;background-color:#fff;background-color:rgba(255,255,255,var(--bg-opacity));--border-opacity:1;border-color:#9f7aea;border-color:rgba(159,122,234,var(--border-opacity))}.input-label{display:block;--text-opacity:1;color:#a0aec0;color:rgba(160,174,192,var(--text-opacity));font-weight:700;margin-bottom:.25rem;padding-right:1rem}@media (min-width:768px){.input-label{text-align:right;margin-bottom:0}}.block{display:block}.table{display:table}.font-bold{font-weight:700}.text-2xl{font-size:1.5rem}.m-2{margin:.5rem}.mx-auto{margin-left:auto;margin-right:auto}.mt-4{margin-top:1rem}.mb-4{margin-bottom:1rem}.mb-6{margin-bottom:1.5rem}.max-w-sm{max-width:24rem}.text-purple-800{--text-opacity:1;color:#553c9a;color:rgba(85,60,154,var(--text-opacity))}.w-full{width:100%}@-webkit-keyframes spin{to{transform:rotate(1turn)}}@keyframes spin{to{transform:rotate(1turn)}}@-webkit-keyframes ping{75%,to{transform:scale(2);opacity:0}}@keyframes ping{75%,to{transform:scale(2);opacity:0}}@-webkit-keyframes pulse{50%{opacity:.5}}@keyframes pulse{50%{opacity:.5}}@-webkit-keyframes bounce{0%,to{transform:translateY(-25%);-webkit-animation-timing-function:cubic-bezier(.8,0,1,1);animation-timing-function:cubic-bezier(.8,0,1,1)}50%{transform:none;-webkit-animation-timing-function:cubic-bezier(0,0,.2,1);animation-timing-function:cubic-bezier(0,0,.2,1)}}@keyframes bounce{0%,to{transform:translateY(-25%);-webkit-animation-timing-function:cubic-bezier(.8,0,1,1);animation-timing-function:cubic-bezier(.8,0,1,1)}50%{transform:none;-webkit-animation-timing-function:cubic-bezier(0,0,.2,1);animation-timing-function:cubic-bezier(0,0,.2,1)}}@media (min-width:768px){.md\:flex{display:flex}.md\:items-center{align-items:center}.md\:w-1\/3{width:33.333333%}.md\:w-2\/3{width:66.666667%}} \ No newline at end of file +html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0}main{display:block}h1{font-size:2em;margin:.67em 0}hr{box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace,monospace;font-size:1em}a{background-color:transparent}abbr[title]{border-bottom:none;text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}img{border-style:none}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}[type=button],[type=submit],button{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}[type=button]:-moz-focusring,[type=submit]:-moz-focusring,button:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:.35em .75em .625em}legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{vertical-align:baseline}textarea{overflow:auto}[type=checkbox]{box-sizing:border-box;padding:0}details{display:block}summary{display:list-item}template{display:none}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}button{background-color:transparent;background-image:none}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}fieldset,ol,ul{margin:0;padding:0}ol,ul{list-style:none}html{font-family:system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;line-height:1.5}*,:after,:before{box-sizing:border-box;border:0 solid #e2e8f0}hr{border-top-width:1px}img{border-style:solid}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{color:#a0aec0}input:-ms-input-placeholder,textarea:-ms-input-placeholder{color:#a0aec0}input::placeholder,textarea::placeholder{color:#a0aec0}button{cursor:pointer}table{border-collapse:collapse}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}button,input,optgroup,select,textarea{padding:0;line-height:inherit;color:inherit}code,kbd,pre,samp{font-family:Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}.container{width:100%}@media (min-width:640px){.container{max-width:640px}}@media (min-width:768px){.container{max-width:768px}}@media (min-width:1024px){.container{max-width:1024px}}@media (min-width:1280px){.container{max-width:1280px}}.btn{font-weight:600;padding:.5rem 1rem;border-radius:.25rem}.btn:focus{outline:0;box-shadow:0 0 0 3px rgba(66,153,225,.5)}.btn:hover{font-weight:800}.btn-purple{--bg-opacity:1;background-color:#b794f4;background-color:rgba(183,148,244,var(--bg-opacity));--text-opacity:1;color:#2d3748;color:rgba(45,55,72,var(--text-opacity))}.btn-purple:focus,.btn-purple:hover{--bg-opacity:1;background-color:#805ad5;background-color:rgba(128,90,213,var(--bg-opacity))}.category{font-size:1.125rem;display:inline-block;text-align:center;font-weight:600;color:#2d3748;color:rgba(45,55,72,var(--text-opacity));background-color:#9ae6b4;background-color:rgba(154,230,180,var(--bg-opacity));border-radius:9999px;padding:.25rem .75rem;margin-top:.5rem;margin-bottom:.5rem}.category,.input{--text-opacity:1;--bg-opacity:1}.input{background-color:#edf2f7;background-color:rgba(237,242,247,var(--bg-opacity));-webkit-appearance:none;-moz-appearance:none;appearance:none;border-width:2px;--border-opacity:1;border-color:#edf2f7;border-color:rgba(237,242,247,var(--border-opacity));border-radius:.25rem;width:100%;padding:.5rem 1rem;color:#4a5568;color:rgba(74,85,104,var(--text-opacity));line-height:1.25}.input:focus{outline:0;--bg-opacity:1;background-color:#fff;background-color:rgba(255,255,255,var(--bg-opacity));--border-opacity:1;border-color:#9f7aea;border-color:rgba(159,122,234,var(--border-opacity))}.input-label{display:block;--text-opacity:1;color:#a0aec0;color:rgba(160,174,192,var(--text-opacity));font-weight:700;margin-bottom:.25rem;padding-right:1rem}@media (min-width:768px){.input-label{text-align:right;margin-bottom:0}}.block{display:block}.table{display:table}.font-bold{font-weight:700}.text-2xl{font-size:1.5rem}.m-2{margin:.5rem}.mx-auto{margin-left:auto;margin-right:auto}.mt-4{margin-top:1rem}.mb-4{margin-bottom:1rem}.mb-6{margin-bottom:1.5rem}.max-w-sm{max-width:24rem}.text-gray-700{--text-opacity:1;color:#4a5568;color:rgba(74,85,104,var(--text-opacity))}.text-red-700{--text-opacity:1;color:#c53030;color:rgba(197,48,48,var(--text-opacity))}.text-purple-800{--text-opacity:1;color:#553c9a;color:rgba(85,60,154,var(--text-opacity))}.w-full{width:100%}@-webkit-keyframes spin{to{transform:rotate(1turn)}}@keyframes spin{to{transform:rotate(1turn)}}@-webkit-keyframes ping{75%,to{transform:scale(2);opacity:0}}@keyframes ping{75%,to{transform:scale(2);opacity:0}}@-webkit-keyframes pulse{50%{opacity:.5}}@keyframes pulse{50%{opacity:.5}}@-webkit-keyframes bounce{0%,to{transform:translateY(-25%);-webkit-animation-timing-function:cubic-bezier(.8,0,1,1);animation-timing-function:cubic-bezier(.8,0,1,1)}50%{transform:none;-webkit-animation-timing-function:cubic-bezier(0,0,.2,1);animation-timing-function:cubic-bezier(0,0,.2,1)}}@keyframes bounce{0%,to{transform:translateY(-25%);-webkit-animation-timing-function:cubic-bezier(.8,0,1,1);animation-timing-function:cubic-bezier(.8,0,1,1)}50%{transform:none;-webkit-animation-timing-function:cubic-bezier(0,0,.2,1);animation-timing-function:cubic-bezier(0,0,.2,1)}}@media (min-width:768px){.md\:flex{display:flex}.md\:items-center{align-items:center}.md\:w-1\/3{width:33.333333%}.md\:w-2\/3{width:66.666667%}} \ No newline at end of file diff --git a/src/main/resources/views/#macros.twig b/src/main/resources/views/#macros.twig index e50ac5f..16e528d 100644 --- a/src/main/resources/views/#macros.twig +++ b/src/main/resources/views/#macros.twig @@ -1,9 +1,14 @@ {% macro dependency(dependency) %} - +
+ + + +
{{ depAsXml(dependency) }}
+
{% endmacro %} {% macro input(input) %} @@ -17,4 +22,4 @@ -{% endmacro %} \ No newline at end of file +{% endmacro %}