Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Supporting additional CSS properties #117

Open
soundasleep opened this issue Jun 20, 2022 · 1 comment
Open

Supporting additional CSS properties #117

soundasleep opened this issue Jun 20, 2022 · 1 comment

Comments

@soundasleep
Copy link

soundasleep commented Jun 20, 2022

Hello! Thank you so much for this project, it's incredible.

I'm building a libgdx-based game engine with rendering based on CSSBox, and I'm needing to add additional custom CSS properties, not just for CSS3 things not yet supported (such as text-shadow and text-decoration-thickness), but also custom things specific to my renderer (like x-tooltip-position or x-selection-color).

I've got a vaguely-working solution by extending SupportedCSS3's setSupportedCSS(), and then accessing the value directly:

// connecting up my engine
CSSFactory.registerSupportedCSS(MyCustomSupportedCSS3.getInstance());
DOMAnalyzer da = new DOMAnalyzer(document, url);
// ... etc

// in my BoxRenderer
@Override
public void renderTextContent(TextBox text) {
    System.out.println("text-shadow = " + text.getParent().getStyle().getValue("text-shadow", false /* not sure why I have to force no inherited styles */));
    // ...
}

However, since jStyleParser is LGPL, I'd need to put my changes to SupportedCSS3 into a separate library. (Which is fine! Open source ❤️)

Before I do that, would you be open to having a method to register additional supported CSS properties? I'm thinking something like

SupportedCSS3.getInstance().addSupportedProperty("x-selection-color", Color.color);
SupportedCSS3.getInstance().addSupportedProperty("x-tooltip-position", MyCustomEnum.component_values);

which would just look like

public void addSupportedProperty(String key, CSSProperty value) {
  this.defaultCSSproperties.add(key, value);
}

(and of course, addSupportedDefaultValue, addSupportedOrdinal, etc)

I'mappy to try and attempt a PR, not sure how awkward it'll be to get a jStyleParser dev environment working.

@soundasleep
Copy link
Author

soundasleep commented Jun 21, 2022

Update: Found out that, along with adding a supported property, you also need to register a declaration parser. So my original solution is not suitable. With liberal use of delegation and updating the factories, I've got a solution working with any type of term, which I'm sharing for anyone else looking to add additional CSS properties with CSSbox or jStyleParser:

(the css we want to support)

.custom-css {
    x-custom-color: red;
    x-custom-dimension: 4px 8px;
}

(before creating a DOMAnalyzer)

SupportedCSS3Extender.addCSS3Extensions();			
DOMAnalyzer da = new DOMAnalyzer(document, url);
// ...

(extending SupportedCSS3 with new values)

public class SupportedCSS3Extender {
	/** have we extended {@link SupportedCSS3} yet? */
	private static boolean isApplied = false;
	
	private SupportedCSS3Extender() {
		// empty, prevent instantiation
	}

	public enum CustomDimension implements CSSProperty {
		list_values(""),
			INHERIT("inherit"), INITIAL("initial"), UNSET("unset");

		private String text;

		private CustomDimension(String text) {
			this.text = text;
		}

		@Override
		public boolean inherited() {
			return false;
		}

		@Override
		public boolean equalsInherit() {
			return this == INHERIT;
		}

        @Override
	public boolean equalsInitial() {
            return this == INITIAL;
        }

        @Override
	public boolean equalsUnset() {
            return this == UNSET;
        }
		
        @Override
        public ValueType getValueType() {
            return ValueType.LIST;
        }
        
		@Override
		public String toString() {
			return text;
		}
	}
	
	/**
	 * If not already registered, registers additional supported CSS3 properties
	 * for our CSSBox.
	 * 
	 * <p>
	 * Does nothing if already registered.
	 */
	public static synchronized void addCSS3Extensions() {
		if (!isApplied) {
			CSSFactory.registerDeclarationTransformer(dtInstance);
			CSSFactory.registerSupportedCSS(supportedCssInstance);
			isApplied = true;
		}
	}
	
	private static ExtendedSupportedCSS3 supportedCssInstance = new ExtendedSupportedCSS3();
	private static ExtendedDeclarationTransformerImpl dtInstance = new ExtendedDeclarationTransformerImpl();
}

(extended SupportedCSS with additional CSS properties)

public class ExtendedSupportedCSS3 implements SupportedCSS {
	private SupportedCSS3 delegate = SupportedCSS3.getInstance();
	
	private Map<String, CSSProperty> additionalDefaultCssProperties = new HashMap<>();
	private Map<String, Term<?>> additionalDefaultCssValues = new HashMap<>();

	public ExtendedSupportedCSS3() {
		additionalDefaultCssProperties.put("x-custom-color", CSSProperty.Color.color);
		additionalDefaultCssProperties.put("x-custom-dimension", CustomDimension.list_values);
		
		setOrdinals();
	}

	@Override
	public int getTotalProperties() {
		return delegate.getTotalProperties() + additionalDefaultCssProperties.size();
	}

