All Downloads are FREE. Search and download functionalities are using the official Maven repository.

nz.net.ultraq.thymeleaf.layoutdialect.models.extensions.IModelExtensions.groovy Maven / Gradle / Ivy

Go to download

A dialect for Thymeleaf that allows you to use layout/decorator templates to style your content.

The newest version!
/* 
 * 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.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package nz.net.ultraq.thymeleaf.layoutdialect.models.extensions

import org.thymeleaf.engine.TemplateModel
import org.thymeleaf.model.ICloseElementTag
import org.thymeleaf.model.IModel
import org.thymeleaf.model.IModelFactory
import org.thymeleaf.model.IOpenElementTag
import org.thymeleaf.model.ITemplateEvent

import groovy.transform.stc.ClosureParams
import groovy.transform.stc.SimpleType

/**
 * Meta-programming extensions to the {@link IModel} class.
 * 
 * @author Emanuel Rabina
 */
class IModelExtensions {

	/**
	 * Set that a model evaluates to 'false' if it has no events.
	 * 
	 * @param self
	 * @return {@code true} if this model has events.
	 */
	static boolean asBoolean(IModel self) {
		return self.size() > 0
	}

	/**
	 * If this model represents an element, then this method returns an
	 * iterator over any potential child items as models of their own.
	 * 
	 * @param self
	 * @param modelFactory
	 * @return New model iterator.
	 */
	static Iterator childModelIterator(IModel self) {
		return self.element ? new ChildModelIterator(self) : null
	}

	/**
	 * Iterate through each event in the model.
	 * 
	 * @param self
	 * @param closure
	 */
	static void each(IModel self,
		@ClosureParams(value = SimpleType, options = 'org.thymeleaf.model.ITemplateEvent')
		Closure closure) {
		self.iterator().each(closure)
	}

	/**
	 * Compare 2 models, returning {@code true} if all of the model's events
	 * are equal.
	 * 
	 * @param self
	 * @param other
	 * @return {@code true} if this model is the same as the other one.
	 */
	@SuppressWarnings('EqualsOverloaded')
	static boolean equals(IModel self, Object other) {
		if (other instanceof IModel && self.size() == other.size()) {
			return self.everyWithIndex { event, index -> event == other.get(index) }
		}
		return false
	}

	/**
	 * Return {@code true} only if all the events in the model return
	 * {@code true} for the given closure.
	 * 
	 * @param self
	 * @param closure
	 * @return {@code true} if every event satisfies the closure.
	 */
	static boolean everyWithIndex(IModel self,
		@ClosureParams(value = SimpleType, options = ['org.thymeleaf.model.ITemplateEvent', 'int'])
		Closure closure) {
		for (def i = 0; i < self.size(); i++) {
			if (!closure(self.get(i), i)) {
				return false
			}
		}
		return true
	}

	/**
	 * Returns the first event in the model that meets the criteria of the
	 * given closure.
	 * 
	 * @param self
	 * @param closure
	 * @return The first event to match the closure criteria, or {@code null}
	 *         if nothing matched.
	 */
	static ITemplateEvent find(IModel self,
		@ClosureParams(value = SimpleType, options = 'org.thymeleaf.model.ITemplateEvent')
		Closure closure) {
		return self.iterator().find(closure)
	}

	/**
	 * Find all events in the model that match the given closure.
	 * 
	 * @param self
	 * @param closure
	 * @return A list of matched events.
	 */
	static List findAll(IModel self,
		@ClosureParams(value = SimpleType, options = 'org.thymeleaf.model.ITemplateEvent')
		Closure closure) {
		return self.iterator().findAll(closure)
	}

	/**
	 * Returns the index of the first event in the model that meets the
	 * criteria of the given closure.
	 * 
	 * @param self
	 * @param closure
	 * @return The index of the first event to match the closure criteria, or
	 *         {@code -1} if nothing matched.
	 */
	static int findIndexOf(IModel self,
		@ClosureParams(value = SimpleType, options = 'org.thymeleaf.model.ITemplateEvent')
		Closure closure) {
		return self.iterator().findIndexOf(closure)
	}

