2323import static org .thymeleaf .templatemode .TemplateMode .HTML ;
2424
2525import java .util .ArrayList ;
26+ import java .util .Collections ;
2627import java .util .HashMap ;
2728import java .util .List ;
2829import java .util .Map ;
30+ import java .util .Set ;
31+ import java .util .stream .IntStream ;
32+ import java .util .stream .Stream ;
33+
2934import org .thymeleaf .context .ITemplateContext ;
3035import org .thymeleaf .engine .TemplateManager ;
3136import org .thymeleaf .engine .TemplateModel ;
3641import org .thymeleaf .model .IModelFactory ;
3742import org .thymeleaf .model .IOpenElementTag ;
3843import org .thymeleaf .model .IProcessableElementTag ;
44+ import org .thymeleaf .model .IStandaloneElementTag ;
3945import org .thymeleaf .model .ITemplateEvent ;
4046import org .thymeleaf .processor .element .AbstractElementModelProcessor ;
4147import org .thymeleaf .processor .element .IElementModelStructureHandler ;
@@ -55,7 +61,7 @@ public ComponentModelProcessor(String dialectPrefix, String elementName, String
5561
5662 this .dialectPrefix = dialectPrefix ;
5763 this .elementName = elementName ;
58- this .templatePath = templatePath ;
64+ this .templatePath = templatePath != null ? templatePath : dialectPrefix + "/" + elementName + "/" + elementName ;
5965 }
6066
6167 @ Override
@@ -77,9 +83,15 @@ protected void doProcess(ITemplateContext context, IModel model, IElementModelSt
7783 componentAttributes .forEach (structureHandler ::setLocalVariable );
7884
7985 IModel fragmentModel = loadFragmentModel (context );
86+ IProcessableElementTag fragmentRootElementTag = firstOpenElementTagWithAttribute (fragmentModel , "th:fragment" );
87+ if (fragmentRootElementTag != null ) {
88+ Map <String , Object > defaultAttributes = resolveComponentAttributes (fragmentRootElementTag , context , expressionParser );
89+ componentAttributes .keySet ().forEach (defaultAttributes ::remove );
90+ defaultAttributes .forEach (structureHandler ::setLocalVariable );
91+ }
8092 Map <String , List <ITemplateEvent >> slotContents = extractSlotContents (model );
8193 Map <String , ITemplateEvent > slots = extractSlots (fragmentModel );
82- IModel mergedModel = prepareModel (context , fragmentModel , additionalAttributes , slots , slotContents );
94+ IModel mergedModel = prepareModel (context , fragmentModel , fragmentRootElementTag , additionalAttributes , slots , slotContents );
8395
8496 model .reset ();
8597 model .addModel (mergedModel );
@@ -90,7 +102,7 @@ private boolean isValidComponentTag(IProcessableElementTag componentElementTag)
90102 }
91103
92104 private IModel loadFragmentModel (ITemplateContext context ) {
93- return parseFragmentTemplateModel (context , templatePath != null ? templatePath : "pl/" + elementName + "/" + elementName );
105+ return parseFragmentTemplateModel (context , templatePath );
94106 }
95107
96108 private Map <String , List <ITemplateEvent >> extractSlotContents (IModel model ) {
@@ -133,43 +145,96 @@ private Map<String, ITemplateEvent> extractSlots(IModel fragmentModel) {
133145 private IModel prepareModel (
134146 ITemplateContext context ,
135147 IModel fragmentModel ,
148+ IProcessableElementTag fragmentRootElementTag ,
136149 Map <String , Object > additionalAttributes ,
137150 Map <String , ITemplateEvent > slots ,
138151 Map <String , List <ITemplateEvent >> slotContents
139152 ) {
140153 IModelFactory modelFactory = context .getModelFactory ();
141154 IModel newModel = modelFactory .createModel ();
142155
143- newModel .add (blockOpenElement (modelFactory , additionalAttributes ));
156+ List <ITemplateEvent > fragmentElementTags = subTreeBelow (fragmentModel , fragmentRootElementTag );
157+ boolean hasPassedDownAttributes = replaceAdditionalAttributes (fragmentElementTags , modelFactory , additionalAttributes );
158+ if (!hasPassedDownAttributes ) {
159+ newModel .add (blockOpenElement (modelFactory , additionalAttributes ));
160+ }
161+
162+ fillSlots (fragmentModel , fragmentElementTags , slots , slotContents );
144163
145- List <ITemplateEvent > mergedElementTags = fillSlots (fragmentModel , slots , slotContents );
146- mergedElementTags .forEach (newModel ::add );
164+ fragmentElementTags .forEach (newModel ::add );
147165
148- newModel .add (blockCloseElement (modelFactory ));
166+ if (!hasPassedDownAttributes ) {
167+ newModel .add (blockCloseElement (modelFactory ));
168+ }
149169
150170 return newModel ;
151171 }
152172
153- private List <ITemplateEvent > fillSlots (
154- IModel fragmentModel , Map <String , ITemplateEvent > slots ,
173+ private static Map <String ,String > convertObjectMapToStringMap (Map <String , Object > objectMap ) {
174+ Map <String , String > stringMap = new HashMap <>(objectMap .size ());
175+ objectMap .forEach ((key , value ) -> stringMap .put (key , value != null ? value .toString () : null ));
176+ return stringMap ;
177+ }
178+
179+ private boolean replaceAdditionalAttributes (List <ITemplateEvent > fragmentElementTags , IModelFactory modelFactory , Map <String , Object > additionalAttributes ) {
180+ boolean replaced = false ;
181+ Set <String > removeAttributes = Collections .singleton (dialectPrefix + ":pass-additional-attributes" );
182+ for (int i = 0 ; i < fragmentElementTags .size (); i ++) {
183+ ITemplateEvent templateEvent = fragmentElementTags .get (i );
184+ if (templateEvent instanceof IProcessableElementTag elementTag
185+ && elementTag .hasAttribute (dialectPrefix , "pass-additional-attributes" )) {
186+ fragmentElementTags .set (i , copyTagWithModifiedAttributes (elementTag , modelFactory , additionalAttributes , removeAttributes ));
187+ replaced = true ;
188+ }
189+ }
190+ return replaced ;
191+ }
192+
193+ private IProcessableElementTag copyTagWithModifiedAttributes (IProcessableElementTag elementTag , IModelFactory modelFactory , Map <String , Object > additionalAttributes , Set <String > removeAttributes ) {
194+ Map <String ,String > newAttributes = additionalAttributes == null ? new HashMap <> () : convertObjectMapToStringMap (additionalAttributes );
195+ newAttributes .putAll (elementTag .getAttributeMap ());
196+ if (removeAttributes != null ) {
197+ removeAttributes .forEach (newAttributes ::remove );
198+ }
199+
200+ if (elementTag instanceof IOpenElementTag ) {
201+ return modelFactory .createOpenElementTag (
202+ elementTag .getElementCompleteName (),
203+ newAttributes ,
204+ DOUBLE ,
205+ false
206+ );
207+ } else if (elementTag instanceof IStandaloneElementTag standaloneElementTag ) {
208+ return modelFactory .createStandaloneElementTag (
209+ elementTag .getElementCompleteName (),
210+ newAttributes ,
211+ DOUBLE ,
212+ false ,
213+ standaloneElementTag .isMinimized ()
214+ );
215+ }
216+ throw new IllegalArgumentException ("Unsupported tag class" );
217+ }
218+
219+ private void fillSlots (
220+ IModel fragmentModel ,
221+ List <ITemplateEvent > fragmentElementTags ,
222+ Map <String , ITemplateEvent > slots ,
155223 Map <String , List <ITemplateEvent >> slotContents
156224 ) {
157- List <ITemplateEvent > fragmentElementTags = subTreeBelow (fragmentModel , firstOpenElementTagWithAttribute (fragmentModel , "th:fragment" ));
158225 slots .forEach ((slotName , slotElementTag ) -> {
159226 List <ITemplateEvent > slotContent = slotContents .get (slotName );
160227
161228 if (slotContent == null || slotContent .isEmpty ()) {
162- if (slotElementTag instanceof IOpenElementTag ) {
163- slotContent = fallbackSlotContent (fragmentModel , ( IOpenElementTag ) slotElementTag );
229+ if (slotElementTag instanceof IOpenElementTag openElementTag ) {
230+ slotContent = fallbackSlotContent (fragmentModel , openElementTag );
164231 } else {
165232 slotContent = emptyList ();
166233 }
167234 }
168235
169236 fillSlot (fragmentElementTags , subTreeFrom (fragmentModel , slotElementTag ), slotContent );
170237 });
171-
172- return fragmentElementTags ;
173238 }
174239
175240 private void fillSlot (List <ITemplateEvent > templateEvents , List <ITemplateEvent > slotSubTree , List <ITemplateEvent > slotContent ) {
@@ -183,8 +248,7 @@ private static List<ITemplateEvent> fallbackSlotContent(IModel fragmentModel, IO
183248 }
184249
185250 private static IOpenElementTag blockOpenElement (IModelFactory modelFactory , Map <String , Object > attributes ) {
186- Map <String , String > attributesMap = new HashMap <>();
187- attributes .forEach ((key , value ) -> attributesMap .put (key , value != null ? value .toString () : null ));
251+ Map <String , String > attributesMap = convertObjectMapToStringMap (attributes );
188252
189253 return modelFactory .createOpenElementTag ("th:block" , attributesMap , DOUBLE , false );
190254 }
@@ -194,8 +258,8 @@ private static ICloseElementTag blockCloseElement(IModelFactory modelFactory) {
194258 }
195259
196260 private boolean isSlot (ITemplateEvent templateEvent ) {
197- if (templateEvent instanceof IProcessableElementTag ) {
198- return (( IProcessableElementTag ) templateEvent ) .getElementCompleteName ().equals (dialectPrefix + ":slot" );
261+ if (templateEvent instanceof IProcessableElementTag elementTag ) {
262+ return elementTag .getElementCompleteName ().equals (dialectPrefix + ":slot" );
199263 }
200264
201265 return false ;
@@ -212,16 +276,16 @@ private String slotNameOf(IProcessableElementTag elementTag) {
212276 }
213277
214278 private static IProcessableElementTag firstOpenOrStandaloneElementTag (IModel model ) {
215- return templateEventsIn (model ). stream ()
216- .filter (( elementTag ) -> elementTag instanceof IProcessableElementTag )
279+ return templateEventsIn (model )
280+ .filter (elementTag -> elementTag instanceof IProcessableElementTag )
217281 .map (templateEvent -> (IProcessableElementTag )templateEvent )
218282 .findFirst ()
219283 .orElse (null );
220284 }
221285
222286 private static IProcessableElementTag firstOpenElementTagWithAttribute (IModel model , String attributeName ) {
223- return templateEventsIn (model ). stream ()
224- .filter (( elementTag ) -> elementTag instanceof IOpenElementTag )
287+ return templateEventsIn (model )
288+ .filter (elementTag -> elementTag instanceof IOpenElementTag )
225289 .map (templateEvent -> (IProcessableElementTag )templateEvent )
226290 .filter (elementTag -> elementTag .hasAttribute (attributeName ))
227291 .findFirst ()
@@ -263,10 +327,14 @@ private Map<String, Object> resolveAdditionalAttributes(IProcessableElementTag e
263327
264328 private static Object tryResolveAttributeValue (IAttribute attribute , ITemplateContext context ,
265329 IStandardExpressionParser expressionParser ) {
330+ String value = attribute .getValue ();
331+ if (value == null ) {
332+ return null ;
333+ }
266334 try {
267- return expressionParser .parseExpression (context , attribute . getValue () ).execute (context );
335+ return expressionParser .parseExpression (context , value ).execute (context );
268336 } catch (TemplateProcessingException e ) {
269- return attribute . getValue () ;
337+ return value ;
270338 }
271339 }
272340
@@ -319,13 +387,7 @@ static List<ITemplateEvent> subTreeFrom(IModel model, ITemplateEvent startTempla
319387 return subTree ;
320388 }
321389
322- private static List <ITemplateEvent > templateEventsIn (IModel model ) {
323- List <ITemplateEvent > templateEvents = new ArrayList <>();
324-
325- for (int i = 0 ; i < model .size (); i ++) {
326- templateEvents .add (model .get (i ));
327- }
328-
329- return templateEvents ;
390+ private static Stream <ITemplateEvent > templateEventsIn (IModel model ) {
391+ return IntStream .range (0 , model .size ()).mapToObj (model ::get );
330392 }
331393}
0 commit comments