	@Override
	public Set<String> getDefinedPropertyNames() {
		Set<String> result = new HashSet<>();
		result.addAll(delegate.getDefinedPropertyNames());
		result.addAll(additionalDefaultCssProperties.keySet());
		return result;
	}

	@Override
	public boolean isSupportedMedia(String media) {
		return delegate.isSupportedMedia(media);
	}

	@Override
	public boolean isSupportedCSSProperty(String property) {
		if (additionalDefaultCssProperties.containsKey(property)) {
			return true;
		}
		
		return delegate.isSupportedCSSProperty(property);
	}

	@Override
	public CSSProperty getDefaultProperty(String propertyName) {
		if (additionalDefaultCssProperties.containsKey(propertyName)) {
			return additionalDefaultCssProperties.get(propertyName);
		}
		
		return delegate.getDefaultProperty(propertyName);
	}

	@Override
	public Term<?> getDefaultValue(String propertyName) {
		if (additionalDefaultCssValues.containsKey(propertyName)) {
			return additionalDefaultCssValues.get(propertyName);
		}
		
		return delegate.getDefaultValue(propertyName);
	}

	@Override
	public String getRandomPropertyName() {
		return delegate.getRandomPropertyName(); // ?? what a weird method
	}

	// for faster performance, I think: loops for keys -> ints
	private Map<String, Integer> additionalOrdinals = new HashMap<>();
	private Map<Integer, String> additionalOrdinalsReverse = new HashMap<>();
	
	// we assume that our delegate uses ordinals up to 200, so we can use bigger ones
	private static final int MINIMUM_ADDITIONAL_ORDINAL_INDEX = 200;
	
	private void setOrdinals() {
		int i = MINIMUM_ADDITIONAL_ORDINAL_INDEX;
		for (String key : additionalDefaultCssProperties.keySet()) {
			additionalOrdinals.put(key, i);
			additionalOrdinalsReverse.put(i, key);
			i++;
		}
	}

	@Override
	public int getOrdinal(String propertyName) {
		if (additionalOrdinals.containsKey(propertyName)) {
			return additionalOrdinals.get(propertyName).intValue();
		}
		
		return delegate.getOrdinal(propertyName);
	}

	@Override
	public String getPropertyName(int o) {
		Integer i = Integer.valueOf(o);
		
		if (additionalOrdinalsReverse.containsKey(i)) {
			return additionalOrdinalsReverse.get(i);
		}

		return delegate.getPropertyName(o);
	}
}

(custom Declaration Transformer)

public class ExtendedDeclarationTransformerImpl implements DeclarationTransformer {
	private DeclarationTransformer delegate = DeclarationTransformerImpl.getInstance();

	@Override
	public boolean parseDeclaration(Declaration d, Map<String, CSSProperty> properties,
			Map<String, Term<?>> values) {
		
		final String propertyName = d.getProperty();
		System.out.println("parse -> " + propertyName);
		
		switch (propertyName) {
		case "x-custom-color":
			return processColor(d, properties, values);
		case "x-custom-dimension":
			return processCustomDimensionList(d, properties, values);
		
		default:
			return delegate.parseDeclaration(d, properties, values);
		}
	}

	private boolean processColor(Declaration d,
			Map<String, CSSProperty> properties, Map<String, Term<?>> values) {
		return Decoder.genericOneIdentOrColor(Color.class, Color.color, d, properties,
				values);
	}
    
    private boolean processCustomDimensionList(Declaration d,
            Map<String, CSSProperty> properties, Map<String, Term<?>> values) {
        return Decoder.genericTwoIdentsOrLengthsOrPercents(CustomDimension.class,
                CustomDimension.list_values, ValueRange.DISALLOW_NEGATIVE, d, properties, values);
    }
}

And finally, using a CSSBox renderer to get the actual values from CSS:

public void renderTextContent(TextBox text) {
	VisualContext ctx = text.getVisualContext();
	
	CSSDecoder dec = new CSSDecoder(ctx);
	NodeData style = text.getParent().getStyle();

	TermColor customColor = style.getValue(TermColor.class, "x-custom-color");
	System.out.println("customColor -> " + customColor);
	
	TermList customDimension = style.getValue(TermList.class, "x-custom-dimension");
	if (customDimension.size() == 2) {
		// two terms
		TermLengthOrPercent term1 = (TermLengthOrPercent) customDimension.get(0);
		TermLengthOrPercent term2 = (TermLengthOrPercent) customDimension.get(1);
		
		System.out.println(String.format("customDimension -> %s, %s", term1, term2));
		
		float calculatedWidth = dec.getLength(term1, false, 0, 0, drawArea.width);
		float calculatedHeight = dec.getLength(term2, false, 0, 0, drawArea.height);
		
		System.out.println(String.format("customDimension calculated to be -> %s, %s", calculatedWidth, calculatedHeight));
	} else {
		throw new IllegalStateException("Cannot handle x-custom-dimension with " + customDimension.size() + " terms");
	}
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant