Skip to content

Commit

Permalink
Support custom attributes for CSS and JS includes (#2)
Browse files Browse the repository at this point in the history
  • Loading branch information
stefanseifert authored Nov 17, 2023
1 parent 808db05 commit c008d4c
Show file tree
Hide file tree
Showing 8 changed files with 79 additions and 19 deletions.
3 changes: 3 additions & 0 deletions changes.xml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@
<action type="add" dev="sseifert" issue="3">
All HTML attributes for JS and CSS includes are now rendered in alphabetically order.
</action>
<action type="add" dev="sseifert" issue="1">
Support custom attributes for CSS and JS includes.
</action>
<action type="update" dev="sseifert">
Switch to Java 11 as minimum version.
</action>
Expand Down
13 changes: 10 additions & 3 deletions src/main/java/io/wcm/wcm/ui/clientlibs/components/CSSInclude.java
Original file line number Diff line number Diff line change
Expand Up @@ -62,19 +62,22 @@ public class CSSInclude {
private Object categories;
@RequestAttribute(injectionStrategy = InjectionStrategy.OPTIONAL)
private String rel;
@RequestAttribute(injectionStrategy = InjectionStrategy.OPTIONAL)
private Object customAttributes;

private String include;

@PostConstruct
private void activate() {
// build include string
String[] categoryArray = IncludeUtil.toCategoryArray(categories);
String[] categoryArray = IncludeUtil.toArray(categories);
if (categoryArray != null) {
List<String> libraryPaths = IncludeUtil.getLibraryUrls(htmlLibraryManager, resourceResolver,
categoryArray, LibraryType.CSS);
if (!libraryPaths.isEmpty()) {
Map<String, String> attrs = validateAndBuildAttributes();
this.include = buildIncludeString(libraryPaths, attrs);
Map<String, String> customAttrs = IncludeUtil.getCustomAttributes(customAttributes);
this.include = buildIncludeString(libraryPaths, attrs, customAttrs);
}
}
}
Expand All @@ -100,13 +103,17 @@ private void activate() {
/**
* Build CSS link tags for all client libraries with the defined custom script tag attributes set.
* @param libraryPaths Library paths
* @param attrs HTML attributes for link tag
* @param customAttrs Custom HTML attributes for script tag
* @return HTML markup with script tags
*/
private @NotNull String buildIncludeString(@NotNull List<String> libraryPaths, @NotNull Map<String, String> attrs) {
private @NotNull String buildIncludeString(@NotNull List<String> libraryPaths, @NotNull Map<String, String> attrs,
@NotNull Map<String, String> customAttrs) {
StringBuilder markup = new StringBuilder();
for (String libraryPath : libraryPaths) {
HtmlTagBuilder builder = new HtmlTagBuilder("link", false, xssApi);
builder.setAttrs(attrs);
builder.setAttrs(customAttrs);
builder.setAttr("href", libraryPath);
markup.append(builder.build());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@
package io.wcm.wcm.ui.clientlibs.components;

import java.lang.reflect.Array;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;

Expand All @@ -44,7 +47,7 @@ private IncludeUtil() {
/**
* @return Array of clientlib category names as specified in HTL script
*/
public static @Nullable String[] toCategoryArray(Object categories) {
public static @Nullable String[] toArray(Object categories) {
String[] categoryArray = null;
if (categories instanceof String) {
categoryArray = new String[] { (String)categories };
Expand Down Expand Up @@ -100,4 +103,34 @@ else if (resourceResolver.getResource(library.getPath()) == null) {
return path;
}

/**
* Transform list of custom attributes to a map.
* @param customAttributes List of custom attributes in syntax "attr=value" for each item.
* @return Map with custom attributes
*/
public static @NotNull Map<String, String> getCustomAttributes(@Nullable Object customAttributes) {
String[] customAttributesArray = toArray(customAttributes);
if (customAttributesArray == null) {
return Collections.emptyMap();
}
Map<String, String> result = new HashMap<>();
for (String item : customAttributesArray) {
if (item != null) {
int separator = item.indexOf('=');
String name;
String value;
if (separator > 0) {
name = item.substring(0, separator);
value = item.substring(separator + 1);
}
else {
name = item;
value = null;
}
result.put(name, value);
}
}
return result;
}

}
12 changes: 9 additions & 3 deletions src/main/java/io/wcm/wcm/ui/clientlibs/components/JSInclude.java
Original file line number Diff line number Diff line change
Expand Up @@ -82,19 +82,22 @@ public class JSInclude {
private String referrerpolicy;
@RequestAttribute(injectionStrategy = InjectionStrategy.OPTIONAL)
private String type;
@RequestAttribute(injectionStrategy = InjectionStrategy.OPTIONAL)
private Object customAttributes;

private String include;

@PostConstruct
private void activate() {
// build include string
String[] categoryArray = IncludeUtil.toCategoryArray(categories);
String[] categoryArray = IncludeUtil.toArray(categories);
if (categoryArray != null) {
List<String> libraryPaths = IncludeUtil.getLibraryUrls(htmlLibraryManager, resourceResolver,
categoryArray, LibraryType.JS);
if (!libraryPaths.isEmpty()) {
Map<String, String> attrs = validateAndBuildAttributes();
this.include = buildIncludeString(libraryPaths, attrs);
Map<String, String> customAttrs = IncludeUtil.getCustomAttributes(customAttributes);
this.include = buildIncludeString(libraryPaths, attrs, customAttrs);
}
}
}
Expand Down Expand Up @@ -137,13 +140,16 @@ private void activate() {
* Build script tags for all client libraries with the defined custom script tag attributes set.
* @param libraryPaths Library paths
* @param attrs HTML attributes for script tag
* @param customAttrs Custom HTML attributes for script tag
* @return HTML markup with script tags
*/
private @NotNull String buildIncludeString(@NotNull List<String> libraryPaths, @NotNull Map<String, String> attrs) {
private @NotNull String buildIncludeString(@NotNull List<String> libraryPaths, @NotNull Map<String, String> attrs,
@NotNull Map<String, String> customAttrs) {
StringBuilder markup = new StringBuilder();
for (String libraryPath : libraryPaths) {
HtmlTagBuilder builder = new HtmlTagBuilder("script", true, xssApi);
builder.setAttrs(attrs);
builder.setAttrs(customAttrs);
builder.setAttr("src", libraryPath);
markup.append(builder.build());
}
Expand Down
10 changes: 6 additions & 4 deletions src/main/webapp/app-root/sightly/templates/clientlib.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,14 @@
* @param nonce see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#attr-nonce
* @param referrerpolicy see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#attr-referrerpolicy
* @param type see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#attr-type
* @param customAttributes List of custom attributes, each list item in syntax 'attr=value' or just 'attr'
*/-->
<template data-sly-template.js="${@ categories, async, crossorigin, defer, integrity,
nomodule, nonce, referrerpolicy, type}">
nomodule, nonce, referrerpolicy, type, customAttributes}">
<sly data-sly-test="${request.getResourceResolver}"
data-sly-use.clientlib="${'io.wcm.wcm.ui.clientlibs.components.JSInclude' @
categories=categories, async=async, crossorigin=crossorigin, defer=defer, integrity=integrity,
nomodule=nomodule, nonce=nonce, referrerpolicy=referrerpolicy, type=type}">
nomodule=nomodule, nonce=nonce, referrerpolicy=referrerpolicy, type=type, customAttributes=customAttributes}">
${clientlib.include @ context='unsafe'}
</sly>
</template>
Expand All @@ -24,11 +25,12 @@
* Template used for including CSS client libraries.
* @param categories Client Library categories
* @param rel prefetch|preload see https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/rel
* @param customAttributes List of custom attributes, each list item in syntax 'attr=value' or just 'attr'
*/-->
<template data-sly-template.css="${@ categories, rel}">
<template data-sly-template.css="${@ categories, customAttributes}">
<sly data-sly-test="${request.getResourceResolver}"
data-sly-use.clientlib="${'io.wcm.wcm.ui.clientlibs.components.CSSInclude' @
categories=categories, rel=rel}">
categories=categories, rel=rel, customAttributes=customAttributes}">
${clientlib.include @ context='unsafe'}
</sly>
</template>
Expand Down
3 changes: 3 additions & 0 deletions src/site/markdown/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,11 @@ The following advanced script tag attributes are supported:
* `nonce` = {string}
* `referrerpolicy` = no-referrer | no-referrer-when-downgrade | origin | origin-when-cross-origin | same-origin | strict-origin | strict-origin-when-cross-origin | unsafe-url
* `type` = module | text/javascript
* `customAttributes` - set arbitrary HTML attributes, e.g. `customAttributes=['attr1=value 1','data-attr2=5','attr3']`

See https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#Attributes for a full documentation of this attributes.


### Include CSS Files

Include CSS without special attributes:
Expand All @@ -42,3 +44,4 @@ Include CSS without special attributes:
The following advanced link tag attributes are supported:

* `rel` = prefetch | preload (if not given, `rel="stylesheet" type="text/css"` is set)
* `customAttributes` - set arbitrary HTML attributes, e.g. `customAttributes=['attr1=value 1','data-attr2=5','attr3']`
Original file line number Diff line number Diff line change
Expand Up @@ -85,12 +85,13 @@ void testSingleProxy() {
}

@Test
void testMulti() {
void testMultiWithCustom() {
context.request().setAttribute("categories", CATEGORIES_MULTIPLE);
context.request().setAttribute("customAttributes", new String[] { "attr1=value1", "data-attr2=5", "attr3" });
CSSInclude underTest = AdaptTo.notNull(context.request(), CSSInclude.class);
assertEquals("<link href=\"/etc/clientlibs/app1/clientlib3.min.css\" rel=\"stylesheet\" type=\"text/css\">\n"
+ "<link href=\"/etc.clientlibs/app1/clientlibs/clientlib4_proxy.min.css\" rel=\"stylesheet\" type=\"text/css\">\n"
+ "<link href=\"/etc.clientlibs/app1/clientlibs/clientlib5_proxy.min.css\" rel=\"stylesheet\" type=\"text/css\">\n",
assertEquals("<link attr1=\"value1\" attr3 data-attr2=\"5\" href=\"/etc/clientlibs/app1/clientlib3.min.css\" rel=\"stylesheet\" type=\"text/css\">\n"
+ "<link attr1=\"value1\" attr3 data-attr2=\"5\" href=\"/etc.clientlibs/app1/clientlibs/clientlib4_proxy.min.css\" rel=\"stylesheet\" type=\"text/css\">\n"
+ "<link attr1=\"value1\" attr3 data-attr2=\"5\" href=\"/etc.clientlibs/app1/clientlibs/clientlib5_proxy.min.css\" rel=\"stylesheet\" type=\"text/css\">\n",
underTest.getInclude());
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,15 +127,20 @@ void testSingleInvalidAttributes() {
}

@Test
void testMultiAttributes() {
void testMultiAttributesWithCustom() {
context.request().setAttribute("categories", CATEGORIES_MULTIPLE);
context.request().setAttribute("async", true);
context.request().setAttribute("nomodule", true);
context.request().setAttribute("type", "text/javascript");
context.request().setAttribute("customAttributes", new String[] { "attr1=value1", "data-attr2=5", "attr3" });
JSInclude underTest = AdaptTo.notNull(context.request(), JSInclude.class);
assertEquals("<script async nomodule src=\"/etc/clientlibs/app1/clientlib3.min.js\" type=\"text/javascript\"></script>\n"
+ "<script async nomodule src=\"/etc.clientlibs/app1/clientlibs/clientlib4_proxy.min.js\" type=\"text/javascript\"></script>\n"
+ "<script async nomodule src=\"/etc.clientlibs/app1/clientlibs/clientlib5_proxy.min.js\" type=\"text/javascript\"></script>\n",
assertEquals(
"<script async attr1=\"value1\" attr3 data-attr2=\"5\" nomodule "
+ "src=\"/etc/clientlibs/app1/clientlib3.min.js\" type=\"text/javascript\"></script>\n"
+ "<script async attr1=\"value1\" attr3 data-attr2=\"5\" nomodule "
+ "src=\"/etc.clientlibs/app1/clientlibs/clientlib4_proxy.min.js\" type=\"text/javascript\"></script>\n"
+ "<script async attr1=\"value1\" attr3 data-attr2=\"5\" nomodule "
+ "src=\"/etc.clientlibs/app1/clientlibs/clientlib5_proxy.min.js\" type=\"text/javascript\"></script>\n",
underTest.getInclude());
}

Expand Down

0 comments on commit c008d4c

Please sign in to comment.