Skip to content

Commit

Permalink
Add more validations for mobile. (#32)
Browse files Browse the repository at this point in the history
* Add CSS validations for mobile.

* Add comment headers.
  • Loading branch information
sfdctaka authored Jul 9, 2021
1 parent 3a1fb46 commit 909e267
Show file tree
Hide file tree
Showing 23 changed files with 7,851 additions and 5,502 deletions.
12 changes: 12 additions & 0 deletions core/src/main/java/com/salesforce/slds/shared/RegexPattern.java
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,18 @@ public class RegexPattern {
public static final String NUMERIC_PATTERN = "[-|+]?" + NUMBER_FRAGMENT + "\\s*[a-zA-Z]+" + "|" +
PERCENT_PATTERN + "|" + "[-|+]?" + NUMBER_FRAGMENT;

public static final String FONT_STYLE_PATTERN = "(?=(?:(?:[-a-z]+\\s*){0,2}(italic|oblique))?)";

public static final String FONT_VARIANT_PATTERN = "(?=(?:(?:[-a-z]+\\s*){0,2}(small-caps))?)";

public static final String FONT_WEIGHT_PATTERN = "(?=(?:(?:[-a-z]+\\s*){0,2}(bold(?:er)?|lighter|[1-9]00))?)(?:(?:normal|\\1|\\2|\\3)\\s*){0,3}";

public static final String FONT_SIZE_PATTERN = "((?:xx?-)?(?:small\\s|large)|medium|smaller|larger|(?<value>[.\\d]+)(?<unit>(?:\\%|in|[cem]m|ex|p[ctx])))";

public static final String LINE_HEIGHT_PATTERN = "(?:\\s*\\/\\s*(normal|[.\\d]+(?:\\%|in|[cem]m|ex|p[ctx])))?";

public static final String FONT_FAMILY_PATTERN = "([-,\"\\sa-zA-Z]*)";

public static final String AURA_TOKEN_FUNCTION = "t(?:oken)?\\((?<token>[\\w\\d]+)\\)";
public static final String VAR_FUNCTION = "var\\(\\s*--lwc-(?<token>[\\w\\d-]+)\\s*(,\\s*(?<fallback>"+
COLOR_PATTERN + "|" + NUMERIC_PATTERN + "|" + WORD_FRAGMENT + ")\\s*)?\\)";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,22 +68,61 @@ protected Set<String> generateNumbers(String sign, String number, String unit) {
private Set<String> generateNumbers(Optional<String> sign, String number, Optional<String> unit) {
Set<String> results = new LinkedHashSet<>();

if (unit.isPresent() && unit.get().contentEquals("px")) {
Double num = Double.parseDouble(number) / 16.0;
// Using size conversion from this website:
// https://websemantics.uk/tools/font-size-conversion-pixel-point-em-rem-percent/
if (unit.isPresent()) {
String unitContent = unit.get();
DecimalFormat df = new DecimalFormat();
df.setMaximumFractionDigits(3);
df.setMinimumFractionDigits(0);
Double parsedNumber = Double.parseDouble(number);

results.add(generateString(sign, df.format(num), Optional.of("rem")));
}
if (unitContent.contentEquals("px")) {
Double num = parsedNumber / 16.0;
results.add(generateString(sign, df.format(num), Optional.of("rem")));
results.add(generateString(sign, df.format(num), Optional.of("em")));

if (unit.isPresent() && unit.get().contentEquals("rem")) {
Double num = Double.parseDouble(number) * 16;
DecimalFormat df = new DecimalFormat();
df.setMaximumFractionDigits(3);
df.setMinimumFractionDigits(0);
num = num * 100;
results.add(generateString(sign, df.format(num), Optional.of("%")));

results.add(generateString(sign, df.format(num), Optional.of("px")));
num = parsedNumber * 0.75;
results.add(generateString(sign, df.format(num), Optional.of("pt")));
}

if (unitContent.contentEquals("rem") || unit.get().contentEquals("em")) {
Double num = parsedNumber * 16;
results.add(generateString(sign, df.format(num), Optional.of("px")));

num = num * 0.75;
results.add(generateString(sign, df.format(num), Optional.of("pt")));

num = parsedNumber * 100;
results.add(generateString(sign, df.format(num), Optional.of("%")));
}

if (unitContent.contentEquals("pt")) {
Double num = parsedNumber / 0.75;
results.add(generateString(sign, df.format(num), Optional.of("px")));

num = num * 0.75;
results.add(generateString(sign, df.format(num), Optional.of("pt")));

num = parsedNumber * 100;
results.add(generateString(sign, df.format(num), Optional.of("%")));
}

if (unitContent.contentEquals("%")) {
Double num = parsedNumber / 100;
results.add(generateString(sign, df.format(num), Optional.of("rem")));
results.add(generateString(sign, df.format(num), Optional.of("em")));

Double numInRem = num;
num = numInRem * 16;
results.add(generateString(sign, df.format(num), Optional.of("px")));

num = numInRem * 0.75;
results.add(generateString(sign, df.format(num), Optional.of("pt")));
}
}

results.add(generateString(sign, number, unit));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
* Copyright (c) 2021, salesforce.com, inc.
* All rights reserved.
* SPDX-License-Identifier: BSD-3-Clause
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/

package com.salesforce.slds.shared.utils;

import com.salesforce.slds.shared.RegexPattern;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class CssFontShortHandUtilities {
private final String FONT_SHORTHAND_PATTERN = "^\\s*" +
RegexPattern.FONT_STYLE_PATTERN +
RegexPattern.FONT_VARIANT_PATTERN +
RegexPattern.FONT_WEIGHT_PATTERN +
RegexPattern.FONT_SIZE_PATTERN +
RegexPattern.LINE_HEIGHT_PATTERN +
"\\s*" +
RegexPattern.FONT_FAMILY_PATTERN +
"\\s*";

private String fontStyle;
private String fontVariant;
private String fontWeight;
private String fontSize;
private String lineHeight;
private String fontFamily;

public CssFontShortHandUtilities(String fontShorthandValue) {
Pattern pattern = Pattern.compile(FONT_SHORTHAND_PATTERN);
Matcher matcher = pattern.matcher(fontShorthandValue);
if (matcher.find()) {
fontStyle = matcher.group(1);
fontVariant = matcher.group(2);
fontWeight = matcher.group(3);
fontSize = matcher.group(4);
lineHeight = matcher.group(7);
fontFamily = matcher.group(8);
}
}

public String getFontStyle() {
return fontStyle;
}

public String getFontVariant() {
return fontVariant;
}

public String getFontWeight() {
return fontWeight;
}

public String getFontSize() {
return fontSize;
}

public String getLineHeight() { return lineHeight; }

public String getFontFamily() {
return fontFamily;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@
UtilityClassValidator.class, ValidatorFactories.class,
ValidateRunner.class, ComponentOverrideValidator.class,
HTMLElementUtilities.class, DesignTokenValidator.class,
MobileFriendlyValidator.class
MobileSLDS_MarkupFriendlyValidator.class,
MobileSLDS_MarkupLabelValidator.class,
MobileSLDS_CSSValidator.class
})
public class ValidationConfiguration {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
/*
* Copyright (c) 2021, salesforce.com, inc.
* All rights reserved.
* SPDX-License-Identifier: BSD-3-Clause
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/

package com.salesforce.slds.validation.validators.impl.recommendation;

import com.google.common.collect.ImmutableList;
import com.salesforce.slds.shared.RegexPattern;
import com.salesforce.slds.shared.converters.Converter;
import com.salesforce.slds.shared.converters.TypeConverters;
import com.salesforce.slds.shared.converters.tokens.VarTokenType;
import com.salesforce.slds.shared.models.context.Context;
import com.salesforce.slds.shared.models.core.*;
import com.salesforce.slds.shared.models.locations.Range;
import com.salesforce.slds.shared.models.recommendation.Action;
import com.salesforce.slds.shared.models.recommendation.ActionType;
import com.salesforce.slds.shared.models.recommendation.Item;
import com.salesforce.slds.shared.models.recommendation.Recommendation;
import com.salesforce.slds.shared.utils.CssFontShortHandUtilities;
import com.salesforce.slds.tokens.models.DesignToken;
import com.salesforce.slds.tokens.registry.TokenRegistry;
import com.salesforce.slds.validation.utils.CSSValidationUtilities;
import com.salesforce.slds.validation.validators.interfaces.RecommendationValidator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.*;
import java.util.concurrent.atomic.AtomicReference;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

@Component
public class MobileSLDS_CSSValidator implements RecommendationValidator {
public static final String USE_FONT_SIZE_4_OR_LARGER = "For best readability on mobile devices, consider using fontSize4 or larger.";
public static final String USE_FONT_SIZE_14PX_OR_LARGER = "For best readability on mobile devices, consider using 14px or larger.";
public static final String AVOID_TRUNCATION = "On a mobile device, a long label can exceed the screen width if it's prevented from wrapping.";

private final String ACTION_NAME = "Mobile SLDS CSS";
private final String FONT_SIZE_SLDS_PREFIX = "fontSize";
private final String PX = "px";

@Autowired
CSSValidationUtilities cssValidationUtilities;

@Autowired
TokenRegistry tokenRegistry;

@Override
public List<Recommendation> matches(Entry entry, Bundle bundle, Context context) {
List<Recommendation> recommendations = new ArrayList<>();

List<Input> inputs = entry.getInputs().stream()
.filter(input -> input.getType() == Input.Type.STYLE).collect(Collectors.toList());

for (Input input : inputs) {
List<Style> styles = input.asRuleSet().getStylesWithAnnotationType();
recommendations.addAll(styles.stream()
.filter(Style::validate)
.map(style -> process(style, context, entry.getEntityType(), entry.getRawContent()))
.filter(Objects::nonNull)
.collect(Collectors.toList()));
}

return recommendations;
}

/**
* Process CSS RuleSet
* @param style
* @param rawContents
* @return Recommendation
*/
private Recommendation process(Style style, Context context, Entry.EntityType entityType, List<String> rawContents) {
Set<Item> items = provideRecommendations(style, context, entityType, rawContents);
if (items.isEmpty() == false) {
Recommendation.RecommendationBuilder builder = Recommendation.builder();
builder.input(style).items(items);
return builder.build();
}
return null;
}

private boolean isWhiteSpaceNowrap(Style style) {
if (style.getProperty().equals("white-space") &&
style.getValue().equals("nowrap")) {
return true;
}
return false;
}

private boolean isTextOverflowEllipsis(Style style) {
if (style.getProperty().equals("text-overflow") &&
style.getValue().equals("ellipsis")) {
return true;
}
return false;
}

/**
* Provide recommendations
* @param style
* @param rawContents
* @return
*/
private Set<Item> provideRecommendations(Style style, Context context, Entry.EntityType entityType, List<String> rawContents) {
Set<Item> items = new LinkedHashSet<>();
Action.ActionBuilder actionBuilder = Action.builder().fileType(Input.Type.STYLE);
String styleValue = style.getValue();

// Check that SLDS token font size smaller than 14px(fontSize4) is not used.
TypeConverters converters = new TypeConverters(ImmutableList.of(new VarTokenType()));
Converter.State state = converters.process(Converter.State.builder().input(style.getValue()).build());
state.getValues().forEach((location, values) -> {
for (String value : values) {
Optional<DesignToken> designToken = tokenRegistry.getDesignToken(value);

String fontSizePattern = "fontSize(?<size>[.\\d+])";
Pattern pattern = Pattern.compile(fontSizePattern);
Matcher matcher = pattern.matcher(value);

if (matcher.find() &&
designToken.isPresent() == true &&
designToken.get().getDeprecated() == null
) {
String size = matcher.group("size");
int fontSize = Integer.parseInt(size);
if (fontSize > 0 && fontSize < 4) {
Range range = cssValidationUtilities.getValueSpecificRange(value, style, rawContents);
actionBuilder.name(ACTION_NAME)
.description(USE_FONT_SIZE_4_OR_LARGER)
.actionType(ActionType.NONE);
items.add(new Item(style.getValue(), actionBuilder.range(range).build()));
}
}
}
});

// Check that font size smaller than 14px is not used.
if (style.getProperty().equals("font-size")) {
addActionItemsForSmallFonts(styleValue, style, rawContents, actionBuilder, items);
}

if (style.getProperty().equals("font")) {
CssFontShortHandUtilities fontShortHandUtilities = new CssFontShortHandUtilities(styleValue);
String fontSize = fontShortHandUtilities.getFontSize();
if (fontSize != null) {
addActionItemsForSmallFonts(fontSize, style, rawContents, actionBuilder, items);
}
}

// Check that word wrapping is not explicitly specified.
if (isTextOverflowEllipsis(style) || isWhiteSpaceNowrap(style)) {
Range range = cssValidationUtilities.getValueSpecificRange(styleValue, style, rawContents);
actionBuilder.name(ACTION_NAME)
.description(AVOID_TRUNCATION)
.actionType(ActionType.NONE);
items.add(new Item(style.getValue(), actionBuilder.range(range).build()));
}

return items;
}

private void addActionItemsForSmallFonts(String cssValue, Style style, List<String> rawContents, Action.ActionBuilder actionBuilder, Set<Item> items) {
Range range;
String fontAbsoluteSize = "((?:xx?-)?small)";
Pattern pattern = Pattern.compile(fontAbsoluteSize);
Matcher matcher = pattern.matcher(cssValue);

if (matcher.find()) {
range = cssValidationUtilities.getValueSpecificRange(matcher.group(0), style, rawContents);
} else {
range = getRangeForSmallFonts(cssValue, style, rawContents);
}

if (!range.equals(Range.EMPTY_RANGE)) {
actionBuilder.name(ACTION_NAME)
.description(USE_FONT_SIZE_14PX_OR_LARGER)
.actionType(ActionType.NONE);
items.add(new Item(style.getValue(), actionBuilder.range(range).build()));
}
}
private Range getRangeForSmallFonts(String cssValue, Style style, List<String> rawContents) {
Pattern pattern = Pattern.compile(RegexPattern.FONT_SIZE_PATTERN);
Matcher matcher = pattern.matcher(cssValue);
Range range = Range.EMPTY_RANGE;
if (matcher.find()) {
String value = matcher.group("value");
String unit = matcher.group("unit");
if (value != null &&
!value.trim().isEmpty() &&
unit != null &&
!unit.trim().isEmpty()) {
try {
AtomicReference<Double> pxValue = new AtomicReference<>(0.0);
if (!unit.contentEquals(PX)) {
TypeConverters converters = new TypeConverters();
Converter.State state = converters.process(Converter.State.builder().input(style.getValue()).build());
state.getValues().forEach((location, valuesWithUnit) -> {
for (String valueWithUnit : valuesWithUnit) {
if (!valueWithUnit.contains(PX)) {
continue;
}
String valueWithoutUnit = valueWithUnit.substring(valueWithUnit.length(), valueWithUnit.length() - PX.length());
pxValue.set(Double.parseDouble(valueWithoutUnit));
break;
}
});
} else {
pxValue.set(Double.parseDouble(value));
}

if (pxValue.get() < 14) {
range = cssValidationUtilities.getValueSpecificRange(matcher.group("value"), style, rawContents);
}
} catch(Exception e) {
System.out.println(String.format("Failed to convert font size value: %s", e.getMessage()));
}
}
}
return range;
}
}
Loading

0 comments on commit 909e267

Please sign in to comment.