LegacyStyleFlattener.java

/* ========================================================================
 * PlantUML : a free UML diagram generator
 * ========================================================================
 *
 * (C) Copyright 2009-2024, Arnaud Roques
 *
 * Project Info:  https://plantuml.com
 *
 * If you like this project or if you find it useful, you can support us at:
 *
 * https://plantuml.com/patreon (only 1$ per month!)
 * https://plantuml.com/paypal
 *
 * This file is part of PlantUML.
 *
 * PlantUML is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * PlantUML distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
 * License for more details.
 *
 * You should have received a copy of the GNU General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301,
 * USA.
 *
 *
 * Original Author:  Arnaud Roques
 *
 *
 */
package net.sourceforge.plantuml.style;

import java.util.ArrayList;
import java.util.EnumMap;
import java.util.List;
import java.util.Map;

import net.sourceforge.plantuml.style.parser2.MergedStyleNode;
import net.sourceforge.plantuml.style.parser2.PrioritizedValue;
import net.sourceforge.plantuml.style.parser2.RawSelector;
import net.sourceforge.plantuml.style.parser2.StyleQuery;

/**
 * Turns a {@link MergedStyleNode} tree (built by the new {@code parser2} text parser) into the
 * flat {@code Collection<Style>} shape the legacy {@code net.sourceforge.plantuml.style.parser}
 * package used to hand back -- so that everything downstream of parsing (loading a .skin file
 * via {@code StyleBuilder#loadInternal}, layering a {@code <style>} overlay via
 * {@code StyleBuilder#muteStyle}, both ultimately resolved through {@link StyleIndex}) keeps
 * working against legacy {@link Style}/{@link Value} objects exactly as before, unaware that the
 * text was parsed by a different engine.
 *
 * This walk follows the {@link MergedStyleNode} tree top to bottom, accumulating a
 * {@link StyleQuery} along the path, and emits one {@link Style} per node that has properties,
 * converting each {@link PrioritizedValue} to a {@link Value} on the way. Two legacy behaviors
 * matter here and are handled explicitly:
 * <ul>
 * <li>a stereotype-scoped node's properties carry their {@link Specificity} stereotype count
 * (how many stereotypes {@code pathSoFar} requires), exactly like the legacy
 * {@code net.sourceforge.plantuml.style.parser.Context#toStyles} used to apply via
 * {@code StyleLoader#addPriorityForStereotype} -- without it, a stereotype rule could lose to a
 * later plain one it must always outrank;</li>
 * <li>an unrecognized selector word (neither a known {@link SName}, a {@code .stereotype}, nor a
 * {@code depth(n)}) is folded in as a stereotype tag, exactly like the legacy
 * {@code Context#push}'s {@code SName.retrieve(s) == null -> addStereotype(s)} fallback.</li>
 * </ul>
 */
final class LegacyStyleFlattener {

	private LegacyStyleFlattener() {
	}

	/** Every declaration in {@code root}, as flat legacy {@link Style} objects. */
	static List<Style> flatten(MergedStyleNode root) {
		final List<Style> result = new ArrayList<Style>();
		flattenNode(root, StyleQuery.empty(), result);
		return result;
	}

	private static void flattenNode(MergedStyleNode node, StyleQuery pathSoFar, List<Style> result) {
		if (node.getProperties().isEmpty() == false)
			result.add(new Style(pathSoFar, toValueMap(node.getProperties(), pathSoFar.getStereotypes().size())));

		// A selector declared once plain and once starred (e.g. ".europeStyle {...}" alongside
		// ".europeStyle * {...}") lives in two separate MergedStyleNode child slots, never one --
		// see MergedStyleNode's own javadoc for why merging them would leak a non-cascading
		// property onto descendants that must only inherit the cascading one. So each of the two
		// child kinds (named, other) is visited across both its plain and its starred map.
		flattenNamedChildren(node.getNamedChildren(), pathSoFar, result);
		flattenNamedChildren(node.getStarredNamedChildren(), pathSoFar, result);
		flattenOtherChildren(node, node.getOtherChildren(), pathSoFar, result);
		flattenOtherChildren(node, node.getStarredOtherChildren(), pathSoFar, result);
	}

	private static void flattenNamedChildren(Map<SName, MergedStyleNode> children, StyleQuery pathSoFar,
			List<Style> result) {
		for (Map.Entry<SName, MergedStyleNode> ent : children.entrySet()) {
			final MergedStyleNode child = ent.getValue();
			StyleQuery childSignature = pathSoFar.addSName(ent.getKey());
			if (child.isStar())
				childSignature = childSignature.addStar();
			flattenNode(child, childSignature, result);
		}
	}

	private static void flattenOtherChildren(MergedStyleNode node, Map<String, MergedStyleNode> children,
			StyleQuery pathSoFar, List<Style> result) {
		for (Map.Entry<String, MergedStyleNode> ent : children.entrySet()) {
			final String key = ent.getKey();
			final MergedStyleNode child = ent.getValue();
			final RawSelector.Kind kind = node.getOtherChildKind(key);

			StyleQuery childSignature;
			if (kind == RawSelector.Kind.DEPTH)
				childSignature = pathSoFar.addLevel(parseDepthKey(key));
			else
				// STEREOTYPE, and UNKNOWN too -- see this class's own documentation for why an
				// unrecognized selector word is folded in as a stereotype tag here, unlike
				// StyleAtomTrie#compileNode's UNKNOWN handling.
				childSignature = pathSoFar.withStereotype(key);

			if (child.isStar())
				childSignature = childSignature.addStar();
			flattenNode(child, childSignature, result);
		}
	}

	private static Map<PName, Value> toValueMap(Map<PName, PrioritizedValue> properties, int stereotypeCount) {
		final Map<PName, Value> result = new EnumMap<PName, Value>(PName.class);
		for (Map.Entry<PName, PrioritizedValue> ent : properties.entrySet()) {
			final PrioritizedValue pv = ent.getValue();
			final Specificity specificity = Specificity.atOrder(pv.getPriority()).withStereotypeCount(stereotypeCount);
			result.put(ent.getKey(), ValueImpl.of(pv.getLight(), pv.getDark(), specificity));
		}
		return result;
	}

	private static int parseDepthKey(String key) {
		return Integer.parseInt(key.substring("depth(".length(), key.length() - 1));
	}

}