	/**
	 * A special variant of {@code findIndexOf} that uses models, as I seem to
	 * be using those a lot.
	 * 
	 * This doesn't use an equality check, but an object reference check, so
	 * if a submodel is ever located from a parent (eg: any of the {@code find}
	 * methods, you can use this method to find the location of that submodel
	 * within the event queue.
	 * 
	 * @param self
	 * @param model
	 * @return Index of an extracted submodel within this model.
	 */
	static int findIndexOfModel(IModel self, IModel model) {
		def modelEvent = model.first()
		return self.findIndexOf { event -> event.is(modelEvent) }
	}

	/**
	 * Returns the first instance of a model that meets the given closure
	 * criteria.
	 * 
	 * @param self
	 * @param closure
	 * @return A model over the event that matches the closure criteria, or
	 *         {@code null} if nothing matched.
	 */
	static IModel findModel(IModel self,
		@ClosureParams(value = SimpleType, options = 'org.thymeleaf.model.ITemplateEvent')
		Closure closure) {
		return self.getModel(self.findIndexOf(closure))
	}

	/**
	 * Returns the first event on the model.
	 * 
	 * @param self
	 * @return The model's first event.
	 */
	static ITemplateEvent first(IModel self) {
		return self.get(0)
	}

	/**
	 * Returns the model at the given index.  If the event at the index is an
	 * opening element, then the returned model will consist of that element
	 * and all the way through to the matching closing element.
	 * 
	 * @param self
	 * @param pos A valid index within the current model.
	 * @return Model at the given position, or `null` if the position is
	 *         outside of the event queue.
	 */
	static IModel getModel(IModel self, int pos) {
		if (0 <= pos && pos < self.size()) {
			def clone = self.cloneModel()
			def removeBefore = self instanceof TemplateModel ? pos - 1 : pos
			def removeAfter = clone.size() - (removeBefore + self.sizeOfModelAt(pos))
			while (removeBefore-- > 0) {
				clone.removeFirst()
			}
			while (removeAfter-- > 0) {
				clone.removeLast()
			}
			return clone
		}
		return null
	}

	/**
	 * Inserts a model, creating whitespace events around it so that it appears in
	 * line with all the existing events.
	 * 

* This is currently only targeting uses in the layout dialect so doesn't work * very well as a general-purpose whitespace generator. * * @param self * @param pos A valid index within the current model. * @param model * @param modelFactory */ static void insertModelWithWhitespace(IModel self, int pos, IModel model, IModelFactory modelFactory) { if (0 <= pos && pos <= self.size()) { // Derive the amount of whitespace to apply by finding the first // whitespace event before the insertion point. Defaults to a single tab. def whitespace = '\t' if (pos > 0) { for (def i = pos - 1; i >= 0; i--) { def event = self.get(i) if (event.whitespace && !event.text.empty) { whitespace = event.text.replaceAll(~/[\r\n]/, '') break } } } // Insert an extra whitespace event for when adding to an immediately-closed // element, eg:

if (pos > 0 && self.get(pos - 1) instanceof IOpenElementTag && self.get(pos) instanceof ICloseElementTag) { self.insertModel(pos, modelFactory.createModel(modelFactory.createText(System.lineSeparator()))) } self.insertModel(pos, model) self.insertModel(pos, modelFactory.createModel(modelFactory.createText(System.lineSeparator() + whitespace))) } } /** * Inserts an event, creating a whitespace event before it so that it * appears in line with all the existing events. * * @param self * @param pos A valid index within the current model. * @param event * @param modelFactory */ static void insertWithWhitespace(IModel self, int pos, ITemplateEvent event, IModelFactory modelFactory) { if (0 <= pos && pos <= self.size()) { // TODO: Because I can't check the parent for whitespace hints, I should // make this smarter and find whitespace within the model to copy. def whitespace = self.getModel(pos) if (whitespace?.whitespace) { self.insert(pos, event) self.insertModel(pos, whitespace) } else { def newLine = modelFactory.createText('\n') if (pos == 0) { self.insert(pos, newLine) self.insert(pos, event) } else if (pos == self.size()) { self.insert(pos, newLine) self.insert(pos, event) self.insert(pos, newLine) } } } } /** * Returns whether or not this model represents a single HTML element. * * @param self * @return {@code true} if the first event in this model is an opening tag * and the last event is the matching closing tag. */ static boolean isElement(IModel self) { return self.sizeOfModelAt(0) == self.size() } /** * Returns whether or not this model represents collapsible whitespace. * * @param self * @return {@code true} if this is a collapsible text model. */ static boolean isWhitespace(IModel self) { return self.size() == 1 && self.first().whitespace } /** * Used to make this class iterable as an event queue. * * @param self * @return A new iterator over the events of this model. */ static Iterator iterator(IModel self) { return new EventIterator(self) } /** * Returns the last event on the model. * * @param self * @return The model's last event. */ @SuppressWarnings('UnnecessaryCallForLastElement') static ITemplateEvent last(IModel self) { return self.get(self.size() - 1) } /** * Remove a model identified by an event matched by the given closure. Note * that this closure can match any event in the model, including the top-level * model itself. * * @param self * @param closure */ static void removeAllModels(IModel self, @ClosureParams(value = SimpleType, options = 'org.thymeleaf.model.ITemplateEvent') Closure closure) { while (true) { def modelIndex = self.findIndexOf(closure) if (modelIndex == -1) { return } self.removeModel(modelIndex) } } /** * If the model represents an element open to close tags, then this method * removes all of the inner events. * * @param self */ static void removeChildren(IModel self) { if (self.element) { while (self.size() > 2) { self.remove(1) } } } /** * Removes the first event on the model. * * @param self */ static void removeFirst(IModel self) { self.remove(0) } /** * Removes the last event on the model. * * @param self */ static void removeLast(IModel self) { self.remove(self.size() - 1) } /** * Removes a models-worth of events from the specified position. What * this means is that, if the event at the position is an opening element, * then it, and everything up to and including its matching end element, * is removed. * * @param self * @param pos A valid index within the current model. */ static void removeModel(IModel self, int pos) { if (0 <= pos && pos < self.size()) { def modelSize = self.sizeOfModelAt(pos) while (modelSize > 0) { self.remove(pos) modelSize-- } } } /** * Replaces the model at the specified index with the given model. * * @param self * @param pos A valid index within the current model. * @param model */ static void replaceModel(IModel self, int pos, IModel model) { if (0 <= pos && pos < self.size()) { self.removeModel(pos) self.insertModel(pos, model) } } /** * If an opening element exists at the given position, this method will * return the 'size' of that element (number of events from here to its * matching closing tag). * * @param self * @param model * @param index * @return Size of an element from the given position, or 1 if the event * at the position isn't an opening element. */ @SuppressWarnings('EmptyIfStatement') static int sizeOfModelAt(IModel self, int index) { def eventIndex = index def event = self.get(eventIndex++) if (event instanceof IOpenElementTag) { def level = 0 while (true) { event = self.get(eventIndex++) if (event instanceof IOpenElementTag) { level++ } else if (event instanceof ICloseElementTag) { if (event.unmatched) { // Do nothing. Unmatched closing tags do not correspond to any // opening element, and so should not affect the model level. } else if (level == 0) { break } else { level-- } } } return eventIndex - index } return 1 } /** * Removes whitespace events from the head and tail of the model's * underlying event queue. * * @param self */ static void trim(IModel self) { while (self.first().whitespace) { self.removeFirst() } while (self.last().whitespace) { self.removeLast() } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy