/*! * iOS SDK * * Tencent is pleased to support the open source community by making * Hippy available. * * Copyright (C) 2019 THL A29 Limited, a Tencent company. * All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #import "HippyCSSModel.h" #import "HippyCSSPropsDefine.h" #import "HippyLog.h" #import "HippyUtils.h" typedef NSDictionary *> HippyStyleEnumMap; // CSS JSON Key Define NSString *const HippyCSSKeyComputedStyle = @"computedStyle"; NSString *const HippyCSSKeyInlineStyleKey = @"inlineStyle"; NSString *const HippyCSSKeyCSSProperties = @"cssProperties"; NSString *const HippyCSSKeyShorthandEntries = @"shorthandEntries"; NSString *const HippyCSSKeyCSSText = @"cssText"; NSString *const HippyCSSKeyRange = @"range"; NSString *const HippyCSSKeyName = @"name"; NSString *const HippyCSSKeyValue = @"value"; NSString *const HippyCSSKeyImportant = @"important"; NSString *const HippyCSSKeyImplicit = @"implicit"; NSString *const HippyCSSKeyText = @"text"; NSString *const HippyCSSKeyParsedOK = @"parsedOk"; NSString *const HippyCSSKeyDisabled = @"disabled"; NSString *const HippyCSSKeyStartLine = @"startLine"; NSString *const HippyCSSKeyStartColumn = @"startColumn"; NSString *const HippyCSSKeyEndLine = @"endLine"; NSString *const HippyCSSKeyEndColumn = @"endColumn"; NSString *const HippyCSSKeyStyleSheetId = @"styleSheetId"; // Default value NSString *const HippyCSSDefaultLength = @"0"; NSString *const HippyCSSDefaultDisplay = @"block"; NSString *const HippyCSSDefaultPosition = @"relative"; @interface HippyCSSModel () @property (nonatomic, strong) HippyStyleEnumMap *styleEnumMap; @property (nonatomic, strong) NSSet *styleNumberSet; @property (nonatomic, strong) NSSet *styleColorSet; @property (nonatomic, strong) NSDictionary *boxModelRequireMap; @end @implementation HippyCSSModel #pragma mark - initialize method - (instancetype)init { self = [super init]; if (self) { [self initializeStyleEnumMap]; [self initializeStyleNumberSet]; [self initializeColorSet]; [self initializeBoxModelRequireMap]; } return self; } #pragma mark - CSS Protocol - (BOOL)matchedStyleJSONWithNode:(HippyVirtualNode *)node completion:(void (^)(NSDictionary *rspObject))completion { if (!completion) { return NO; } if (!node || node.props.count <= 0) { HippyLogWarn(@"CSS Model, matched style, node is nil or props is empty"); completion(@{}); return NO; } NSDictionary *cssStyleDic = [self assemblyCSSStyleJSONWithProps:node.props nodeId:node.hippyTag]; completion(@{HippyCSSKeyInlineStyleKey: cssStyleDic}); return YES; } - (BOOL)computedStyleJSONWithNode:(HippyVirtualNode *)node completion:(void (^)(NSDictionary *rspObject))completion { if (!completion) { return NO; } if (!node || node.props.count <= 0) { HippyLogWarn(@"CSS Model, computed style, node is nil or props is empty"); return NO; } NSArray *computedStyles = [self assemblyComputedStyleJSONWithProps:node.props width:@(node.frame.size.width) height:@(node.frame.size.height)]; completion(@{HippyCSSKeyComputedStyle: computedStyles}); return NO; } - (BOOL)inlineStyleJSONWithNode:(HippyVirtualNode *)node completion:(void (^)(NSDictionary *respObject))completion{ if (completion) { completion(@{}); } return YES; } - (BOOL)styleTextJSONWithUIManager:(HippyUIManager *)manager editDic:(NSDictionary *)editDic completion:(void (^)(NSDictionary *rspObject))completion { if (!completion) { return NO; } if (!manager || editDic.count <= 0) { HippyLogWarn(@"CSS Model, style text json, manager is nil or editDic is empty"); return NO; } dispatch_async(dispatch_get_main_queue(), ^{ NSNumber *nodeId = editDic[HippyCSSKeyStyleSheetId]; HippyVirtualNode *node = [manager nodeForHippyTag:nodeId]; if (!node) { HippyLogWarn(@"CSS Model, style text json, node is nil"); completion(@{}); } NSMutableDictionary *newProps = [NSMutableDictionary dictionary]; if (node.props.count > 0) { newProps = [NSMutableDictionary dictionaryWithDictionary:node.props]; } NSString *editText = editDic[HippyCSSKeyText]; NSArray *editTexts = [editText componentsSeparatedByString:@";"]; for (NSString *propText in editTexts) { NSArray *propTextArray = [propText componentsSeparatedByString:@":"]; if (propTextArray.count != 2) { continue; } NSString *propKey = [self camelize:[propTextArray.firstObject stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]]; NSString *propValue = [propTextArray.lastObject stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; if ([self.styleNumberSet containsObject:propKey]) { newProps[propKey] = @(propValue.doubleValue); } if ([self.styleColorSet containsObject:propKey]) { newProps[propKey] = @(propValue.integerValue); } if ([self.styleEnumMap.allKeys containsObject:propKey]) { NSArray *enumValues = self.styleEnumMap[propKey]; if ([enumValues containsObject:propValue]) { newProps[propKey] = propValue; } else { newProps[propKey] = enumValues.firstObject; } } } NSDictionary *resultProps = [newProps copy]; dispatch_async(HippyGetUIManagerQueue(), ^{ [manager updateViewWithHippyTag:nodeId props:resultProps]; }); NSDictionary *ret = [self assemblyCSSStyleJSONWithProps:resultProps nodeId:nodeId]; completion(ret); }); return YES; } #pragma mark - private method - (NSDictionary *)assemblyCSSStyleJSONWithProps:(NSDictionary *)props nodeId:(NSNumber *)nodeId { if (props.count <= 0) { return @{}; } NSMutableArray *cssProps = [NSMutableArray array]; NSMutableString *allOfCSSText = [NSMutableString string]; for (NSString *propKey in props) { if (![self canHandleStyle:propKey]) { continue; } NSString *cssName = [self unCamelize:propKey]; NSString *cssValue = [NSString stringWithFormat:@"%@", props[propKey]]; NSString *cssPropStr = [NSString stringWithFormat:@"%@:%@", cssName, cssValue]; NSDictionary *rangeDic = [self assemblyRangeJSONWithStartLine:0 startColumn:allOfCSSText.length endLine:0 endColumn:allOfCSSText.length + cssPropStr.length + 1]; NSDictionary *cssPropDic = [self assemblyCSSPropertyWithName:cssName value:cssValue rangeJSON:rangeDic]; [cssProps addObject:cssPropDic]; [allOfCSSText appendFormat:@"%@;", cssPropStr]; } NSMutableDictionary *resultDictionary = [NSMutableDictionary dictionary]; resultDictionary[HippyCSSKeyStyleSheetId] = nodeId; resultDictionary[HippyCSSKeyCSSProperties] = cssProps; resultDictionary[HippyCSSKeyShorthandEntries] = @{}; resultDictionary[HippyCSSKeyCSSText] = allOfCSSText; resultDictionary[HippyCSSKeyRange] = [self assemblyRangeJSONWithStartLine:0 startColumn:0 endLine:0 endColumn:allOfCSSText.length]; return resultDictionary; } - (NSArray *)assemblyComputedStyleJSONWithProps:(NSDictionary *)props width:(NSNumber *)width height:(NSNumber *)height { if (props.count <= 0) { return @[]; } NSMutableArray *computedStyles = [NSMutableArray array]; for (NSString *propKey in props) { if (![self canHandleStyle:propKey] || [propKey isEqualToString:HippyDevtoolsCSSPropWidth] || [propKey isEqualToString:HippyDevtoolsCSSPropHeight]) { continue; } NSString *cssName = [self unCamelize:propKey]; NSString *cssValue = [NSString stringWithFormat:@"%@", props[propKey]]; NSDictionary *cssProp = [self assemblyStylePropertyWithName:cssName value:cssValue]; [computedStyles addObject:cssProp]; } for (NSString *boxModelKey in self.boxModelRequireMap) { if ([props.allKeys containsObject:boxModelKey]) { continue; } NSString *boxModelName = [self unCamelize:boxModelKey]; NSString *boxModelValue = [NSString stringWithFormat:@"%@", self.boxModelRequireMap[boxModelKey]]; NSDictionary *boxModelDic = [self assemblyStylePropertyWithName:boxModelName value:boxModelValue]; [computedStyles addObject:boxModelDic]; } [computedStyles addObject:[self assemblyStylePropertyWithName:[self unCamelize:HippyDevtoolsCSSPropWidth] value:[width stringValue]]]; [computedStyles addObject:[self assemblyStylePropertyWithName:[self unCamelize:HippyDevtoolsCSSPropHeight] value:[height stringValue]]]; return [computedStyles copy]; } - (NSDictionary *)assemblyRangeJSONWithStartLine:(NSInteger)startLine startColumn:(NSInteger)startColumn endLine:(NSInteger)endLine endColumn:(NSInteger)endColumn { return @{ HippyCSSKeyStartLine : @(startLine), HippyCSSKeyStartColumn : @(startColumn), HippyCSSKeyEndLine : @(endLine), HippyCSSKeyEndColumn : @(endColumn) }; } - (NSDictionary *)assemblyCSSPropertyWithName:(NSString *)name value:(NSString *)value rangeJSON:(NSDictionary *)rangeJSON { return @{ HippyCSSKeyName : name != nil ? name : @"", HippyCSSKeyValue : value != nil ? value : @"", HippyCSSKeyImportant : @(NO), HippyCSSKeyImplicit : @(NO), HippyCSSKeyText : @"", HippyCSSKeyParsedOK : @(YES), HippyCSSKeyDisabled : @(NO), HippyCSSKeyRange : rangeJSON != nil ? rangeJSON : @{} }; } - (NSDictionary *)assemblyStylePropertyWithName:(NSString *)name value:(NSString *)value { return @{ HippyCSSKeyName : name != nil ? name : @"", HippyCSSKeyValue : value != nil ? value : @"" }; } /** * a-b to aB */ - (NSString *)camelize:(NSString *)originStr { if (originStr.length <= 0) { HippyLogInfo(@"CSS Model, camelize, origin string is empty"); return @""; } NSArray *splitStrings = [originStr componentsSeparatedByString:@"-"]; NSMutableString *resultString = [NSMutableString stringWithString:@""]; BOOL isHeadString = true; for (NSString *tempStr in splitStrings) { if (isHeadString) { [resultString appendString:tempStr]; isHeadString = false; continue; } [resultString appendString:[tempStr capitalizedString]]; } return resultString; } /** * aB to a-b */ - (NSString *)unCamelize:(NSString *)originStr { if (originStr.length <= 0) { HippyLogInfo(@"CSS Model, unCamelize, origin string is empty"); return @""; } NSMutableString *resultString = [NSMutableString stringWithString:@""]; NSString *regularString = @"[A-Z]"; NSString *tempOriginStr = [originStr copy]; NSRange range = [tempOriginStr rangeOfString:regularString options:NSRegularExpressionSearch]; while (range.location != NSNotFound) { if (range.location != 0) { [resultString appendFormat:@"%@-", [[tempOriginStr substringToIndex:range.location] lowercaseString]]; tempOriginStr = [tempOriginStr substringFromIndex:range.location]; } tempOriginStr = [tempOriginStr stringByReplacingCharactersInRange:NSMakeRange(0, 1) withString:[[tempOriginStr substringToIndex:1] lowercaseString]]; range = [tempOriginStr rangeOfString:regularString options:NSRegularExpressionSearch]; } [resultString appendString:tempOriginStr]; return resultString; } - (BOOL)canHandleStyle:(NSString *)obj { return [self.styleNumberSet containsObject:obj] || [self.styleEnumMap.allKeys containsObject:obj] || [self.styleColorSet containsObject:obj]; } - (void)initializeStyleNumberSet { self.styleNumberSet = [NSSet setWithArray:@[ HippyDevtoolsCSSPropFlex, HippyDevtoolsCSSPropFlexGrow, HippyDevtoolsCSSPropFlexShrink, HippyDevtoolsCSSPropFlexBasis, HippyDevtoolsCSSPropWidth, HippyDevtoolsCSSPropHeight, HippyDevtoolsCSSPropMaxWidth, HippyDevtoolsCSSPropMinWidth, HippyDevtoolsCSSPropMaxHeight, HippyDevtoolsCSSPropMinHeight, HippyDevtoolsCSSPropMarginTop, HippyDevtoolsCSSPropMarginLeft, HippyDevtoolsCSSPropMarginRight, HippyDevtoolsCSSPropMarginBottom, HippyDevtoolsCSSPropPaddingTop, HippyDevtoolsCSSPropPaddingLeft, HippyDevtoolsCSSPropPaddingRight, HippyDevtoolsCSSPropPaddingBottom, HippyDevtoolsCSSPropBorderWidth, HippyDevtoolsCSSPropBorderTopWidth, HippyDevtoolsCSSPropBorderLeftWidth, HippyDevtoolsCSSPropBorderRightWidth, HippyDevtoolsCSSPropBorderBottomWidth, HippyDevtoolsCSSPropBorderRadius, HippyDevtoolsCSSPropBorderTopLeftRadius, HippyDevtoolsCSSPropBorderTopRightRadius, HippyDevtoolsCSSPropBorderBottomLeftRadius, HippyDevtoolsCSSPropBorderBottomRightRadius, HippyDevtoolsCSSPropTop, HippyDevtoolsCSSPropLeft, HippyDevtoolsCSSPropRight, HippyDevtoolsCSSPropBottom, HippyDevtoolsCSSPropZIndex, HippyDevtoolsCSSPropOpacity, HippyDevtoolsCSSPropFontSize, HippyDevtoolsCSSPropLineHeight ]]; } - (void)initializeStyleEnumMap { self.styleEnumMap = @{ HippyDevtoolsCSSPropDisplay : @[HippyDevtoolsCSSPropValueDisplayFlex, HippyDevtoolsCSSPropValueDisplayNone], HippyDevtoolsCSSPropFlexDirection : @[HippyDevtoolsCSSPropValueColumn, HippyDevtoolsCSSPropValueColumnReverse, HippyDevtoolsCSSPropValueRow, HippyDevtoolsCSSPropValueRowReverse], HippyDevtoolsCSSPropFlexWrap : @[HippyDevtoolsCSSPropValueNowrap, HippyDevtoolsCSSPropValueWrap, HippyDevtoolsCSSPropValueWrapReverse], HippyDevtoolsCSSPropAlignItems : @[HippyDevtoolsCSSPropValueFlexStart, HippyDevtoolsCSSPropCenter, HippyDevtoolsCSSPropValueFlexEnd, HippyDevtoolsCSSPropValueStretch, HippyDevtoolsCSSPropValueBaseline], HippyDevtoolsCSSPropAlignSelf : @[HippyDevtoolsCSSPropValueAuto, HippyDevtoolsCSSPropValueFlexStart, HippyDevtoolsCSSPropCenter, HippyDevtoolsCSSPropValueFlexEnd, HippyDevtoolsCSSPropValueStretch, HippyDevtoolsCSSPropValueBaseline], HippyDevtoolsCSSPropJustifyContent : @[HippyDevtoolsCSSPropValueFlexStart, HippyDevtoolsCSSPropCenter, HippyDevtoolsCSSPropValueFlexEnd, HippyDevtoolsCSSPropValueSpaceBetween, HippyDevtoolsCSSPropValueSpaceAround, HippyDevtoolsCSSPropValueSpaceEvenly], HippyDevtoolsCSSPropOverFlow : @[HippyDevtoolsCSSPropValueHidden, HippyDevtoolsCSSPropValueVisible, HippyDevtoolsCSSPropValueScroll], HippyDevtoolsCSSPropPosition : @[HippyDevtoolsCSSPropValueRelative, HippyDevtoolsCSSPropValueAbsolute], HippyDevtoolsCSSPropBackgroundSize : @[HippyDevtoolsCSSPropValueAuto, HippyDevtoolsCSSPropValueContain, HippyDevtoolsCSSPropValueCover, HippyDevtoolsCSSPropValueBackgroundSizeFit], HippyDevtoolsCSSPropBackgroundPositionX : @[HippyDevtoolsCSSPropLeft, HippyDevtoolsCSSPropCenter, HippyDevtoolsCSSPropRight], HippyDevtoolsCSSPropBackgroundPositionY : @[HippyDevtoolsCSSPropTop, HippyDevtoolsCSSPropCenter, HippyDevtoolsCSSPropBottom], HippyDevtoolsCSSPropFontStyle : @[HippyDevtoolsCSSPropValueNormal, HippyDevtoolsCSSPropValueItalic], HippyDevtoolsCSSPropFontWeight : @[HippyDevtoolsCSSPropValueNormal, HippyDevtoolsCSSPropValueBold, HippyDevtoolsCSSPropValueFontWeight100, HippyDevtoolsCSSPropValueFontWeight200, HippyDevtoolsCSSPropValueFontWeight300, HippyDevtoolsCSSPropValueFontWeight400, HippyDevtoolsCSSPropValueFontWeight500, HippyDevtoolsCSSPropValueFontWeight600, HippyDevtoolsCSSPropValueFontWeight700, HippyDevtoolsCSSPropValueFontWeight800, HippyDevtoolsCSSPropValueFontWeight900], HippyDevtoolsCSSPropTextAlign : @[HippyDevtoolsCSSPropLeft, HippyDevtoolsCSSPropCenter, HippyDevtoolsCSSPropRight], HippyDevtoolsCSSPropResizeMode : @[HippyDevtoolsCSSPropValueCover, HippyDevtoolsCSSPropValueContain, HippyDevtoolsCSSPropValueStretch, HippyDevtoolsCSSPropValueRepeat, HippyDevtoolsCSSPropCenter] }; } - (void)initializeColorSet { self.styleColorSet = [NSSet setWithArray:@[ HippyDevtoolsCSSPropBackgroundColor, HippyDevtoolsCSSPropBorderColor, HippyDevtoolsCSSPropBorderLeftColor, HippyDevtoolsCSSPropBorderTopColor, HippyDevtoolsCSSPropBorderRightColor, HippyDevtoolsCSSPropBorderBottomColor, HippyDevtoolsCSSPropShadowColor, HippyDevtoolsCSSPropColor, HippyDevtoolsCSSPropTextShadowColor, HippyDevtoolsCSSPropTextDecorationColor ]]; } - (void)initializeBoxModelRequireMap { self.boxModelRequireMap = @{ HippyDevtoolsCSSPropPaddingTop : HippyCSSDefaultLength, HippyDevtoolsCSSPropPaddingLeft : HippyCSSDefaultLength, HippyDevtoolsCSSPropPaddingRight : HippyCSSDefaultLength, HippyDevtoolsCSSPropPaddingBottom : HippyCSSDefaultLength, HippyDevtoolsCSSPropBorderLeftWidth : HippyCSSDefaultLength, HippyDevtoolsCSSPropBorderRightWidth : HippyCSSDefaultLength, HippyDevtoolsCSSPropBorderTopWidth : HippyCSSDefaultLength, HippyDevtoolsCSSPropBorderBottomWidth : HippyCSSDefaultLength, HippyDevtoolsCSSPropMarginTop : HippyCSSDefaultLength, HippyDevtoolsCSSPropMarginLeft : HippyCSSDefaultLength, HippyDevtoolsCSSPropMarginRight : HippyCSSDefaultLength, HippyDevtoolsCSSPropMarginBottom : HippyCSSDefaultLength, HippyDevtoolsCSSPropDisplay : HippyCSSDefaultDisplay, HippyDevtoolsCSSPropPosition : HippyCSSDefaultPosition }; } @end