diff --git a/DivKit/Expressions/CalcExpression/CalcExpression.swift b/DivKit/Expressions/CalcExpression/CalcExpression.swift index c653073c..08c6cac4 100644 --- a/DivKit/Expressions/CalcExpression/CalcExpression.swift +++ b/DivKit/Expressions/CalcExpression/CalcExpression.swift @@ -177,6 +177,8 @@ final class CalcExpression: CustomStringConvertible { /// An array was accessed with an index outside the valid range case arrayBounds(Symbol, Double) + case escaping + /// Empty expression static let emptyExpression = unexpectedToken("") @@ -188,7 +190,7 @@ final class CalcExpression: CustomStringConvertible { case .emptyExpression: return "Empty expression" case let .unexpectedToken(string): - return "Unexpected token `\(string)`" + return "Error tokenizing '\(string)'." case let .missingDelimiter(string): return "Missing `\(string)`" case let .undefinedSymbol(symbol): @@ -215,6 +217,8 @@ final class CalcExpression: CustomStringConvertible { return "\(description.prefix(1).uppercased())\(description.dropFirst()) expects \(arity)" case let .arrayBounds(symbol, index): return "Index \(CalcExpression.stringify(index)) out of bounds for \(symbol)" + case .escaping: + return "Incorrect string escape" } } } @@ -454,10 +458,11 @@ extension CalcExpression { // comparison: -4 "&&": -5, "and": -5, // and "||": -6, "or": -6, // or - "?": -7, ":": -7, // ternary - // assignment: -8 + ":": -8, // ternary + // assignment: -9 ",": -100, ].mapValues { ($0, false) } + precedences["?"] = (-7, true) // ternary let comparisonOperators = [ "<", "<=", ">=", ">", "==", "!=", "===", "!==", @@ -471,7 +476,7 @@ extension CalcExpression { "<<=", ">>=", "&=", "^=", "|=", ":=", ] for op in assignmentOperators { - precedences[op] = (-8, true) + precedences[op] = (-9, true) } return precedences }() @@ -1148,9 +1153,14 @@ extension UnicodeScalarView { return .error(.unexpectedToken(hex), string) } string.append(Character(c)) - default: + case "'", "\\": string.append(Character(c)) + case "@" where scanCharacter("{"): + string += "@{" + default: + return .error(.escaping, string) } + part = "" } } while part != nil guard scanCharacter(delimiter) else { @@ -1215,7 +1225,8 @@ extension UnicodeScalarView { } else { stack[i...i + 2] = [.symbol(.infix(symbol.name), [lhs, rhs], nil)] } - try collapseStack(from: 0) + let from = symbol.name == "?" ? i : 0 + try collapseStack(from: from) } else if case let .symbol(symbol2, _, _) = rhs { if case .prefix = symbol2 { try collapseStack(from: i + 2) diff --git a/DivKit/Expressions/ExpressionError.swift b/DivKit/Expressions/ExpressionError.swift index 6fb5362f..8aa6f1b3 100644 --- a/DivKit/Expressions/ExpressionError.swift +++ b/DivKit/Expressions/ExpressionError.swift @@ -1,7 +1,7 @@ import Foundation public enum ExpressionError: Error { - case incorrectExpression(expression: String) + case tokenizing(expression: String) case emptyValue case incorrectSingleItemExpression(expression: String, type: Any.Type) case initializingValue( @@ -19,14 +19,15 @@ public enum ExpressionError: Error { value: String, type: Any.Type ) + case escaping case unknown(error: Error) } extension ExpressionError: CustomStringConvertible { public var description: String { switch self { - case let .incorrectExpression(expression): - return "Incorrect expression: '\(expression)'" + case let .tokenizing(expression): + return "Error tokenizing '\(expression)'." case .emptyValue: return "Empty value" case let .incorrectSingleItemExpression(expression, type): @@ -37,6 +38,8 @@ extension ExpressionError: CustomStringConvertible { return "Error on initializing value '\(stringValue)' of type \(type), expression: '\(expression)'" case let .validating(expression, value, type): return "Value '\(value)' did not pass validation for type \(type), expression: '\(expression)'" + case .escaping: + return "Incorrect string escape" case let .unknown(error): return "Unknown error: \(error)" } diff --git a/DivKit/Expressions/ExpressionLink.swift b/DivKit/Expressions/ExpressionLink.swift index 47e8558f..bcbc9ba0 100644 --- a/DivKit/Expressions/ExpressionLink.swift +++ b/DivKit/Expressions/ExpressionLink.swift @@ -20,8 +20,8 @@ public struct ExpressionLink { validator: ExpressionValueValidator?, errorTracker: ExpressionErrorTracker? = nil, resolveNested: Bool = true - ) { - self.init( + ) throws { + try self.init( rawValue: "@{\(expression)}", validator: validator, errorTracker: errorTracker, @@ -35,7 +35,7 @@ public struct ExpressionLink { validator: ExpressionValueValidator?, errorTracker: ExpressionErrorTracker? = nil, resolveNested: Bool = true - ) { + ) throws { guard !rawValue.isEmpty else { errorTracker?(.emptyValue) return nil @@ -50,8 +50,8 @@ public struct ExpressionLink { let currentValue = rawValue[index.. { items.append(.string("")) } else { let value = String(currentValue[start...end]) - if resolveNested, let link = ExpressionLink( + if resolveNested, let link = try ExpressionLink( rawValue: value, validator: nil, errorTracker: errorTracker @@ -78,6 +78,9 @@ public struct ExpressionLink { currentString = currentString + String(currentValue[index]) index = currentValue.index(after: index) if index == endIndex { + if currentString == rawValue { + return nil + } items.append(.string(currentString)) } } @@ -114,17 +117,19 @@ extension StringProtocol { return i } } - if char == "'" { - if i == startIndex || self[index(before: i)] != "\\" { - stringStarted[nestedExpressionCounter] = !stringStarted[nestedExpressionCounter]! - } + if char == "'" && notEscaped(at: i) { + stringStarted[nestedExpressionCounter] = !stringStarted[nestedExpressionCounter]! } } return nil } fileprivate func hasExpression(at i: String.Index) -> Bool { - self[i...].hasPrefix(expressionPrefix) && (i == startIndex || self[index(before: i)] != "\\") + self[i...].hasPrefix(expressionPrefix) && notEscaped(at: i) + } + + fileprivate func notEscaped(at i: String.Index) -> Bool { + i == startIndex || self[index(before: i)] != "\\" || !notEscaped(at: index(before: i)) } } diff --git a/DivKit/Expressions/ExpressionResolver.swift b/DivKit/Expressions/ExpressionResolver.swift index 9318225d..2a12797a 100644 --- a/DivKit/Expressions/ExpressionResolver.swift +++ b/DivKit/Expressions/ExpressionResolver.swift @@ -52,14 +52,13 @@ public final class ExpressionResolver { ) ?? T(rawValue: expression) } - @inlinable func resolveStringBasedValue( expression: Expression?, initializer: (String) -> T? ) -> T? { switch expression { case let .value(val): - return val + return resolveEscaping(val) case let .link(link): return evaluateStringBasedValue( link: link, @@ -70,6 +69,39 @@ public final class ExpressionResolver { } } + func resolveEscaping(_ value: T?) -> T? { + guard let value = value as? String else { + return value + } + var result = "" + + var index = value.startIndex + let escapingValues = ["@{", "'", "\\"] + + while (index < value.endIndex) { + if value[index] == "\\" { + index = value.index(index, offsetBy: 1) + let next = value[index...] + if let escaped = escapingValues.first(where: { next.starts(with: $0) }) { + result += escaped + index = value.index(index, offsetBy: escaped.count) + } else { + if next.isEmpty { + errorTracker(ExpressionError.tokenizing(expression: value)) + } else { + errorTracker(ExpressionError.escaping) + } + return nil + } + } else { + result += value[index] + index = value.index(after: index) + } + } + + return result as? T + } + @inlinable func resolveNumericValue( expression: Expression? @@ -146,13 +178,13 @@ public final class ExpressionResolver { return nil } case let .string(value): - stringValue += value.replacingOccurrences(of: "\\@{", with: "@{") + stringValue += value case let .nestedCalcExpression(link): if let expression = evaluateStringBasedValue( link: link, initializer: { $0 } ) { - let link = ExpressionLink( + let link = try? ExpressionLink( expression: expression, validator: nil, errorTracker: link.errorTracker, @@ -181,7 +213,6 @@ public final class ExpressionResolver { @inlinable func getVariableValue(_ name: String) -> T? { guard let value: T = variables[DivVariableName(rawValue: name)]?.typedValue() else { - DivKitLogger.error("No variable: \(name)") return nil } return value @@ -207,7 +238,7 @@ public final class ExpressionResolver { expression: String, initializer: (String) -> T? ) -> T? { - guard let expressionLink = ExpressionLink(rawValue: expression, validator: nil) else { + guard let expressionLink = try? ExpressionLink(rawValue: expression, validator: nil) else { return nil } return resolveStringBasedValue( diff --git a/DivKit/Expressions/Serialization/Expression+Helpers.swift b/DivKit/Expressions/Serialization/Expression+Helpers.swift index 0a57e9bb..04ddc469 100644 --- a/DivKit/Expressions/Serialization/Expression+Helpers.swift +++ b/DivKit/Expressions/Serialization/Expression+Helpers.swift @@ -9,9 +9,17 @@ func expressionTransform( transform: (U) -> T?, validator: ExpressionValueValidator? = nil ) -> Expression? { - if let rawValue = value as? String, - let resolver = ExpressionLink(rawValue: rawValue, validator: validator) { - return .link(resolver) + do { + if let rawValue = value as? String, + let resolver = try ExpressionLink( + rawValue: rawValue, + validator: validator, + errorTracker: { DivKitLogger.error($0.description) } + ) { + return .link(resolver) + } + } catch { + return nil } guard let value = value else { diff --git a/DivKitExtensions/DivExtensions.swift b/DivKitExtensions/DivExtensions.swift index f901be08..4d94e842 100644 --- a/DivKitExtensions/DivExtensions.swift +++ b/DivKitExtensions/DivExtensions.swift @@ -6,6 +6,9 @@ extension Div { public func makeImageURLs(with expressionResolver: ExpressionResolver) -> [URL] { var urls: [URL] = value.background?.compactMap { $0.makeImageURL(with: expressionResolver) } ?? [] + if let url = LottieExtensionHandler.getPreloadURL(div: value) { + urls.append(url) + } switch self { case let .divImage(divImage): if let url = divImage.resolveImageUrl(expressionResolver) { diff --git a/DivKitExtensions/Lottie/AnimationBlock.swift b/DivKitExtensions/Lottie/AnimationBlock.swift index da9b876d..63bd5c9c 100644 --- a/DivKitExtensions/Lottie/AnimationBlock.swift +++ b/DivKitExtensions/Lottie/AnimationBlock.swift @@ -7,12 +7,12 @@ import LayoutKit final class AnimationBlock: SizeForwardingBlock { let animatableView: Lazy + let animationHolder: AnimationHolder + let sizeProvider: Block - var sizeProvider: Block var debugDescription: String { return "Animation Block playing animation with view: \(animatableView)" } - let holder: AnimationHolder init( animatableView: Lazy, @@ -20,7 +20,7 @@ final class AnimationBlock: SizeForwardingBlock { sizeProvider: Block ) { self.animatableView = animatableView - self.holder = animationHolder + self.animationHolder = animationHolder self.sizeProvider = sizeProvider } diff --git a/DivKitExtensions/Lottie/AnimationBlockView+UIViewRenderableBlock.swift b/DivKitExtensions/Lottie/AnimationBlockView+UIViewRenderableBlock.swift index 15cca230..c64efd91 100644 --- a/DivKitExtensions/Lottie/AnimationBlockView+UIViewRenderableBlock.swift +++ b/DivKitExtensions/Lottie/AnimationBlockView+UIViewRenderableBlock.swift @@ -20,9 +20,9 @@ extension AnimationBlock { renderingDelegate _: RenderingDelegate? ) { let lottieView = view as! AnimationBlockView - if lottieView.animationHolder !== holder { + if lottieView.animationHolder !== animationHolder { lottieView.animatableView = animatableView.value - lottieView.animationHolder = holder + lottieView.animationHolder = animationHolder } } } diff --git a/DivKitExtensions/Lottie/LottieExtensionHandler.swift b/DivKitExtensions/Lottie/LottieExtensionHandler.swift index ae967327..77e67fc8 100644 --- a/DivKitExtensions/Lottie/LottieExtensionHandler.swift +++ b/DivKitExtensions/Lottie/LottieExtensionHandler.swift @@ -57,6 +57,13 @@ public final class LottieExtensionHandler: DivExtensionHandler { sizeProvider: block ) } + + static func getPreloadURL(div: DivBase) -> URL? { + let extensionData = div.extensions?.first { $0.id == "lottie" } + guard let paramsDict = extensionData?.params, + let params = LottieExtensionParams(params: paramsDict) else { return nil } + return params.source.url + } } private class JSONAnimationHolder: AnimationHolder { @@ -133,3 +140,12 @@ private struct LottieExtensionParams { } } } + +extension LottieExtensionParams.Source { + fileprivate var url: URL? { + switch self { + case let .url(url): return url + case .json: return nil + } + } +} diff --git a/LayoutKit/LayoutKit/Blocks/GalleryBlock.swift b/LayoutKit/LayoutKit/Blocks/GalleryBlock.swift index e4c61e0a..f506bb1b 100644 --- a/LayoutKit/LayoutKit/Blocks/GalleryBlock.swift +++ b/LayoutKit/LayoutKit/Blocks/GalleryBlock.swift @@ -120,7 +120,7 @@ extension GalleryBlock { direction: direction, crossAlignment: crossAlignment, scrollMode: scrollMode, - state: GalleryViewState(contentPosition: contentPosition), + state: GalleryViewState(contentPosition: contentPosition, isScrolling: false), widthTrait: widthTrait, heightTrait: heightTrait, areEmptySpaceTouchesEnabled: areEmptySpaceTouchesEnabled, diff --git a/LayoutKit/LayoutKit/UI/Blocks/PagerBlock+UIViewRenderableBlock.swift b/LayoutKit/LayoutKit/UI/Blocks/PagerBlock+UIViewRenderableBlock.swift index d4a9cec4..cddeab69 100644 --- a/LayoutKit/LayoutKit/UI/Blocks/PagerBlock+UIViewRenderableBlock.swift +++ b/LayoutKit/LayoutKit/UI/Blocks/PagerBlock+UIViewRenderableBlock.swift @@ -43,8 +43,6 @@ private final class PagerView: BlockView { private weak var observer: ElementStateObserver? private weak var overscrollDelegate: ScrollDelegate? - private var isConfiguring = false - var effectiveBackgroundColor: UIColor? { backgroundColor } func configure( @@ -56,8 +54,6 @@ private final class PagerView: BlockView { overscrollDelegate: ScrollDelegate?, renderingDelegate: RenderingDelegate? ) { - guard !isConfiguring else { return } - self.observer = observer self.overscrollDelegate = overscrollDelegate @@ -84,7 +80,7 @@ private final class PagerView: BlockView { renderingDelegate: renderingDelegate ) - let oldModel: GalleryViewModel? = self.model + let oldModel = self.model self.model = model self.selectedActions = selectedActions @@ -101,7 +97,6 @@ private final class PagerView: BlockView { let lastStateCurrentPage = lastState?.currentPage if lastState != state, lastStateCurrentPage != currentPage { lastState = state - isConfiguring = true if notifyingObservers { observer?.elementStateChanged(state, forPath: model.path) @@ -115,8 +110,6 @@ private final class PagerView: BlockView { ).sendFrom(self) selectedActions[pageIndex].perform(sendingFrom: self) - - isConfiguring = false } } @@ -144,19 +137,16 @@ extension PagerView: ElementStateObserver { assertionFailure() return } - - let lastCurrentPage = lastState?.currentPage ?? 0 - let roundedPageIndex = pageIndex.rounded() - let currentPage = abs(pageIndex - roundedPageIndex) < 0.1 - ? Int(roundedPageIndex) - : Int(round(lastCurrentPage)) - setState( - PagerViewState( - numberOfPages: model.items.count, - currentPage: currentPage - ), - notifyingObservers: true - ) + + if !galleryState.isScrolling { + setState( + PagerViewState( + numberOfPages: model.items.count, + currentPage: Int(pageIndex.rounded()) + ), + notifyingObservers: true + ) + } } } diff --git a/LayoutKit/LayoutKit/UI/Views/GalleryView.swift b/LayoutKit/LayoutKit/UI/Views/GalleryView.swift index 2de0248e..22bb6350 100644 --- a/LayoutKit/LayoutKit/UI/Views/GalleryView.swift +++ b/LayoutKit/LayoutKit/UI/Views/GalleryView.swift @@ -288,9 +288,8 @@ extension GalleryView: ScrollDelegate { contentPosition = .paging(index: pageIndex) } - var state = self.state - state.contentPosition = contentPosition - setState(state, notifyingObservers: true) + let newState = GalleryViewState(contentPosition: contentPosition, isScrolling: true) + setState(newState, notifyingObservers: true) updatesDelegate?.onContentOffsetChanged(offset, in: model) visibilityDelegate?.onGalleryVisibilityChanged() } @@ -312,6 +311,8 @@ extension GalleryView: ScrollDelegate { } private func onDidEndScroll(_ scrollView: ScrollView) { + let newState = GalleryViewState(contentPosition: state.contentPosition, isScrolling: false) + setState(newState, notifyingObservers: true) visibilityDelegate?.onGalleryVisibilityChanged() let firstVisibleItemOffset = getOffset(scrollView) diff --git a/LayoutKit/LayoutKit/ViewModels/GalleryViewState.swift b/LayoutKit/LayoutKit/ViewModels/GalleryViewState.swift index cdafca92..559965e0 100644 --- a/LayoutKit/LayoutKit/ViewModels/GalleryViewState.swift +++ b/LayoutKit/LayoutKit/ViewModels/GalleryViewState.swift @@ -51,20 +51,27 @@ public struct GalleryViewState: ElementState, Equatable { } } - public var contentPosition: Position + public private(set) var contentPosition: Position + public let isScrolling: Bool public static let `default` = GalleryViewState(contentOffset: 0) public init(contentOffset: CGFloat) { self.contentPosition = .offset(contentOffset) + self.isScrolling = false } public init(contentPageIndex: CGFloat) { self.contentPosition = .paging(index: contentPageIndex) + self.isScrolling = false } - public init(contentPosition: Position) { + public init( + contentPosition: Position, + isScrolling: Bool + ) { self.contentPosition = contentPosition + self.isScrolling = isScrolling } }