Skip to content

Commit

Permalink
Add optional support for rel=preload|prefetch for CSS includes
Browse files Browse the repository at this point in the history
  • Loading branch information
stefanseifert committed Nov 10, 2023
1 parent 076381f commit b826317
Show file tree
Hide file tree
Showing 4 changed files with 77 additions and 15 deletions.
3 changes: 3 additions & 0 deletions changes.xml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@
<body>

<release version="1.3.0" date="not released">
<action type="add" dev="sseifert" issue="3">
Add optional support for rel=preload|prefetch for CSS includes.
</action>
<action type="update" dev="sseifert">
Switch to Java 11 as minimum version.
</action>
Expand Down
43 changes: 40 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 @@ -20,6 +20,9 @@
package io.wcm.wcm.ui.clientlibs.components;

import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;

import javax.annotation.PostConstruct;

Expand All @@ -45,6 +48,9 @@
@ProviderType
public class CSSInclude {

private static final Set<String> REL_ALLOWED_VALUES = Set.of(
"prefetch", "preload");

@SlingObject
private ResourceResolver resourceResolver;
@OSGiService
Expand All @@ -54,6 +60,8 @@ public class CSSInclude {

@RequestAttribute(injectionStrategy = InjectionStrategy.OPTIONAL)
private Object categories;
@RequestAttribute(injectionStrategy = InjectionStrategy.OPTIONAL)
private String rel;

private String include;

Expand All @@ -65,20 +73,49 @@ private void activate() {
List<String> libraryPaths = IncludeUtil.getLibraryUrls(htmlLibraryManager, resourceResolver,
categoryArray, LibraryType.CSS);
if (!libraryPaths.isEmpty()) {
this.include = buildIncludeString(libraryPaths);
Map<String, String> attrs = validateAndBuildAttributes();
this.include = buildIncludeString(libraryPaths, attrs);
}
}
}

/**
* Validate attribute values from HTL script, escape them properly and build a map with all attributes
* for the resulting script tag(s).
* @return Map with attribute for script tag
*/
private @NotNull Map<String, String> validateAndBuildAttributes() {
Map<String, String> attrs = new TreeMap<>();
if (rel != null && REL_ALLOWED_VALUES.contains(rel)) {
attrs.put("rel", rel);
}
else {
// no specific rel defined, provide default rel/type attrs for CSS
attrs.put("rel", "stylesheet");
attrs.put("type", "text/css");
}
return attrs;
}

/**
* Build CSS link tags for all client libraries with the defined custom script tag attributes set.
* @param libraryPaths Library paths
* @return HTML markup with script tags
*/
private @NotNull String buildIncludeString(@NotNull List<String> libraryPaths) {
private @NotNull String buildIncludeString(@NotNull List<String> libraryPaths, @NotNull Map<String, String> attrs) {
StringBuilder markup = new StringBuilder();
for (String libraryPath : libraryPaths) {
markup.append("<link rel=\"stylesheet\" href=\"").append(xssApi.encodeForHTMLAttr(libraryPath)).append("\" type=\"text/css\">\n");
markup.append("<link href=\"").append(xssApi.encodeForHTMLAttr(libraryPath)).append("\"");
for (Map.Entry<String, String> attr : attrs.entrySet()) {
markup.append(" ");
markup.append(attr.getKey());
if (attr.getValue() != null) {
markup.append("=\"");
markup.append(attr.getValue());
markup.append("\"");
}
}
markup.append(">\n");
}
return markup.toString();
}
Expand Down
13 changes: 7 additions & 6 deletions src/main/webapp/app-root/sightly/templates/clientlib.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
* @param crossorigin anonymous|use-credentials see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#attr-crossorigin
* @param defer true|false see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#attr-defer
* @param integrity see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#attr-integrity
* @param nomodule true|false https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#attr-nomodule
* @param nonce https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#attr-nonce
* @param nomodule true|false see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#attr-nomodule
* @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
*/-->
Expand All @@ -16,19 +16,20 @@
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}">
${clientlib.include @ context='unsafe'}
${clientlib.include @ context='unsafe'}
</sly>
</template>

<!--/**
* 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
*/-->
<template data-sly-template.css="${@ categories}">
<template data-sly-template.css="${@ categories, rel}">
<sly data-sly-test="${request.getResourceResolver}"
data-sly-use.clientlib="${'io.wcm.wcm.ui.clientlibs.components.CSSInclude' @
categories=categories}">
${clientlib.include @ context='unsafe'}
categories=categories, rel=rel}">
${clientlib.include @ context='unsafe'}
</sly>
</template>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
import org.apache.sling.api.resource.PersistenceException;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness;
Expand All @@ -42,7 +44,26 @@ class CSSIncludeTest extends AbstractIncludeTest {
void testSingle() {
context.request().setAttribute("categories", CATEGORY_SINGLE);
CSSInclude underTest = AdaptTo.notNull(context.request(), CSSInclude.class);
assertEquals("<link rel=\"stylesheet\" href=\"/etc/clientlibs/app1/clientlib1.min.css\" type=\"text/css\">\n",
assertEquals("<link href=\"/etc/clientlibs/app1/clientlib1.min.css\" rel=\"stylesheet\" type=\"text/css\">\n",
underTest.getInclude());
}

@ParameterizedTest
@ValueSource(strings = { "preload", "prefetch" })
void testSingle_rel_valid(String validRelParameter) {
context.request().setAttribute("categories", CATEGORY_SINGLE);
context.request().setAttribute("rel", validRelParameter);
CSSInclude underTest = AdaptTo.notNull(context.request(), CSSInclude.class);
assertEquals("<link href=\"/etc/clientlibs/app1/clientlib1.min.css\" rel=\"" + validRelParameter + "\">\n",
underTest.getInclude());
}

@Test
void testSingle_rel_invalid() {
context.request().setAttribute("categories", CATEGORY_SINGLE);
context.request().setAttribute("rel", "inalid");
CSSInclude underTest = AdaptTo.notNull(context.request(), CSSInclude.class);
assertEquals("<link href=\"/etc/clientlibs/app1/clientlib1.min.css\" rel=\"stylesheet\" type=\"text/css\">\n",
underTest.getInclude());
}

Expand All @@ -51,25 +72,25 @@ void testSingleUnminified() {
when(htmlLibraryManager.isMinifyEnabled()).thenReturn(false);
context.request().setAttribute("categories", CATEGORY_SINGLE);
CSSInclude underTest = AdaptTo.notNull(context.request(), CSSInclude.class);
assertEquals("<link rel=\"stylesheet\" href=\"/etc/clientlibs/app1/clientlib1.css\" type=\"text/css\">\n",
assertEquals("<link href=\"/etc/clientlibs/app1/clientlib1.css\" rel=\"stylesheet\" type=\"text/css\">\n",
underTest.getInclude());
}

@Test
void testSingleProxy() {
context.request().setAttribute("categories", CATEGORY_SINGLE_PROXY);
CSSInclude underTest = AdaptTo.notNull(context.request(), CSSInclude.class);
assertEquals("<link rel=\"stylesheet\" href=\"/etc.clientlibs/app1/clientlibs/clientlib2_proxy.min.css\" type=\"text/css\">\n",
assertEquals("<link href=\"/etc.clientlibs/app1/clientlibs/clientlib2_proxy.min.css\" rel=\"stylesheet\" type=\"text/css\">\n",
underTest.getInclude());
}

@Test
void testMulti() {
context.request().setAttribute("categories", CATEGORIES_MULTIPLE);
CSSInclude underTest = AdaptTo.notNull(context.request(), CSSInclude.class);
assertEquals("<link rel=\"stylesheet\" href=\"/etc/clientlibs/app1/clientlib3.min.css\" type=\"text/css\">\n"
+ "<link rel=\"stylesheet\" href=\"/etc.clientlibs/app1/clientlibs/clientlib4_proxy.min.css\" type=\"text/css\">\n"
+ "<link rel=\"stylesheet\" href=\"/etc.clientlibs/app1/clientlibs/clientlib5_proxy.min.css\" type=\"text/css\">\n",
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",
underTest.getInclude());
}

Expand Down

0 comments on commit b826317

Please sign in to comment.