Skip to content

Commit

Permalink
Attempt at standard Thymeleaf expression language for title tokens, #172
Browse files Browse the repository at this point in the history
  • Loading branch information
ultraq committed Jan 22, 2024
1 parent 1d5e0d9 commit fe543e1
Show file tree
Hide file tree
Showing 11 changed files with 272 additions and 119 deletions.
10 changes: 5 additions & 5 deletions build.gradle
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
/*
/*
* Copyright 2019, Emanuel Rabina (http://www.ultraq.net.nz/)
*
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*
* http://www.apache.org/licenses/LICENSE-2.0
*
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
Expand All @@ -15,7 +15,7 @@
*/

allprojects {
version = '3.3.0'
version = '3.4.0-SNAPSHOT'

ext {
groovyVersion = '4.0.14'
Expand Down
1 change: 1 addition & 0 deletions thymeleaf-layout-dialect/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ plugins {
id 'jacoco'
id 'distribution'
id 'maven-publish'
id 'idea'
}
apply from: 'https://raw.githubusercontent.com/ultraq/gradle-support/4.3.1/gradle-support.gradle'
apply from: 'https://raw.githubusercontent.com/ultraq/gradle-support/4.3.1/maven-publish-support.gradle'
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
/*
/*
* Copyright 2012, Emanuel Rabina (http://www.ultraq.net.nz/)
*
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*
* http://www.apache.org/licenses/LICENSE-2.0
*
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
Expand Down Expand Up @@ -34,7 +34,7 @@ import org.thymeleaf.templatemode.TemplateMode
/**
* A dialect for Thymeleaf that lets you build layouts and reusable templates in
* order to improve code reuse
*
*
* @author Emanuel Rabina
*/
class LayoutDialect extends AbstractProcessorDialect {
Expand All @@ -43,28 +43,34 @@ class LayoutDialect extends AbstractProcessorDialect {
static final String DIALECT_PREFIX = 'layout'
static final int DIALECT_PRECEDENCE = 10

private final boolean autoHeadMerging
private final SortingStrategy sortingStrategy
private final boolean autoHeadMerging
private final boolean newTitleTokens

/**
* Constructor, configure the layout dialect.
*
*
* @param sortingStrategy
* @param autoHeadMerging
* Experimental option, set to {@code false} to skip the automatic merging
* of an HTML {@code <head>} section.
* @param newTitleTokens
* Experimental option, set to {@code true} to use standard Thymeleaf
* expression syntax for title patterns.
*/
LayoutDialect(SortingStrategy sortingStrategy = new AppendingStrategy(), boolean autoHeadMerging = true) {
LayoutDialect(SortingStrategy sortingStrategy = new AppendingStrategy(), boolean autoHeadMerging = true,
boolean newTitleTokens = false) {

super(DIALECT_NAME, DIALECT_PREFIX, DIALECT_PRECEDENCE)

this.sortingStrategy = sortingStrategy
this.autoHeadMerging = autoHeadMerging
this.newTitleTokens = newTitleTokens
}

/**
* Returns the layout dialect's processors.
*
*
* @param dialectPrefix
* @return All of the processors for HTML and XML template modes.
*/
Expand All @@ -80,7 +86,7 @@ class LayoutDialect extends AbstractProcessorDialect {
new ReplaceProcessor(TemplateMode.HTML, dialectPrefix),
new FragmentProcessor(TemplateMode.HTML, dialectPrefix),
new CollectFragmentProcessor(TemplateMode.HTML, dialectPrefix),
new TitlePatternProcessor(TemplateMode.HTML, dialectPrefix),
new TitlePatternProcessor(TemplateMode.HTML, dialectPrefix, newTitleTokens),

// Processors available in the XML template mode
new StandardXmlNsTagProcessor(TemplateMode.XML, dialectPrefix),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
/*
/*
* Copyright 2012, Emanuel Rabina (http://www.ultraq.net.nz/)
*
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*
* http://www.apache.org/licenses/LICENSE-2.0
*
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
Expand All @@ -21,6 +21,7 @@ import nz.net.ultraq.thymeleaf.layoutdialect.decorators.html.HtmlDocumentDecorat
import nz.net.ultraq.thymeleaf.layoutdialect.decorators.xml.XmlDocumentDecorator
import nz.net.ultraq.thymeleaf.layoutdialect.fragments.FragmentFinder
import nz.net.ultraq.thymeleaf.layoutdialect.models.TemplateModelFinder
import nz.net.ultraq.thymeleaf.layoutdialect.models.TitleExtractor

import org.thymeleaf.context.ITemplateContext
import org.thymeleaf.engine.AttributeName
Expand All @@ -32,21 +33,21 @@ import org.thymeleaf.templatemode.TemplateMode

/**
* Specifies the name of the template to decorate using the current template.
*
*
* @author Emanuel Rabina
*/
class DecorateProcessor extends AbstractAttributeModelProcessor {

static final String PROCESSOR_NAME = 'decorate'
static final int PROCESSOR_PRECEDENCE = 0

private final boolean autoHeadMerging
private final SortingStrategy sortingStrategy
private final boolean autoHeadMerging

/**
* Constructor, configure this processor to work on the 'decorate' attribute
* and to use the given sorting strategy.
*
*
* @param templateMode
* @param dialectPrefix
* @param sortingStrategy
Expand All @@ -61,7 +62,7 @@ class DecorateProcessor extends AbstractAttributeModelProcessor {
/**
* Constructor, configurable processor name for the purposes of the
* deprecated {@code layout:decorator} alias.
*
*
* @param templateMode
* @param dialectPrefix
* @param sortingStrategy
Expand All @@ -80,7 +81,7 @@ class DecorateProcessor extends AbstractAttributeModelProcessor {
/**
* Locates the template to decorate and, once decorated, inserts it into the
* processing chain.
*
*
* @param context
* @param model
* @param attributeName
Expand Down Expand Up @@ -117,6 +118,11 @@ class DecorateProcessor extends AbstractAttributeModelProcessor {
def decorateTemplateData = decorateTemplate.templateData
decorateTemplate = decorateTemplate.cloneModel()

// Extract titles from content and layout templates and save to the template context
def titleExtractor = new TitleExtractor(context)
titleExtractor.extract(contentTemplate, TitlePatternProcessor.CONTENT_TITLE_KEY)
titleExtractor.extract(decorateTemplate, TitlePatternProcessor.LAYOUT_TITLE_KEY)

// Gather all fragment parts from this page to apply to the new document
// after decoration has taken place
def pageFragments = new FragmentFinder(dialectPrefix).findFragments(model)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
/*
/*
* Copyright 2012, Emanuel Rabina (http://www.ultraq.net.nz/)
*
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*
* http://www.apache.org/licenses/LICENSE-2.0
*
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
Expand All @@ -16,6 +16,8 @@

package nz.net.ultraq.thymeleaf.layoutdialect.decorators

import nz.net.ultraq.thymeleaf.expressionprocessor.ExpressionProcessor

import org.thymeleaf.context.ITemplateContext
import org.thymeleaf.engine.AttributeName
import org.thymeleaf.model.IProcessableElementTag
Expand All @@ -30,7 +32,7 @@ import java.util.regex.Pattern
* specifying a pattern with some special tokens. This can be used to extend
* the layout's title with the content's one, instead of simply overriding
* it.
*
*
* @author Emanuel Rabina
*/
class TitlePatternProcessor extends AbstractAttributeTagProcessor {
Expand All @@ -42,24 +44,30 @@ class TitlePatternProcessor extends AbstractAttributeTagProcessor {
static final String PROCESSOR_NAME = 'title-pattern'
static final int PROCESSOR_PRECEDENCE = 1

static final String CONTENT_TITLE_KEY = 'LayoutDialect::ContentTitle'
static final String LAYOUT_TITLE_KEY = 'LayoutDialect::LayoutTitle'
static final String CONTENT_TITLE_KEY = 'layoutDialectContentTitle'
static final String LAYOUT_TITLE_KEY = 'layoutDialectLayoutTitle'
static final String CONTENT_TITLE_KEY_OLD = 'LayoutDialect::ContentTitle'
static final String LAYOUT_TITLE_KEY_OLD = 'LayoutDialect::LayoutTitle'

final boolean newTitleTokens

/**
* Constructor, sets this processor to work on the 'title-pattern' attribute.
*
*
* @param templateMode
* @param dialectPrefix
*/
TitlePatternProcessor(TemplateMode templateMode, String dialectPrefix) {
TitlePatternProcessor(TemplateMode templateMode, String dialectPrefix, boolean newTitleTokens) {

super(templateMode, dialectPrefix, null, false, PROCESSOR_NAME, true, PROCESSOR_PRECEDENCE, true)

this.newTitleTokens = newTitleTokens
}

/**
* Process the {@code layout:title-pattern} directive, replaces the title text
* with the titles from the content and layout pages.
*
*
* @param context
* @param tag
* @param attributeName
Expand All @@ -76,15 +84,21 @@ class TitlePatternProcessor extends AbstractAttributeTagProcessor {
throw new IllegalArgumentException("${attributeName} processor should only appear in a <title> element")
}

def titlePattern = attributeValue
def modelFactory = context.modelFactory
def titleModel = modelFactory.createModel()

def contentTitle = context[CONTENT_TITLE_KEY]
def layoutTitle = context[LAYOUT_TITLE_KEY]
// TODO: Experimental title tokens branch
if (newTitleTokens) {
structureHandler.setBody(new ExpressionProcessor(context).processAsString(attributeValue), false)
return
}

def contentTitle = context[CONTENT_TITLE_KEY_OLD]
def layoutTitle = context[LAYOUT_TITLE_KEY_OLD]

// Break the title pattern up into tokens to map to their respective models
def titleModel = modelFactory.createModel()
if (layoutTitle && contentTitle) {
def titlePattern = attributeValue
def matcher = TOKEN_PATTERN.matcher(titlePattern)
while (matcher.find()) {
def text = titlePattern.substring(matcher.regionStart(), matcher.start())
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
/*
/*
* Copyright 2016, Emanuel Rabina (http://www.ultraq.net.nz/)
*
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*
* http://www.apache.org/licenses/LICENSE-2.0
*
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
Expand All @@ -21,19 +21,17 @@ import nz.net.ultraq.thymeleaf.layoutdialect.decorators.Decorator
import nz.net.ultraq.thymeleaf.layoutdialect.decorators.TitlePatternProcessor
import nz.net.ultraq.thymeleaf.layoutdialect.models.ElementMerger
import nz.net.ultraq.thymeleaf.layoutdialect.models.ModelBuilder
import nz.net.ultraq.thymeleaf.layoutdialect.models.TitleExtractor

import org.thymeleaf.context.ITemplateContext
import org.thymeleaf.model.IModel
import org.thymeleaf.standard.StandardDialect
import org.thymeleaf.standard.processor.StandardTextTagProcessor
import org.thymeleaf.standard.processor.StandardUtextTagProcessor

import groovy.transform.TupleConstructor

/**
* Decorator for the {@code <title>} part of the template to handle the special
* processing required for the {@code layout:title-pattern} processor.
*
*
* @author Emanuel Rabina
*/
@TupleConstructor(defaults = false)
Expand All @@ -44,7 +42,7 @@ class HtmlTitleDecorator implements Decorator {
/**
* Special decorator for the {@code <title>} part, accumulates the important
* processing parts for the {@code layout:title-pattern} processor.
*
*
* @param targetTitleModel
* @param sourceTitleModel
* @return A new {@code <title>} model that is the result of decorating the
Expand All @@ -56,7 +54,6 @@ class HtmlTitleDecorator implements Decorator {

def modelBuilder = new ModelBuilder(context)
def layoutDialectPrefix = context.getPrefixForDialect(LayoutDialect)
def standardDialectPrefix = context.getPrefixForDialect(StandardDialect)

// Get the title pattern to use
def titlePatternProcessorRetriever = { titleModel ->
Expand All @@ -72,41 +69,9 @@ class HtmlTitleDecorator implements Decorator {
// Set the title pattern to use on a new model, as well as the important
// title result parts that we want to use on the pattern.
if (titlePatternProcessor) {
def extractTitle = { titleModel, contextKey ->

// This title part already exists from a previous run, so do nothing
if (context[contextKey]) {
return
}

if (titleModel) {
def titleTag = titleModel.first()

// Escapable title from a th:text attribute on the title tag
if (titleTag.hasAttribute(standardDialectPrefix, StandardTextTagProcessor.ATTR_NAME)) {
context[(contextKey)] = modelBuilder.build {
'th:block'('th:text': titleTag.getAttributeValue(standardDialectPrefix, StandardTextTagProcessor.ATTR_NAME))
}
}

// Unescaped title from a th:utext attribute on the title tag, or
// whatever happens to be within the title tag
else if (titleTag.hasAttribute(standardDialectPrefix, StandardUtextTagProcessor.ATTR_NAME)) {
context[(contextKey)] = modelBuilder.build {
'th:block'('th:utext': titleTag.getAttributeValue(standardDialectPrefix, StandardUtextTagProcessor.ATTR_NAME))
}
}
else {
def titleChildrenModel = context.modelFactory.createModel()
titleModel.childModelIterator().each { model ->
titleChildrenModel.addModel(model)
}
context[(contextKey)] = titleChildrenModel
}
}
}
extractTitle(sourceTitleModel, TitlePatternProcessor.CONTENT_TITLE_KEY)
extractTitle(targetTitleModel, TitlePatternProcessor.LAYOUT_TITLE_KEY)
def titleExtractor = new TitleExtractor(context)
titleExtractor.extract(sourceTitleModel, TitlePatternProcessor.CONTENT_TITLE_KEY_OLD)
titleExtractor.extract(targetTitleModel, TitlePatternProcessor.LAYOUT_TITLE_KEY_OLD)

resultTitle = modelBuilder.build {
title((titlePatternProcessor.attributeCompleteName): titlePatternProcessor.value)
Expand Down
Loading

0 comments on commit fe543e1

Please sign in to comment.