001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.juneau.console.freemarker; 018 019import static org.apache.juneau.commons.utils.Shorts.*; 020 021import java.io.*; 022import java.util.*; 023 024import org.apache.juneau.console.*; 025 026import freemarker.core.*; 027import freemarker.template.*; 028 029/** 030 * The {@code <@token name="--jc-…" value="…"/>} / {@code <@token name="--jc-…" alias="var(--jc-…)"/>} chrome 031 * FreeMarker directive: one custom theme-token override authored inside a {@code <@theme>} element. 032 * 033 * <p> 034 * Exactly one of {@code value=} / {@code alias=} is required, and they are mutually exclusive — they are the 035 * two disjoint {@link Theme} channels: 036 * <ul class='spaced-list'> 037 * <li>{@code value=} is a <b>leaf</b>: a CSS literal, or a {@code var(--jc-name)} reference that 038 * {@link org.apache.juneau.console.Theme.Builder#token(String, String) Theme.Builder#token(String, String)} <b>resolves to a literal</b> 039 * at build time (the substring {@code var(} does not reach the served leaf map). 040 * <li>{@code alias=} is a <b>derived reference</b>: a {@code var(--jc-name)} that 041 * {@link org.apache.juneau.console.Theme.Builder#alias(String, String) Theme.Builder#alias(String, String)} keeps as a reference 042 * <b>surviving to the wire</b>. A literal cannot express a live alias — that is why the attr exists. 043 * </ul> 044 * 045 * <p> 046 * The name/value/target guards and reserved-namespace rejection are the Java {@code Theme.Builder}'s own (fail 047 * closed); an {@link IllegalArgumentException} it raises is re-surfaced through {@link FtlAttrLists#reject(String)} 048 * so its exact sentence reaches the HTTP 500 body. The directive writes no markup — it folds the declaration 049 * into the enclosing {@link ThemeBuildContext}, which {@code <@theme>} builds after its body pass. 050 * 051 * <p> 052 * {@code scheme="dark"} declares the token for the dark colour scheme only. Dark declarations render in their own 053 * {@code html:root[data-jc-scheme="dark"]} rule after the light override block, holding only the declared names. A 054 * dark {@code value=} must be a literal (a {@code var()} leaf would resolve against the light palette); a dark 055 * {@code alias=} stays a live reference. {@code dark} is the only accepted value: a light token omits 056 * {@code scheme=}. 057 * 058 * <h5 class='section'>Example:</h5> 059 * <p class='bftl'> 060 * <@theme name="light-red"> 061 * <#-- A leaf: a literal. --> 062 * <@token name="--jc-pill-red-bg" value="#fdeceb"/> 063 * <#-- An alias: a live reference. --> 064 * <@token name="--jc-tab-bar-bg" alias="var(--jc-card-bg)"/> 065 * <#-- Dark scheme only. --> 066 * <@token name="--jc-link" value="#6cb4ff" scheme="dark"/> 067 * </@theme> 068 * 069 * <#-- Rejected: both value= and alias=, neither one, or a reserved --jc-chrome-* name. --> 070 * <@token name="--jc-x" value="#fff" alias="var(--jc-accent)"/> 071 * <@token name="--jc-x"/> 072 * <@token name="--jc-chrome-control-height" value="26px"/> 073 * </p> 074 * 075 * @since 10.0.0 076 */ 077@SuppressWarnings({ 078 "java:S1192", // Duplicated literals read more clearly inline than as constants 079 "java:S3776" // Validation and rendering chains many independent guards; splitting them adds no clarity. 080}) 081public final class TokenDirectiveModel implements TemplateDirectiveModel { 082 083 /** The shared-variable name this directive registers under. */ 084 public static final String NAME = "token"; 085 086 static final Set<String> ATTRS = Set.of("name", "value", "alias", "scheme"); 087 088 TokenDirectiveModel() {} 089 090 @Override 091 @SuppressWarnings({ 092 "unchecked" // FreeMarker's raw params Map is String-keyed by contract. 093 }) 094 public void execute(Environment env, @SuppressWarnings("rawtypes") Map params, TemplateModel[] loopVars, 095 TemplateDirectiveBody body) throws TemplateException, IOException { 096 Map<String, TemplateModel> p = params; 097 if (p.containsKey("format")) 098 throw FtlAttrLists.reject("<@token> has no format= attribute."); 099 FtlAttrLists.rejectUnknown(p, NAME, ATTRS); 100 101 var cap = PageCapture.get(env); 102 var ctx = n(cap) ? null : cap.themeBuild; 103 if (n(ctx)) 104 throw FtlAttrLists.reject("<@token> must be nested inside <@theme>."); 105 106 var name = FtlAttrLists.scalar(p, "name"); 107 if (name.isEmpty()) 108 throw FtlAttrLists.reject("<@token> requires name=."); 109 110 var hasValue = p.containsKey("value"); 111 var hasAlias = p.containsKey("alias"); 112 if (hasValue == hasAlias) 113 throw FtlAttrLists.reject("<@token name=\"" + name + "\"> requires exactly one of value= or alias=."); 114 115 var dark = p.containsKey("scheme"); 116 if (dark) { 117 var scheme = FtlAttrLists.scalar(p, "scheme"); 118 if (! "dark".equals(scheme)) 119 throw FtlAttrLists.reject("<@token name=\"" + name + "\"> scheme= must be 'dark'; got '" + scheme + "'. Light tokens omit scheme=."); 120 } 121 122 try { 123 if (dark) { 124 // Validated eagerly, one declaration at a time, so the diagnostic names the offending token. 125 var v = FtlAttrLists.scalar(p, hasValue ? "value" : "alias"); 126 if (hasValue) { 127 ConsoleChromeMixin.schemeOverrideBlock(Map.of(name, v), Map.of()); 128 ctx.darkTokens.put(name, v); 129 } else { 130 ConsoleChromeMixin.schemeOverrideBlock(Map.of(), Map.of(name, v)); 131 ctx.darkAliases.put(name, v); 132 } 133 } else { 134 if (hasValue) 135 ctx.themeBuilder.token(name, FtlAttrLists.scalar(p, "value")); 136 else 137 ctx.themeBuilder.alias(name, FtlAttrLists.scalar(p, "alias")); 138 ctx.lightDeclared = true; 139 } 140 } catch (IllegalArgumentException e) { 141 // Theme.Builder's frozen sentence is the diagnostic; re-surface it undecorated (see FtlAttrLists.reject). 142 throw FtlAttrLists.reject(e.getMessage()); 143 } 144 ctx.anyDeclared = true; 145 } 146}