Skip to content

Instantly share code, notes, and snippets.

@nosix
Last active November 22, 2016 09:06
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nosix/43336b995bca174f8721de92eb5c1cbc to your computer and use it in GitHub Desktop.
Save nosix/43336b995bca174f8721de92eb5c1cbc to your computer and use it in GitHub Desktop.
Spring Boot (1.4.2.RELEASE) + Thymeleaf (2.1.5) configuration sample (no escape single quote)
package xxx.thymeleaf
import org.thymeleaf.processor.IProcessor
import org.thymeleaf.spring4.dialect.SpringStandardDialect
class AngularJSDialect : SpringStandardDialect() {
companion object {
private val attributes = arrayOf(
"ng-class", "ng-model", "ng-show",
"minlength",
"popover-title",
"uib-popover-template") // TODO: add attribute names
}
override fun getProcessors(): MutableSet<IProcessor> = super.getProcessors().apply {
addAll(attributes.map(::SingleRemovableAttributeModifierAttrProcessor))
add(NoOpRenameElementProcessor("script"))
}
}
package xxx.thymeleaf
import org.thymeleaf.Arguments
import org.thymeleaf.dom.Element
import org.thymeleaf.processor.ProcessorResult
import org.thymeleaf.processor.element.AbstractElementProcessor
/**
* NoOpRenameElementProcessor rename th:elementName to elementName.
*/
class NoOpRenameElementProcessor(val elementName: String) : AbstractElementProcessor(elementName) {
override fun getPrecedence(): Int = 100000
override fun processElement(args: Arguments, e: Element): ProcessorResult {
e.parent.run {
insertBefore(e, e.cloneElementNodeWithNewName(this, elementName, false))
removeChild(e)
}
return ProcessorResult.OK
}
}
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<title>Widget</title>
<!-- Bootstrap -->
<link href="css/bootstrap.min.css" rel="stylesheet"/>
<!-- HTML5 shim and Respond.js for IE8 support of HTML5 elements and media queries -->
<!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
<!--[if lt IE 9]>
<script src="https://oss.maxcdn.com/html5shiv/3.7.3/html5shiv.min.js"></script>
<script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
<![endif]-->
</head>
<body>
<div th:fragment="input_text(title,modelName,formName,required,min,max)">
<div class="form-group has-feedback" th:ng-class="'{ \'has-error\' : ' + |${formName}.${modelName}.$invalid }|">
<input type="text" class="form-control input-sm"
th:ng-model="|form.${modelName}|" th:name="${modelName}" th:placeholder="${title}"
th:required="${required}" th:minlength="${min}" th:maxlength="${max}"
th:popover-title="${title}" th:uib-popover-template="|'${modelName}Error.html'|" popover-trigger="'focus'" popover-placement="bottom-right" />
<span class="glyphicon glyphicon-remove form-control-feedback" aria-hidden="true"
th:ng-show="|${formName}.${modelName}.$invalid|"></span>
<th:script type="text/ng-template" th:id="|${modelName}Error.html|">
<ul class="list-unstyled">
<li th:if="${required}"
th:ng-show="|${formName}.${modelName}.$error.required|">Required</li>
<li th:if="${min}"
th:ng-show="|${formName}.${modelName}.$error.minlength|" th:text="|More than ${min} letters|"></li>
<li th:if="${max}"
th:ng-show="|${formName}.${modelName}.$error.maxlength|" th:text="|Less than ${max} letters|"></li>
</ul>
</th:script>
</div>
</div>
</body>
</html>
package xxx.thymeleaf
import org.thymeleaf.Arguments
import org.thymeleaf.dom.Document
import org.thymeleaf.templatewriter.AbstractGeneralTemplateWriter
import java.io.Writer
class SingleQuoteUnEscapeTemplateWriter : AbstractGeneralTemplateWriter() {
override fun useXhtmlTagMinimizationRules(): Boolean = true
override fun shouldWriteXmlDeclaration(): Boolean = false
override fun write(arguments: Arguments, writer: Writer, document: Document) {
super.write(arguments, UnEscapeWriter(writer), document)
}
private class UnEscapeWriter(private val writer: Writer) : Writer() {
companion object {
private val SINGLE_QUOTE = "#39;".toList()
}
override fun write(buf: CharArray, off: Int, len: Int) {
val srcBuf = buf.slice(off..(off+len-1))
var drop = false
val newBuf = srcBuf.mapIndexedNotNull { i, c ->
if (drop) {
if (c == SINGLE_QUOTE.last()) {
drop = false
}
null
} else {
if (c == '&' && srcBuf.slice((i+1)..(i+SINGLE_QUOTE.size)) == SINGLE_QUOTE) {
drop = true
'\''
} else {
c
}
}
}
writer.write(newBuf.toCharArray(), 0, newBuf.size)
}
override fun flush() {
writer.flush()
}
override fun close() {
writer.close()
}
}
}
package xxx.thymeleaf
import org.thymeleaf.Arguments
import org.thymeleaf.dom.Attribute
import org.thymeleaf.dom.Element
import org.thymeleaf.standard.processor.attr.AbstractStandardSingleAttributeModifierAttrProcessor
class SingleRemovableAttributeModifierAttrProcessor(attrName: String) :
AbstractStandardSingleAttributeModifierAttrProcessor(attrName) {
companion object {
private val ATTR_PRECEDENCE = 1000
}
override fun getPrecedence(): Int = ATTR_PRECEDENCE
override fun getTargetAttributeName(
args: Arguments, e: Element, attrName: String): String = Attribute.getUnprefixedAttributeName(attrName)
override fun getModificationType(
args: Arguments, e: Element, attrName: String, newAttrName: String): ModificationType = ModificationType.SUBSTITUTION
override fun removeAttributeIfEmpty(
args: Arguments, e: Element, attrName: String, newAttrName: String): Boolean = true
}
package xxx
import xxx.thymeleaf.AngularJSDialect
import xxx.thymeleaf.SingleQuoteUnescapeTemplateWriter
import org.h2.server.web.WebServlet
import org.springframework.boot.SpringApplication
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.web.servlet.ServletRegistrationBean
import org.springframework.context.annotation.Bean
import org.springframework.context.support.ReloadableResourceBundleMessageSource
import org.thymeleaf.spring4.SpringTemplateEngine
import org.thymeleaf.spring4.templateresolver.SpringResourceTemplateResolver
import org.thymeleaf.spring4.view.ThymeleafViewResolver
import org.thymeleaf.templatemode.TemplateModeHandler
import org.thymeleaf.templateparser.html.LegacyHtml5TemplateParser
@SpringBootApplication
open class WebApplication {
private val TEMPLATE_MODE = "LOOSE_HTML5"
private val POOL_SIZE = Runtime.getRuntime().availableProcessors().let {
Math.min(if (it <= 2) it else it - 1, 24) // see StandardTemplateModeHandlers
}
@Bean
open fun viewResolver() = ThymeleafViewResolver().apply {
templateEngine = templateEngine()
}
@Bean
open fun templateEngine() = SpringTemplateEngine().apply {
templateResolvers = setOf(templateResolver())
templateModeHandlers = setOf(TemplateModeHandler(
TEMPLATE_MODE,
LegacyHtml5TemplateParser(TEMPLATE_MODE, POOL_SIZE),
SingleQuoteUnescapeTemplateWriter()))
setDialect(AngularJSDialect())
}
@Bean
open fun templateResolver() = SpringResourceTemplateResolver().apply {
templateMode = TEMPLATE_MODE
prefix = "classpath:/templates/"
suffix = ".html"
isCacheable = false // TODO: make it true before release
}
}
fun main(args: Array<String>) {
SpringApplication.run(WebApplication::class.java, *args)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment