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 &mdash; 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 &mdash; 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 &mdash; 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 *    &lt;@theme name="light-red"&gt;
061 *       &lt;#-- A leaf: a literal. --&gt;
062 *       &lt;@token name="--jc-pill-red-bg" value="#fdeceb"/&gt;
063 *       &lt;#-- An alias: a live reference. --&gt;
064 *       &lt;@token name="--jc-tab-bar-bg" alias="var(--jc-card-bg)"/&gt;
065 *       &lt;#-- Dark scheme only. --&gt;
066 *       &lt;@token name="--jc-link" value="#6cb4ff" scheme="dark"/&gt;
067 *    &lt;/@theme&gt;
068 *
069 *    &lt;#-- Rejected: both value= and alias=, neither one, or a reserved --jc-chrome-* name. --&gt;
070 *    &lt;@token name="--jc-x" value="#fff" alias="var(--jc-accent)"/&gt;
071 *    &lt;@token name="--jc-x"/&gt;
072 *    &lt;@token name="--jc-chrome-control-height" value="26px"/&gt;
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}