From 0dfd74c5b74bd318a1ede937bc1432fa53e433dd Mon Sep 17 00:00:00 2001 From: booster Date: Wed, 3 Jun 2026 12:57:51 +0300 Subject: [PATCH] Image builders support commit_hash:13f6ef6aa582c24314dac2d1f087dfcefabdd017 --- .mapping.json | 3 + .../DivKit/Extensions/DivTextExtensions.swift | 18 +- .../DivTextImageBuilderExtensions.swift | 48 ++++ .../DivKit/generated_sources/DivText.swift | 163 +++++++++-- .../generated_sources/DivTextTemplate.swift | 272 ++++++++++++++++++ .../div-text/image_builder_414@3x.png | Bin 0 -> 13011 bytes schema/div-text.json | 48 ++++ schema/translations.json | 24 ++ .../div-text/image_builder.json | 70 +++++ 9 files changed, 610 insertions(+), 36 deletions(-) create mode 100644 client/ios/DivKit/Extensions/DivTextImageBuilderExtensions.swift create mode 100644 client/ios/Tests/reference_snapshots/div-text/image_builder_414@3x.png create mode 100644 test_data/snapshot_test_data/div-text/image_builder.json diff --git a/.mapping.json b/.mapping.json index 0caff7eba..0ccb5aeea 100644 --- a/.mapping.json +++ b/.mapping.json @@ -21050,6 +21050,7 @@ "client/ios/DivKit/Extensions/DivSwitchExtensions.swift":"divkit/public/client/ios/DivKit/Extensions/DivSwitchExtensions.swift", "client/ios/DivKit/Extensions/DivTabsExtensions.swift":"divkit/public/client/ios/DivKit/Extensions/DivTabsExtensions.swift", "client/ios/DivKit/Extensions/DivTextExtensions.swift":"divkit/public/client/ios/DivKit/Extensions/DivTextExtensions.swift", + "client/ios/DivKit/Extensions/DivTextImageBuilderExtensions.swift":"divkit/public/client/ios/DivKit/Extensions/DivTextImageBuilderExtensions.swift", "client/ios/DivKit/Extensions/DivTextRangeBuilderExtensions.swift":"divkit/public/client/ios/DivKit/Extensions/DivTextRangeBuilderExtensions.swift", "client/ios/DivKit/Extensions/DivTooltipExtensions.swift":"divkit/public/client/ios/DivKit/Extensions/DivTooltipExtensions.swift", "client/ios/DivKit/Extensions/DivTransformExtensions.swift":"divkit/public/client/ios/DivKit/Extensions/DivTransformExtensions.swift", @@ -23276,6 +23277,7 @@ "client/ios/Tests/reference_snapshots/div-text/image-background_414@3x.png":"divkit/public/client/ios/Tests/reference_snapshots/div-text/image-background_414@3x.png", "client/ios/Tests/reference_snapshots/div-text/image-reverse-indexing_414@3x.png":"divkit/public/client/ios/Tests/reference_snapshots/div-text/image-reverse-indexing_414@3x.png", "client/ios/Tests/reference_snapshots/div-text/image-tint-mode_414@3x.png":"divkit/public/client/ios/Tests/reference_snapshots/div-text/image-tint-mode_414@3x.png", + "client/ios/Tests/reference_snapshots/div-text/image_builder_414@3x.png":"divkit/public/client/ios/Tests/reference_snapshots/div-text/image_builder_414@3x.png", "client/ios/Tests/reference_snapshots/div-text/images_414@3x.png":"divkit/public/client/ios/Tests/reference_snapshots/div-text/images_414@3x.png", "client/ios/Tests/reference_snapshots/div-text/line-height/multi-line-text-with-image_414@3x.png":"divkit/public/client/ios/Tests/reference_snapshots/div-text/line-height/multi-line-text-with-image_414@3x.png", "client/ios/Tests/reference_snapshots/div-text/line-height/multi-line-text-with-text-size-ranges_414@3x.png":"divkit/public/client/ios/Tests/reference_snapshots/div-text/line-height/multi-line-text-with-text-size-ranges_414@3x.png", @@ -29641,6 +29643,7 @@ "test_data/snapshot_test_data/div-text/image-background.json":"divkit/public/test_data/snapshot_test_data/div-text/image-background.json", "test_data/snapshot_test_data/div-text/image-reverse-indexing.json":"divkit/public/test_data/snapshot_test_data/div-text/image-reverse-indexing.json", "test_data/snapshot_test_data/div-text/image-tint-mode.json":"divkit/public/test_data/snapshot_test_data/div-text/image-tint-mode.json", + "test_data/snapshot_test_data/div-text/image_builder.json":"divkit/public/test_data/snapshot_test_data/div-text/image_builder.json", "test_data/snapshot_test_data/div-text/images.json":"divkit/public/test_data/snapshot_test_data/div-text/images.json", "test_data/snapshot_test_data/div-text/images_hyphenation.json":"divkit/public/test_data/snapshot_test_data/div-text/images_hyphenation.json", "test_data/snapshot_test_data/div-text/line-height/multi-line-text-with-image.json":"divkit/public/test_data/snapshot_test_data/div-text/line-height/multi-line-text-with-image.json", diff --git a/client/ios/DivKit/Extensions/DivTextExtensions.swift b/client/ios/DivKit/Extensions/DivTextExtensions.swift index 172a4f2a9..420442adb 100644 --- a/client/ios/DivKit/Extensions/DivTextExtensions.swift +++ b/client/ios/DivKit/Extensions/DivTextExtensions.swift @@ -109,8 +109,7 @@ extension DivText: DivBlockModeling { } let images = makeInlineImages( - context: context, - images: self.images, + images: makeImages(self.images, imageBuilder: imageBuilder, context: context), text: attributedString ) @@ -124,8 +123,7 @@ extension DivText: DivBlockModeling { ) } let truncationImages = makeInlineImages( - context: context, - images: ellipsis?.images, + images: makeImages(ellipsis?.images, imageBuilder: nil, context: context), text: truncationToken ) @@ -183,17 +181,17 @@ extension DivText: DivBlockModeling { } private func makeInlineImages( - context: DivBlockModelingContext, - images: [Image]?, + images: [(Image, DivBlockModelingContext)], text: NSAttributedString? ) -> [TextBlock.InlineImage] { guard let text else { return [] } - return (images ?? []).compactMap { - $0.makeImage( - context: context, - textLength: CFAttributedStringGetLength(text) + let textLength = CFAttributedStringGetLength(text) + return images.compactMap { image, imageContext in + image.makeImage( + context: imageContext, + textLength: textLength ) } } diff --git a/client/ios/DivKit/Extensions/DivTextImageBuilderExtensions.swift b/client/ios/DivKit/Extensions/DivTextImageBuilderExtensions.swift new file mode 100644 index 000000000..c8e6e7dfc --- /dev/null +++ b/client/ios/DivKit/Extensions/DivTextImageBuilderExtensions.swift @@ -0,0 +1,48 @@ +import Foundation +import LayoutKit +import VGSL + +extension DivText { + func makeImages( + _ images: [Image]?, + imageBuilder: ImageBuilder?, + context: DivBlockModelingContext + ) -> [(Image, DivBlockModelingContext)] { + if let imageBuilder { + return imageBuilder.makeImages(context: context) + } + return (images ?? []).map { ($0, context) } + } +} + +extension DivText.ImageBuilder { + func makeImages( + context: DivBlockModelingContext + ) -> [(DivText.Image, DivBlockModelingContext)] { + let items = resolveData(context.expressionResolver) ?? [] + return items.enumerated().compactMap { index, item -> ( + DivText.Image, + DivBlockModelingContext + )? in + let itemDict = item as? DivDictionary ?? [:] + let itemContext = context.modifying( + pathSuffix: String(index), + prototypeParams: PrototypeParams( + index: index, + variableName: dataElementName, + value: itemDict + ) + ) + itemContext.variablesStorage.initializeIfNeeded(path: itemContext.path, variables: [ + DivVariableName(rawValue: dataElementName): .dict(itemDict), + "index": .integer(index), + ]) + guard let prototype = prototypes.first(where: { + $0.resolveSelector(itemContext.expressionResolver) + }) else { + return nil + } + return (prototype.image, itemContext) + } + } +} diff --git a/client/ios/DivKit/generated_sources/DivText.swift b/client/ios/DivKit/generated_sources/DivText.swift index 316824d43..ba97c8298 100644 --- a/client/ios/DivKit/generated_sources/DivText.swift +++ b/client/ios/DivKit/generated_sources/DivText.swift @@ -167,6 +167,61 @@ public final class DivText: DivBase, @unchecked Sendable { } } + public final class ImageBuilder: @unchecked Sendable { + public final class Prototype: Sendable { + public let image: Image + public let selector: Expression // default value: true + + public func resolveSelector(_ resolver: ExpressionResolver) -> Bool { + resolver.resolveNumeric(selector) ?? true + } + + public convenience init(dictionary: [String: Any], context: ParsingContext) throws { + self.init( + image: try dictionary.getField("image", transform: { (dict: [String: Any]) in try DivText.Image(dictionary: dict, context: context) }, context: context), + selector: try dictionary.getOptionalExpressionField("selector", context: context) + ) + } + + init( + image: Image, + selector: Expression? = nil + ) { + self.image = image + self.selector = selector ?? .value(true) + } + } + + public let data: Expression<[Any]> + public let dataElementName: String // default value: it + public let prototypes: [Prototype] // at least 1 elements + + public func resolveData(_ resolver: ExpressionResolver) -> [Any]? { + resolver.resolveArray(data) + } + + static let prototypesValidator: AnyArrayValueValidator = + makeArrayValidator(minItems: 1) + + public convenience init(dictionary: [String: Any], context: ParsingContext) throws { + self.init( + data: try dictionary.getExpressionField("data", context: context), + dataElementName: try dictionary.getOptionalField("data_element_name", context: context), + prototypes: try dictionary.getArray("prototypes", transform: { (dict: [String: Any]) in try? DivText.ImageBuilder.Prototype(dictionary: dict, context: context) }, validator: Self.prototypesValidator, context: context) + ) + } + + init( + data: Expression<[Any]>, + dataElementName: String? = nil, + prototypes: [Prototype] + ) { + self.data = data + self.dataElementName = dataElementName ?? "it" + self.prototypes = prototypes + } + } + public final class Range: @unchecked Sendable { public let actions: [DivAction]? public let alignmentVertical: Expression? @@ -440,6 +495,7 @@ public final class DivText: DivBase, @unchecked Sendable { public let hoverEndActions: [DivAction]? public let hoverStartActions: [DivAction]? public let id: String? + public let imageBuilder: ImageBuilder? public let images: [Image]? public let layoutProvider: DivLayoutProvider? public let letterSpacing: Expression // default value: 0 @@ -661,6 +717,7 @@ public final class DivText: DivBase, @unchecked Sendable { hoverEndActions: try dictionary.getOptionalArray("hover_end_actions", transform: { (dict: [String: Any]) in try? DivAction(dictionary: dict, context: context) }), hoverStartActions: try dictionary.getOptionalArray("hover_start_actions", transform: { (dict: [String: Any]) in try? DivAction(dictionary: dict, context: context) }), id: try dictionary.getOptionalField("id", context: context), + imageBuilder: try dictionary.getOptionalField("image_builder", transform: { (dict: [String: Any]) in try DivText.ImageBuilder(dictionary: dict, context: context) }), images: try dictionary.getOptionalArray("images", transform: { (dict: [String: Any]) in try? DivText.Image(dictionary: dict, context: context) }), layoutProvider: try dictionary.getOptionalField("layout_provider", transform: { (dict: [String: Any]) in try DivLayoutProvider(dictionary: dict, context: context) }), letterSpacing: try dictionary.getOptionalExpressionField("letter_spacing", context: context), @@ -736,6 +793,7 @@ public final class DivText: DivBase, @unchecked Sendable { hoverEndActions: [DivAction]? = nil, hoverStartActions: [DivAction]? = nil, id: String? = nil, + imageBuilder: ImageBuilder? = nil, images: [Image]? = nil, layoutProvider: DivLayoutProvider? = nil, letterSpacing: Expression? = nil, @@ -808,6 +866,7 @@ public final class DivText: DivBase, @unchecked Sendable { self.hoverEndActions = hoverEndActions self.hoverStartActions = hoverStartActions self.id = id + self.imageBuilder = imageBuilder self.images = images self.layoutProvider = layoutProvider self.letterSpacing = letterSpacing ?? .value(0) @@ -926,97 +985,98 @@ extension DivText: Equatable { return false } guard + lhs.imageBuilder == rhs.imageBuilder, lhs.images == rhs.images, - lhs.layoutProvider == rhs.layoutProvider, - lhs.letterSpacing == rhs.letterSpacing + lhs.layoutProvider == rhs.layoutProvider else { return false } guard + lhs.letterSpacing == rhs.letterSpacing, lhs.lineHeight == rhs.lineHeight, - lhs.longtapActions == rhs.longtapActions, - lhs.margins == rhs.margins + lhs.longtapActions == rhs.longtapActions else { return false } guard + lhs.margins == rhs.margins, lhs.maxLines == rhs.maxLines, - lhs.minHiddenLines == rhs.minHiddenLines, - lhs.paddings == rhs.paddings + lhs.minHiddenLines == rhs.minHiddenLines else { return false } guard + lhs.paddings == rhs.paddings, lhs.pressEndActions == rhs.pressEndActions, - lhs.pressStartActions == rhs.pressStartActions, - lhs.rangeBuilder == rhs.rangeBuilder + lhs.pressStartActions == rhs.pressStartActions else { return false } guard + lhs.rangeBuilder == rhs.rangeBuilder, lhs.ranges == rhs.ranges, - lhs.reuseId == rhs.reuseId, - lhs.rowSpan == rhs.rowSpan + lhs.reuseId == rhs.reuseId else { return false } guard + lhs.rowSpan == rhs.rowSpan, lhs.selectable == rhs.selectable, - lhs.selectedActions == rhs.selectedActions, - lhs.strike == rhs.strike + lhs.selectedActions == rhs.selectedActions else { return false } guard + lhs.strike == rhs.strike, lhs.text == rhs.text, - lhs.textAlignmentHorizontal == rhs.textAlignmentHorizontal, - lhs.textAlignmentVertical == rhs.textAlignmentVertical + lhs.textAlignmentHorizontal == rhs.textAlignmentHorizontal else { return false } guard + lhs.textAlignmentVertical == rhs.textAlignmentVertical, lhs.textColor == rhs.textColor, - lhs.textGradient == rhs.textGradient, - lhs.textShadow == rhs.textShadow + lhs.textGradient == rhs.textGradient else { return false } guard + lhs.textShadow == rhs.textShadow, lhs.tightenWidth == rhs.tightenWidth, - lhs.tooltips == rhs.tooltips, - lhs.transform == rhs.transform + lhs.tooltips == rhs.tooltips else { return false } guard + lhs.transform == rhs.transform, lhs.transformations == rhs.transformations, - lhs.transitionChange == rhs.transitionChange, - lhs.transitionIn == rhs.transitionIn + lhs.transitionChange == rhs.transitionChange else { return false } guard + lhs.transitionIn == rhs.transitionIn, lhs.transitionOut == rhs.transitionOut, - lhs.transitionTriggers == rhs.transitionTriggers, - lhs.truncate == rhs.truncate + lhs.transitionTriggers == rhs.transitionTriggers else { return false } guard + lhs.truncate == rhs.truncate, lhs.underline == rhs.underline, - lhs.variableTriggers == rhs.variableTriggers, - lhs.variables == rhs.variables + lhs.variableTriggers == rhs.variableTriggers else { return false } guard + lhs.variables == rhs.variables, lhs.visibility == rhs.visibility, - lhs.visibilityAction == rhs.visibilityAction, - lhs.visibilityActions == rhs.visibilityActions + lhs.visibilityAction == rhs.visibilityAction else { return false } guard + lhs.visibilityActions == rhs.visibilityActions, lhs.width == rhs.width else { return false @@ -1062,6 +1122,7 @@ extension DivText: Serializable { result["hover_end_actions"] = hoverEndActions?.map { $0.toDictionary() } result["hover_start_actions"] = hoverStartActions?.map { $0.toDictionary() } result["id"] = id + result["image_builder"] = imageBuilder?.toDictionary() result["images"] = images?.map { $0.toDictionary() } result["layout_provider"] = layoutProvider?.toDictionary() result["letter_spacing"] = letterSpacing.toValidSerializationValue() @@ -1106,6 +1167,21 @@ extension DivText: Serializable { } } +#if DEBUG +// WARNING: this == is incomplete because of [String: Any] in class fields +extension DivText.ImageBuilder: Equatable { + public static func ==(lhs: DivText.ImageBuilder, rhs: DivText.ImageBuilder) -> Bool { + guard + lhs.dataElementName == rhs.dataElementName, + lhs.prototypes == rhs.prototypes + else { + return false + } + return true + } +} +#endif + #if DEBUG // WARNING: this == is incomplete because of [String: Any] in class fields extension DivText.Range: Equatable { @@ -1247,6 +1323,20 @@ extension DivText.Image: Equatable { } #endif +#if DEBUG +extension DivText.ImageBuilder.Prototype: Equatable { + public static func ==(lhs: DivText.ImageBuilder.Prototype, rhs: DivText.ImageBuilder.Prototype) -> Bool { + guard + lhs.image == rhs.image, + lhs.selector == rhs.selector + else { + return false + } + return true + } +} +#endif + #if DEBUG extension DivText.RangeBuilder.Prototype: Equatable { public static func ==(lhs: DivText.RangeBuilder.Prototype, rhs: DivText.RangeBuilder.Prototype) -> Bool { @@ -1301,6 +1391,27 @@ extension DivText.Image: Serializable { } } +extension DivText.ImageBuilder.Prototype: Serializable { + @_optimize(size) + public func toDictionary() -> [String: ValidSerializationValue] { + var result: [String: ValidSerializationValue] = [:] + result["image"] = image.toDictionary() + result["selector"] = selector.toValidSerializationValue() + return result + } +} + +extension DivText.ImageBuilder: Serializable { + @_optimize(size) + public func toDictionary() -> [String: ValidSerializationValue] { + var result: [String: ValidSerializationValue] = [:] + result["data"] = data.toValidSerializationValue() + result["data_element_name"] = dataElementName + result["prototypes"] = prototypes.map { $0.toDictionary() } + return result + } +} + extension DivText.Range: Serializable { @_optimize(size) public func toDictionary() -> [String: ValidSerializationValue] { diff --git a/client/ios/DivKit/generated_sources/DivTextTemplate.swift b/client/ios/DivKit/generated_sources/DivTextTemplate.swift index 77b82c133..f62b3146e 100644 --- a/client/ios/DivKit/generated_sources/DivTextTemplate.swift +++ b/client/ios/DivKit/generated_sources/DivTextTemplate.swift @@ -159,6 +159,255 @@ public final class DivTextTemplate: TemplateValue, @unchecked Sendable { } } + public final class ImageBuilderTemplate: TemplateValue, @unchecked Sendable { + public final class PrototypeTemplate: TemplateValue, Sendable { + public let image: Field? + public let selector: Field>? // default value: true + + public convenience init(dictionary: [String: Any], templateToType: [TemplateName: String]) throws { + self.init( + image: dictionary.getOptionalField("image", templateToType: templateToType), + selector: dictionary.getOptionalExpressionField("selector") + ) + } + + init( + image: Field? = nil, + selector: Field>? = nil + ) { + self.image = image + self.selector = selector + } + + private static func resolveOnlyLinks(context: TemplatesContext, parent: PrototypeTemplate?) -> DeserializationResult { + let imageValue = { parent?.image?.resolveValue(context: context, useOnlyLinks: true) ?? .noValue }() + let selectorValue = { parent?.selector?.resolveOptionalValue(context: context) ?? .noValue }() + var errors = mergeErrors( + imageValue.errorsOrWarnings?.map { .nestedObjectError(field: "image", error: $0) }, + selectorValue.errorsOrWarnings?.map { .nestedObjectError(field: "selector", error: $0) } + ) + if case .noValue = imageValue { + errors.append(.requiredFieldIsMissing(field: "image")) + } + guard + let imageNonNil = imageValue.value + else { + return .failure(NonEmptyArray(errors)!) + } + let result = DivText.ImageBuilder.Prototype( + image: { imageNonNil }(), + selector: { selectorValue.value }() + ) + return errors.isEmpty ? .success(result) : .partialSuccess(result, warnings: NonEmptyArray(errors)!) + } + + public static func resolveValue(context: TemplatesContext, parent: PrototypeTemplate?, useOnlyLinks: Bool) -> DeserializationResult { + if useOnlyLinks { + return resolveOnlyLinks(context: context, parent: parent) + } + var imageValue: DeserializationResult = .noValue + var selectorValue: DeserializationResult> = { parent?.selector?.value() ?? .noValue }() + _ = { + // Each field is parsed in its own lambda to keep the stack size managable + // Otherwise the compiler will allocate stack for each intermediate variable + // upfront even when we don't actually visit a relevant branch + for (key, __dictValue) in context.templateData { + _ = { + if key == "image" { + imageValue = deserialize(__dictValue, templates: context.templates, templateToType: context.templateToType, type: DivTextTemplate.ImageTemplate.self).merged(with: imageValue) + } + }() + _ = { + if key == "selector" { + selectorValue = deserialize(__dictValue).merged(with: selectorValue) + } + }() + _ = { + if key == parent?.image?.link { + imageValue = imageValue.merged(with: { deserialize(__dictValue, templates: context.templates, templateToType: context.templateToType, type: DivTextTemplate.ImageTemplate.self) }) + } + }() + _ = { + if key == parent?.selector?.link { + selectorValue = selectorValue.merged(with: { deserialize(__dictValue) }) + } + }() + } + }() + if let parent = parent { + _ = { imageValue = imageValue.merged(with: { parent.image?.resolveValue(context: context, useOnlyLinks: true) }) }() + } + var errors = mergeErrors( + imageValue.errorsOrWarnings?.map { .nestedObjectError(field: "image", error: $0) }, + selectorValue.errorsOrWarnings?.map { .nestedObjectError(field: "selector", error: $0) } + ) + if case .noValue = imageValue { + errors.append(.requiredFieldIsMissing(field: "image")) + } + guard + let imageNonNil = imageValue.value + else { + return .failure(NonEmptyArray(errors)!) + } + let result = DivText.ImageBuilder.Prototype( + image: { imageNonNil }(), + selector: { selectorValue.value }() + ) + return errors.isEmpty ? .success(result) : .partialSuccess(result, warnings: NonEmptyArray(errors)!) + } + + private func mergedWithParent(templates: [TemplateName: Any]) throws -> PrototypeTemplate { + return self + } + + public func resolveParent(templates: [TemplateName: Any]) throws -> PrototypeTemplate { + let merged = try mergedWithParent(templates: templates) + + return PrototypeTemplate( + image: try merged.image?.resolveParent(templates: templates), + selector: merged.selector + ) + } + } + + public let data: Field>? + public let dataElementName: Field? // default value: it + public let prototypes: Field<[PrototypeTemplate]>? // at least 1 elements + + public convenience init(dictionary: [String: Any], templateToType: [TemplateName: String]) throws { + self.init( + data: dictionary.getOptionalExpressionField("data"), + dataElementName: dictionary.getOptionalField("data_element_name"), + prototypes: dictionary.getOptionalArray("prototypes", templateToType: templateToType) + ) + } + + init( + data: Field>? = nil, + dataElementName: Field? = nil, + prototypes: Field<[PrototypeTemplate]>? = nil + ) { + self.data = data + self.dataElementName = dataElementName + self.prototypes = prototypes + } + + private static func resolveOnlyLinks(context: TemplatesContext, parent: ImageBuilderTemplate?) -> DeserializationResult { + let dataValue = { parent?.data?.resolveValue(context: context) ?? .noValue }() + let dataElementNameValue = { parent?.dataElementName?.resolveOptionalValue(context: context) ?? .noValue }() + let prototypesValue = { parent?.prototypes?.resolveValue(context: context, validator: ResolvedValue.prototypesValidator, useOnlyLinks: true) ?? .noValue }() + var errors = mergeErrors( + dataValue.errorsOrWarnings?.map { .nestedObjectError(field: "data", error: $0) }, + dataElementNameValue.errorsOrWarnings?.map { .nestedObjectError(field: "data_element_name", error: $0) }, + prototypesValue.errorsOrWarnings?.map { .nestedObjectError(field: "prototypes", error: $0) } + ) + if case .noValue = dataValue { + errors.append(.requiredFieldIsMissing(field: "data")) + } + if case .noValue = prototypesValue { + errors.append(.requiredFieldIsMissing(field: "prototypes")) + } + guard + let dataNonNil = dataValue.value, + let prototypesNonNil = prototypesValue.value + else { + return .failure(NonEmptyArray(errors)!) + } + let result = DivText.ImageBuilder( + data: { dataNonNil }(), + dataElementName: { dataElementNameValue.value }(), + prototypes: { prototypesNonNil }() + ) + return errors.isEmpty ? .success(result) : .partialSuccess(result, warnings: NonEmptyArray(errors)!) + } + + public static func resolveValue(context: TemplatesContext, parent: ImageBuilderTemplate?, useOnlyLinks: Bool) -> DeserializationResult { + if useOnlyLinks { + return resolveOnlyLinks(context: context, parent: parent) + } + var dataValue: DeserializationResult> = { parent?.data?.value() ?? .noValue }() + var dataElementNameValue: DeserializationResult = { parent?.dataElementName?.value() ?? .noValue }() + var prototypesValue: DeserializationResult<[DivText.ImageBuilder.Prototype]> = .noValue + _ = { + // Each field is parsed in its own lambda to keep the stack size managable + // Otherwise the compiler will allocate stack for each intermediate variable + // upfront even when we don't actually visit a relevant branch + for (key, __dictValue) in context.templateData { + _ = { + if key == "data" { + dataValue = deserialize(__dictValue).merged(with: dataValue) + } + }() + _ = { + if key == "data_element_name" { + dataElementNameValue = deserialize(__dictValue).merged(with: dataElementNameValue) + } + }() + _ = { + if key == "prototypes" { + prototypesValue = deserialize(__dictValue, templates: context.templates, templateToType: context.templateToType, validator: ResolvedValue.prototypesValidator, type: DivTextTemplate.ImageBuilderTemplate.PrototypeTemplate.self).merged(with: prototypesValue) + } + }() + _ = { + if key == parent?.data?.link { + dataValue = dataValue.merged(with: { deserialize(__dictValue) }) + } + }() + _ = { + if key == parent?.dataElementName?.link { + dataElementNameValue = dataElementNameValue.merged(with: { deserialize(__dictValue) }) + } + }() + _ = { + if key == parent?.prototypes?.link { + prototypesValue = prototypesValue.merged(with: { deserialize(__dictValue, templates: context.templates, templateToType: context.templateToType, validator: ResolvedValue.prototypesValidator, type: DivTextTemplate.ImageBuilderTemplate.PrototypeTemplate.self) }) + } + }() + } + }() + if let parent = parent { + _ = { prototypesValue = prototypesValue.merged(with: { parent.prototypes?.resolveValue(context: context, validator: ResolvedValue.prototypesValidator, useOnlyLinks: true) }) }() + } + var errors = mergeErrors( + dataValue.errorsOrWarnings?.map { .nestedObjectError(field: "data", error: $0) }, + dataElementNameValue.errorsOrWarnings?.map { .nestedObjectError(field: "data_element_name", error: $0) }, + prototypesValue.errorsOrWarnings?.map { .nestedObjectError(field: "prototypes", error: $0) } + ) + if case .noValue = dataValue { + errors.append(.requiredFieldIsMissing(field: "data")) + } + if case .noValue = prototypesValue { + errors.append(.requiredFieldIsMissing(field: "prototypes")) + } + guard + let dataNonNil = dataValue.value, + let prototypesNonNil = prototypesValue.value + else { + return .failure(NonEmptyArray(errors)!) + } + let result = DivText.ImageBuilder( + data: { dataNonNil }(), + dataElementName: { dataElementNameValue.value }(), + prototypes: { prototypesNonNil }() + ) + return errors.isEmpty ? .success(result) : .partialSuccess(result, warnings: NonEmptyArray(errors)!) + } + + private func mergedWithParent(templates: [TemplateName: Any]) throws -> ImageBuilderTemplate { + return self + } + + public func resolveParent(templates: [TemplateName: Any]) throws -> ImageBuilderTemplate { + let merged = try mergedWithParent(templates: templates) + + return ImageBuilderTemplate( + data: merged.data, + dataElementName: merged.dataElementName, + prototypes: try merged.prototypes?.resolveParent(templates: templates) + ) + } + } + public final class ImageTemplate: TemplateValue, Sendable { public final class AccessibilityTemplate: TemplateValue, Sendable { public typealias Kind = DivText.Image.Accessibility.Kind @@ -1339,6 +1588,7 @@ public final class DivTextTemplate: TemplateValue, @unchecked Sendable { public let hoverEndActions: Field<[DivActionTemplate]>? public let hoverStartActions: Field<[DivActionTemplate]>? public let id: Field? + public let imageBuilder: Field? public let images: Field<[ImageTemplate]>? public let layoutProvider: Field? public let letterSpacing: Field>? // default value: 0 @@ -1414,6 +1664,7 @@ public final class DivTextTemplate: TemplateValue, @unchecked Sendable { hoverEndActions: dictionary.getOptionalArray("hover_end_actions", templateToType: templateToType), hoverStartActions: dictionary.getOptionalArray("hover_start_actions", templateToType: templateToType), id: dictionary.getOptionalField("id"), + imageBuilder: dictionary.getOptionalField("image_builder", templateToType: templateToType), images: dictionary.getOptionalArray("images", templateToType: templateToType), layoutProvider: dictionary.getOptionalField("layout_provider", templateToType: templateToType), letterSpacing: dictionary.getOptionalExpressionField("letter_spacing"), @@ -1490,6 +1741,7 @@ public final class DivTextTemplate: TemplateValue, @unchecked Sendable { hoverEndActions: Field<[DivActionTemplate]>? = nil, hoverStartActions: Field<[DivActionTemplate]>? = nil, id: Field? = nil, + imageBuilder: Field? = nil, images: Field<[ImageTemplate]>? = nil, layoutProvider: Field? = nil, letterSpacing: Field>? = nil, @@ -1563,6 +1815,7 @@ public final class DivTextTemplate: TemplateValue, @unchecked Sendable { self.hoverEndActions = hoverEndActions self.hoverStartActions = hoverStartActions self.id = id + self.imageBuilder = imageBuilder self.images = images self.layoutProvider = layoutProvider self.letterSpacing = letterSpacing @@ -1637,6 +1890,7 @@ public final class DivTextTemplate: TemplateValue, @unchecked Sendable { let hoverEndActionsValue = { parent?.hoverEndActions?.resolveOptionalValue(context: context, useOnlyLinks: true) ?? .noValue }() let hoverStartActionsValue = { parent?.hoverStartActions?.resolveOptionalValue(context: context, useOnlyLinks: true) ?? .noValue }() let idValue = { parent?.id?.resolveOptionalValue(context: context) ?? .noValue }() + let imageBuilderValue = { parent?.imageBuilder?.resolveOptionalValue(context: context, useOnlyLinks: true) ?? .noValue }() let imagesValue = { parent?.images?.resolveOptionalValue(context: context, useOnlyLinks: true) ?? .noValue }() let layoutProviderValue = { parent?.layoutProvider?.resolveOptionalValue(context: context, useOnlyLinks: true) ?? .noValue }() let letterSpacingValue = { parent?.letterSpacing?.resolveOptionalValue(context: context) ?? .noValue }() @@ -1709,6 +1963,7 @@ public final class DivTextTemplate: TemplateValue, @unchecked Sendable { hoverEndActionsValue.errorsOrWarnings?.map { .nestedObjectError(field: "hover_end_actions", error: $0) }, hoverStartActionsValue.errorsOrWarnings?.map { .nestedObjectError(field: "hover_start_actions", error: $0) }, idValue.errorsOrWarnings?.map { .nestedObjectError(field: "id", error: $0) }, + imageBuilderValue.errorsOrWarnings?.map { .nestedObjectError(field: "image_builder", error: $0) }, imagesValue.errorsOrWarnings?.map { .nestedObjectError(field: "images", error: $0) }, layoutProviderValue.errorsOrWarnings?.map { .nestedObjectError(field: "layout_provider", error: $0) }, letterSpacingValue.errorsOrWarnings?.map { .nestedObjectError(field: "letter_spacing", error: $0) }, @@ -1790,6 +2045,7 @@ public final class DivTextTemplate: TemplateValue, @unchecked Sendable { hoverEndActions: { hoverEndActionsValue.value }(), hoverStartActions: { hoverStartActionsValue.value }(), id: { idValue.value }(), + imageBuilder: { imageBuilderValue.value }(), images: { imagesValue.value }(), layoutProvider: { layoutProviderValue.value }(), letterSpacing: { letterSpacingValue.value }(), @@ -1869,6 +2125,7 @@ public final class DivTextTemplate: TemplateValue, @unchecked Sendable { var hoverEndActionsValue: DeserializationResult<[DivAction]> = .noValue var hoverStartActionsValue: DeserializationResult<[DivAction]> = .noValue var idValue: DeserializationResult = { parent?.id?.value() ?? .noValue }() + var imageBuilderValue: DeserializationResult = .noValue var imagesValue: DeserializationResult<[DivText.Image]> = .noValue var layoutProviderValue: DeserializationResult = .noValue var letterSpacingValue: DeserializationResult> = { parent?.letterSpacing?.value() ?? .noValue }() @@ -2069,6 +2326,11 @@ public final class DivTextTemplate: TemplateValue, @unchecked Sendable { idValue = deserialize(__dictValue).merged(with: idValue) } }() + _ = { + if key == "image_builder" { + imageBuilderValue = deserialize(__dictValue, templates: context.templates, templateToType: context.templateToType, type: DivTextTemplate.ImageBuilderTemplate.self).merged(with: imageBuilderValue) + } + }() _ = { if key == "images" { imagesValue = deserialize(__dictValue, templates: context.templates, templateToType: context.templateToType, type: DivTextTemplate.ImageTemplate.self).merged(with: imagesValue) @@ -2424,6 +2686,11 @@ public final class DivTextTemplate: TemplateValue, @unchecked Sendable { idValue = idValue.merged(with: { deserialize(__dictValue) }) } }() + _ = { + if key == parent?.imageBuilder?.link { + imageBuilderValue = imageBuilderValue.merged(with: { deserialize(__dictValue, templates: context.templates, templateToType: context.templateToType, type: DivTextTemplate.ImageBuilderTemplate.self) }) + } + }() _ = { if key == parent?.images?.link { imagesValue = imagesValue.merged(with: { deserialize(__dictValue, templates: context.templates, templateToType: context.templateToType, type: DivTextTemplate.ImageTemplate.self) }) @@ -2643,6 +2910,7 @@ public final class DivTextTemplate: TemplateValue, @unchecked Sendable { _ = { heightValue = heightValue.merged(with: { parent.height?.resolveOptionalValue(context: context, useOnlyLinks: true) }) }() _ = { hoverEndActionsValue = hoverEndActionsValue.merged(with: { parent.hoverEndActions?.resolveOptionalValue(context: context, useOnlyLinks: true) }) }() _ = { hoverStartActionsValue = hoverStartActionsValue.merged(with: { parent.hoverStartActions?.resolveOptionalValue(context: context, useOnlyLinks: true) }) }() + _ = { imageBuilderValue = imageBuilderValue.merged(with: { parent.imageBuilder?.resolveOptionalValue(context: context, useOnlyLinks: true) }) }() _ = { imagesValue = imagesValue.merged(with: { parent.images?.resolveOptionalValue(context: context, useOnlyLinks: true) }) }() _ = { layoutProviderValue = layoutProviderValue.merged(with: { parent.layoutProvider?.resolveOptionalValue(context: context, useOnlyLinks: true) }) }() _ = { longtapActionsValue = longtapActionsValue.merged(with: { parent.longtapActions?.resolveOptionalValue(context: context, useOnlyLinks: true) }) }() @@ -2699,6 +2967,7 @@ public final class DivTextTemplate: TemplateValue, @unchecked Sendable { hoverEndActionsValue.errorsOrWarnings?.map { .nestedObjectError(field: "hover_end_actions", error: $0) }, hoverStartActionsValue.errorsOrWarnings?.map { .nestedObjectError(field: "hover_start_actions", error: $0) }, idValue.errorsOrWarnings?.map { .nestedObjectError(field: "id", error: $0) }, + imageBuilderValue.errorsOrWarnings?.map { .nestedObjectError(field: "image_builder", error: $0) }, imagesValue.errorsOrWarnings?.map { .nestedObjectError(field: "images", error: $0) }, layoutProviderValue.errorsOrWarnings?.map { .nestedObjectError(field: "layout_provider", error: $0) }, letterSpacingValue.errorsOrWarnings?.map { .nestedObjectError(field: "letter_spacing", error: $0) }, @@ -2780,6 +3049,7 @@ public final class DivTextTemplate: TemplateValue, @unchecked Sendable { hoverEndActions: { hoverEndActionsValue.value }(), hoverStartActions: { hoverStartActionsValue.value }(), id: { idValue.value }(), + imageBuilder: { imageBuilderValue.value }(), images: { imagesValue.value }(), layoutProvider: { layoutProviderValue.value }(), letterSpacing: { letterSpacingValue.value }(), @@ -2864,6 +3134,7 @@ public final class DivTextTemplate: TemplateValue, @unchecked Sendable { hoverEndActions: hoverEndActions ?? mergedParent.hoverEndActions, hoverStartActions: hoverStartActions ?? mergedParent.hoverStartActions, id: id ?? mergedParent.id, + imageBuilder: imageBuilder ?? mergedParent.imageBuilder, images: images ?? mergedParent.images, layoutProvider: layoutProvider ?? mergedParent.layoutProvider, letterSpacing: letterSpacing ?? mergedParent.letterSpacing, @@ -2943,6 +3214,7 @@ public final class DivTextTemplate: TemplateValue, @unchecked Sendable { hoverEndActions: merged.hoverEndActions?.tryResolveParent(templates: templates), hoverStartActions: merged.hoverStartActions?.tryResolveParent(templates: templates), id: merged.id, + imageBuilder: merged.imageBuilder?.tryResolveParent(templates: templates), images: merged.images?.tryResolveParent(templates: templates), layoutProvider: merged.layoutProvider?.tryResolveParent(templates: templates), letterSpacing: merged.letterSpacing, diff --git a/client/ios/Tests/reference_snapshots/div-text/image_builder_414@3x.png b/client/ios/Tests/reference_snapshots/div-text/image_builder_414@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..f8349e875b3a5831dc3a611c271effd7d9b0c415 GIT binary patch literal 13011 zcmZv@WmsInwk_H?L7QMfg1fsrjZ1LX;O@cQA-F?ucXto&?gT<`2ohYwTiIuyckhqe z^{uX2{gv0OS#yq29jT-!g^Gla1OkCjWuzf0AP_Vekd{V-2fimDIl@2$<)R`b2CA7R zJO*Ao%(P_86%;^pKpGJQ2lXBV^Dhe!Kv4J~xc{U&E_6$tjf zd31pI-)9wgfYSesq4S{sHwQFX9?buyLDK)S&iqU!0HSw}(%LQ{5Gl{U2TCH9)d={J z&b|a**BmqKR06a99 zHm@n35_l1@BEcu0zm(b?cFM~O{1>3{5k-mu=EYKh7pPEiNz~+{!%qDFQ##^b>4N5X z>VKsRwsTW0M_FUC_|UL8z(7!fz(7S{&FPF{aNV(cVmQ(HZwp;O z3lz1(Vo!O2WBM?lSk{3ewi^$tBVo5Lr^#lJB-Q;m#yBxYydhB4-ir1p1_z6$4GBK= zTy326&9VLbY^^?!kyH8pzvJGU69JKVHet`h1CtP;HZ0BY8>QT*&JPp3lWwz#{68Iw zm-<)R?6QWg9p%}2^R@j&&vQ-MZaS-pX(VOp+vD%X886SHs}eS(cT||D5bKU}#PY7rD2^^Z@Be8-)jVMgf}5XiW=YaiX=ds4 z>i{D_I`$q{D4lQ|NrqYh^{XoA@xr}$=N7JL$A5tn1|xQf^ZcC^FaXm<5lc;WI_vj} zI6#p!g8x(GuUr;r$CT0qi(yWLEYRDJP$`Yy{uE2OaT6coS@~h%q z=NJ2G(dG6R^q1r_tSSQ|I)(+6|5N=EP<^Ad^}K>6Vua7jpON{}PsGP_h2gHJ%L+{0 zH!AJV=bih_D+WnnVJ+G&WKm>z-1d6WF*ar@4@M1E*R{QW6H9ZvWbNDcad_WeuJ_9d zV-pMGgmk8bo^9KoPAYf3{r^5%aGArY`rfTs%y+swv|o+W?>8+dCJmtR%g6Gc^YGqp zy6iul)~0c9`>@y0!>z4+i@6xAs(tEGDPQm)O!239I>#^XY7BxVRaJ+(fvr^-{<#{MGlOwnvj4l-a_V<~& z(aX-xWbVp*EUoni>Y4R{ChgJPhC^?PrhzAA4RUF&pV#KF;N7ap)fjc|sPJ3UUw3Yw zhsy_l|Hu8bk=v`w0ra%SX+C;;O=7Pj(np}njkuOR7M2!@-ToKd_g$Y}?zjET>W6TyudKFv zx?lc;>kJHt8~Wa+$n&2y83$Xb)op` zoAWqrv3NKdE6d~aMeo9@MMvEE{h9@F1~BKUxu4q2(~aSeH`^OgKJsy3zBRnhuPjd0 z8K>RlJ^ZG+TqwI&DDv7yyfm4vT685`MXIV5FpBj@a6Z1p;F|_I&reWkjoT|zN9o^W z(O`Jg#3V&NnS%_A9QlT6wh0F3y#%RoV8$|^9;kDD*f3`b_*kxgxUJ}yvr%FK5?GG< z%=)qOc3GF%E$BOzsyt~lQ%9Me(-1bB*Nr8ABedqRo`LI}M8h~;rD=uSi#Jo(rP$+n z=LMS}BPO{2i4~9ui}xa$vVM|Uvg7Bh>BU~amNSdxJ*XQ`MTq!H$g48na@M+SV4m%! zadCH2S?l!(I?~XwqGU#=d;fEs8eM~SEAmqgD%Wb?1kB%00)Il%!Very1U(fmrtVFa z+`c#G0s^_;Fu{~mkQ#8&`)1JLdQ9dMQe*K@19POvvjhA1dcU1CWZi7y^*|MG0D#>h&T>*T*Qu^>G933&#L1|c)iSP__0gL51yqHCgs`T)a)b&^lJAR;T^|J+YmQtDbyreFD5@3q{VX*rd*6o{-EDb0IPm9h z`>y9WVtB5rGopTMWLU0fI&{fZdhhyOe-DoAYqf=^v`*&7FRP^3811l4`lTYe_U&dB zwmoqRds!Or=fZ$aF>k4G2_x|7D#rcqWZe>bObEY{Z)Xl7lYV? z(c)(~rxd&9B@N8U0By_a=4DyN+*96~QxrVG%RU%wB14*~Km?+6I+s@{3{HmY+^CON z{a)5l=K|#-Asj`Np9E267RkZ=&_}0`_LI`nj@oJ)Ig($A+@69>l}ifm21sN+E_rR% zn~W^JNvK2-`@bls>N$_8m>Fq}n7Ms{PSADeUZREV_985GjQ4y#YuyKSOM*>JyQGgW zJR#gf-io2WK8}thX{qfP?!hyUf_CDg-pGy7JmTKr_>5anJjT~aDOh$n_Rv7*1$ld( z?nl!dBJ&-xrpxch-k!USFa^TblkEEkSq`xyxc3_l!rB(uZYqo)Htaj?s1kgLkp!o* zzpT@KN!~9k5duVT$oaA#0lCCB*B^BtDi{HMnnFAW(uE$NxXxeBPr|0CT`r0Cfw<3h z+?8bhQ?WAC^eFkBok;~ZNf2Bgf>@b<=UGdZ61sio8CN@?Ja>)ls}{2CTC{(|7#-X< zyJ6x-Y`V+{DmVyK*SH8bRVtiBp@YnOzI+ChKnnXbkND-#j{n2MZrq6YhWB}Usl4Fr z;`AL{={&=rf9RL>+w(H-)7su1LZ?9t=iL}?q;rfF1Mg}j;jzOcsINvYO)bzEejzPH zyyN+-_ns2MFP8($#HgPCVg@hA5~$xs%8s<7UFXj{Q{m~Jc85E`R?F*pE>r5aMCMs` zT)YSACa^Y>;k4bzZ?vyd|By3A+szW}{cV{-$zh(STY<4UyxsG(;t31q- z?5bAoUc6w)=C;S7o}X>hf5+po2gLsw;Gkpl{MBVf&qggw05+DyRUB`&_f*&-_Pt4H z_AT0C9gIlIuUtuI;F_-1TH(mMYj)#dT-H(4_3KOgYjEQ?jAxN-=>OU*qvrdg75&UQ zsRK5bRFm2@K>{aI)XL@Lgxq`@q@n+0{>28Lbzn9ncZ}{q^a*h#*eq3hcnl^NBdJPc z517VQw)Wjv{>hhRmlDZ!CFQv-?nb%?tG48U-v}Fm+ap#;_7e^U$i(uE%km@*zs{CnVQbp@rSH#sPr-CmN&*%+jZObUR@BO)NI3+x#YtN z_Ul~;B>2t&Po~*Wt`~epJl~|5pfN^H=b)Q}8ev44K09bzZkoMx`}MV=vW8B=9RJrq zXd!3gHWh_&M=K+%u})8tnd`^!FU^lr`i}i@>SwTC1Ai6|P39)^|3t(lD{O12`oP9J zX`7_WDt??63oyjZGlmX%_p>q}!bXQia8+P%Hbw!1x%b7Hi0jV{haVf4fu4B%Wz!F4 zf{33l8Isy1+;biemWyS|(*{XQ)QSdH9F9LlDh)W$?|33`NKql__M>2Z<&M2Xxo>P_ zy>XU{kCv7Z3t{CxKDL7Vt^+)Nc!N?_1mgu;D*j0q!Mh#MWOG^xJ880%cc^1*424kR z$xcL9NCNBeCvXwq-eI4WZ!pr=K@VJN7B7}1_*rKCeBxgtDKe~V5A*fymOr@lFJfpc zF!*sy=~2Kl)|CI;_PJJIvx+vs4WPZsHQEcI9WnRh5x!ki)fH$(ikL;r#H+V2dKJ)h zRHrr>iN$_xl)c(;-}ap=pPVuVUvWG72nGU# zzR$Jzzy0l@$TCiaEm2p){+2E_%d0s&lPgh|<@?HYk?Gec}#nd^kg++O;pqSy3e&%0eU>Ka2?eI{8!YGNM3O(>R%ywrY z3;yQuD^k7dl6>^@7AbxyZMcwyJsX-{<~WEfGtHrQ2uGL4Y`&rR_Ls6>W+UUi9P*XK z%z%bf6XEa2{?a*Q=B{`hDvyk&F{-Rlbm12x1149$1qE(xGf>mx=)ujN+(oAaL7Ex- zr)t2o{O%=6xV#kpPFGQ_CgW5)}v0KA~qcdP?z~)lePyX zd1R?J@M8bW;Io-Z=NE*0X;!xTDw*!J|81>OB7&5epck+ke~eFaiiY-?HiJP+R0y#x znL01Vh>2zsO0EJ+oE-7P==9+VXf-?hMzO_;G$e_Xmtb~sAJ`MK;pj>qy~9%J+5xG_ zuKJicszKMHg&arTlr1^lvj4gK|qdrij(B))~(x5zk<0NQUXY3$(qOQ241D*`E zu;@zL!pg%t?n_L;ENc|KxRsoTnRcsMl)N=+#Jfd0iQnO@gU+@@u9T(deXVEhU-pk{qM`<9h zfpDnCiiG7hKnc>?G>b|y625YSPajI5QDf8xVBS#d^myGD!{5K&rjAA&s)HKUi*}lM zqhK&naEUIXJn6RnoOB69TU^v>nLoptwkLnRR40~ztYXG3}9~oxIG*@S@g4dV)L!gJ0G2HxD{6!Q(*XNu|jhTqtA@-$46TEo1 zjCjUW<*}$q)KQ@x!sN)Io6Pa@arw1fV`A=pi zwMCQmgykP#%NhO9?x3GQ0nSHwoaVpqAjS(A9Ylph@Ea}jPbWpJ=|Wj|9kUs~U&yCf zD}iIju~#uB`Hrrmja6WHuGyDg3K{(U@C1E3AgoZ3KO$$Ql;$B_boJ+sZv0qM#nXYY zne9AE#nXh;Y$v^wm1nK8QDz`Od#QdMezKq?BPIA)-dgo13}}8YPlo^;8Hn<4Hq>W+ zY74OYak0^8tARO8#fP;tOeMdJ=Y_e`V46iwNNdxwn`!bzRMQ^~irJw;Y=w1PGAkv1 zcXSTk8$By-dx!^);g6OOU-Kp*gXk zh!w4`&%;6xEA3qTs+N>P%QDS(cn}2Iq+u}pAlgCC@-!shv8Z&l6IUA2X)?UBCQXQT zi){&lWKW{C<;{hzefCo{70v63*xjZlE%;l_nT`wSv3%FT1~PHDx|@VOp-m~t`ZE@+ zwVsFBt)T`Pp5~{KSny<8B8zpB951qw@WgZ}fx5E$;&PZ-puv${}w3(Ssu)Kgj~!4E|KoggoPGM_DMqFG6qHw)Wa$R+2u+!Cl?fk zfR+NBqroYe5rd)a?&%X4Yq(DJz&A4leeO*UmvA<7R ztOFSiG|gbEC=_`JzXmq>$jgH!<-0gDO`alsHwjgpb~hkZliXRqVvK zC$d2-$%XCEKCyLJtE;Ma5q&8Bkr>Q!W~&+X!XtaokW2mi$z;Lh!1C3>cH{_f6n>xa zvfu`<$vv#`)1$@yNMi8iJGMvTKa1H_X$D?0w!$FxIM3-x{)KItaIN;lj?Z~Y?hB*I z8()almr`|pFI8%)XNx*8U(m%=zO^U4*J~1Fz36^RI%;K*X;jXla&%^zVIpYOkubv{Ad|^$UZox^*Ozu^}lj zY$_UnkS~nFLu@e^l5YFlqH=nwOkEePpe!)V{GQ$^UDCXFw`lmXYSNUdI7m zP`Nr_hvsGzpBzrJdHTT(g`x4%L|L;WaQ?(f=>5BXYJS-RDk0U%_C&q`P`_ zpC&p{i?LlZtmIQ2BaCs<9P2tSN+5=cyZ?@=vP6vCzT@6miyGKMiRt=O80kISe)WX_VFvqrrwlLhCbcvHG@VPItfR0 z%SNr+6{)u#ygJS}WbGH5%JNshZURX#I5VtElUI2^q<1=Pjg$-6AyPA*6x)0(-s2u+ z#lay4!ymmz>gRk0frq|ku|*ox+4UDa~>9}kd!S+?!C);D2n z>FTlPLMECaVX-nOgw5acy>g1PY;Y2sqcds=XH>;g9k!)Gb~tVTbg@|5ep5__&|uf4 z9f3nveHyxA2?&-Losj5p)%ObI;plbyr+?$0BIZADD+I7H!{yE(bkrW?{T1u{F=U8q z_Mtd6p?AV9+e2iAV(mkwsTHg&0$F_!J()>Q@=+G@t_LCNYpJ>rIVSlBWj6Y?NLn#% znj}btE$3m4sc;?vV@@9>5W0=>QEi=g>|Kp#T>@hE562u;|{3w`w_QWA@WtM&H= zfA8*E@0sqH{O+6~L(m#^#ur*b_3lEQSWw7OWf6JMI9Q@}q_;n^n0tMhFU~gj$IgFr zwqA~YYqZd1%aCta8#6VZm*wn(!_~{vX|`5(l+WZCqmXf{sjQ^$li@A><}6F+qf!F! z28s;3p|}R|iP1Auir>n9#loQRh&0=6EPLLZZLCBd9_IRM{96*t5dgNV>3O}=rOufV zdX{JGeYsK;;Qw70jEQjT`)3F@n@8SPqNLbowOW_EzPoHDn;gvXxzZE(M zr>eU7>P~4OaESA}JJoos`EQoZarm3FCxDUf2Q1Iin^0lM;ZQSGTP< z+fW~{`d+MA{yI8e`032?Ksk3cCG=C;QdcaGZxl| zP|WmGr`_QUit=gI5_yb2IRO4$ekqgD0ESR>%~^C_^mJ;ijFu9O}E#Ba$jRue9r1wl(R+BGGw<3GM0y|zZgOe zEo+?QVcbA@l3WuIi7gR3J2){0AFD zwg74(zrSrex#RK#AZJuwbc_85GHn3J7r@WI%O_B`>nWZA6GDc%^;E@|g zo#DYt!4;P&&a_NEH)|!YLX!DHskopJiobuKFUU=C+@Ar0A=;z1>uUTLm{&zp7QAK> zCpg#HGKbQ1hbeT2ZwCR&yX6 z9i8LHoKVAg$0;4`8qWi;Q5Oxo&(uZaunZHQlRgP){{W;Mu5G>5VOv_Vki_}z^&Xd^ z2u!DjREI^1;e9>Dofg!hZNMCX#Z=yZt@9pY;CrjCEE_&8pGuZ_8}Z9t3trbWK?3QC zRt%F)nxN}uUJlMvDGLV%8v{Z@!>vz`JzV+HYEiKnA7i5d4&<^7(Xg!HXviB*ipvC;r$+UuA+0tQ0fx67t!1@mH;KwjCUegN+3b7BTbk8d zN;(z9$!SK#?7c|5xoxbq59POcc32uJm!Ao#ndyUgmRJy_47t&#+f$4A5>Z{UH}eBf1Cj0HbCMGLVv5Q zYLNX^Y%;$8<~1)vtxcU6rr{zf-UcYpGUY-^&P2vF0^>@vJY}%)fdx2pCQkTon@rY; zt*(vF{SSB5og_AOS#Z8;h$p!|`O+|yWQK~t^;frj9_%uO1ye`IVG&w1rN_|g)tcSQ z)vawn$t~XwI7&6`*Y3IKKF1OXb_AfvEUju2cw!@CV?$7!{LwWYiE9_&tL)VE-yWL* zJ3$3Cuqa~lEneECq}DWH1&HtuhBtO ze3-g8THMv1PL_|1zIv~~u>k40eq(?OLnWa4bp|N)Im>!opY!&^#pBXkA_l0M?z?sC zB|#xnG3c+IAOxg0U-Q=NSGI3RkAR{ z`*TZt0pnR^_EFxV>dyJATJAquqX>5+mBr|Ru6&49$COKZl5|HKAowH*i>;{%PUBZ> z2HpHCUmm2I8Hb_hR%pJetyh}Y4VuT#1~MnVj^FGiYfhHQEkcfYxnfV#D-lVoQ@|sAN1phOFkDhx8aIdfgc?_V|ZJHT3>obl?OLnd?HRnr0Y^Sm>>wJhBwLRI`` z)2J6cF6av=T8VYM%YoSf*n~~2;*!{Trp9Q^RYk}~_z@eeuIPX*G#4YR-0XV+fFb*w z8)dy#)a5kukRiRTGvB9f`~q;~*iPAw-{0++N*K!BNaT8oLTH0NNj`oz_xg@@~4oDlvP;sj``)!ezKlVLXQyHoTm(2i0QJr3V4Uw z{u;dMJi&skzdQY4j)pwx1gT`)5Zy(l>i5Ei-kVtwnQ2KCibc1%+be z-`~GPEbDgM2Suo??nrleo;9!7{-B$4$;!8MZRf>kAj4#JN<4uqp3q<+k^yJDzNk3` z9j&twmi7Ztu$<5=4e5RafikTqMI=W0lr~^z zC!Vse$Z0pR#jOj9{MC6e{hvsvIZ*bY1;P+5zF~vkOaLv*T!t){WF-inIICeNN+JBn z_)+Aye-p`k2U3vqDWH95a3K~3#mOd(V-d)osTn=5V~RMT_dChO%pdrR&tH+$Sn$G3 zUP|Phj+e2`cDQKQOzY+^--`RBZn6O8PqO~ekQXXH@D(}!K@Kh`N#?X{(d-{*g5AyT zzXX@??o;JU+B4@MW_jIn%@=}_-(4m7dI7S;GlEo-#ULU|d9s3CvU`W_eVih#9OK{7 z2NGJJ8f89WC>FN0S)f;%T0Md=J|3uKoOVQ*r0il@rMd(T+UB%eKC;a62$V>3KA>0Q{JmpfNG z$rLMd6}Dg$VV8xWQcF=2YV0#)ps5eWbFUruZ3w(eFMs*Vo-DKGsUY6oXGt?hQnGf8 z(-A>FAVk08XJ3;PX5DIq_6|w?YZ@{}T`{NskqmwZ|D?@l{EUu}85jgK6l(g@W@63L zHF^n+Vui~lh8DpGYBo}o*7EjYmSs3w{Xkr;`bv_Yki5@9w&O^-c$IcKT*}}_z`U2Z zS1u?Vs!l7!Pe!?%ZAd;vP^T3g3@2Njc=R%-wVFI~M4<_}u}Do@GS>BD4T?BE`f$!# z#)gdO@X7A^=jv@j-6j+FaZ`(O3tRD_IyS`lt>Pt}z6t!fpi)DNyr8gMg^PnmL5{Z% zk*p-QZh=1ag~5LqYBz7n9?@sgDCsW=m3;avjR1kpWCrn6w)@sG1%l|tT_H5O^(94C zMkq3=R;uX+_I)AJ?&z=exXN2S(;51#_(-nU;(ov%;?-6(QW0o4QZ4Zhok4{8I>*Ff z5cu_);Y>u6FfUSE`inxtyBsSRWiTmg#hWAC_m#mI1lmRkv4vl(qVaSLP6Nl^SN2zh z*44(5$rSK2SZ0*vMxeqz?+CQm1c&#prIrxR*y*A`;1V3yAoON3ktC`RIm$dVWkT&r zDes^@h*Gy>&JpK?kd--X*e978bxg<=?-?Jz%7&%u{C7~`WCiB0?1p?O9V`wr6lKxD zCiWz{--Qt;V=4&nD=0-F=ThQfb3Jw}+&p?Pm1?N$eOWJa?CTig=Ud(0b(_IdVIrEp zH%QkuGG2fSd(qAONk_+vP%VfG-q+v>yFNZ$gkM zoTR~L=sZTL5UO2M@QQU&&bh25e%5}N)1Mx7My!K5eu!_EYa@p6(MfVqLlhRjw&%&( zqPQqZc7~+s+fTRz-cMkwg%SlSU)7UZTXS6Q>(L5f%}?Mt1EO_lj{DoW6mFHX4ZUk< z)Y*coFEdF^FD>uLR6235Q98poNY0ei zX69O$?})Pcc@Pk2B9X|3QZW_cvh}~6C8~l0X@uHnetkaIW_E6GQI;%Mq7{X#hm(#Y zkCE}{2{UZj5g6m?y_oKP=^zB4wlWP)9v`Lkg`IW56A9?OwKkA=?B1)ii)ciVP6v}fufBPriMLP1Ovi(X`{uLo1 zN4+*1Iw9j6(LPdS%pTtx!Cv+6ro-k?xCmBH!F6;KHam}N%2@7=+!VVo>0{mv=K=*#1X|iy8ithX4Lm5NCp9pU>U_m$(_CdB{e6B|%e?5|J zP?N$gA}^SbNrvOF8po3N`N&aNMdb;?Ll`M2_=WU>k|xum%l$VW2E2u9#tDnF5#33x zYV=+Qy4YUJU&Rjd9hvE%ev&U{QOuw3A%9ePfJ~3Uq=#Qvp-ve7cKJ(U zc0K^T&i!Ib^lLJJ6~i|o3DLm+Bog$3)wRb|XjqLa-oKQo1a4HSwf9fh^h3pE9B>iT zJZ}a_sGSd_xIq@flb;S3ycpp#QEG14w-Sa!3x8$mbDjHs4LMx)f+`y> zn`PIhBF>F|4q_9{e#8904b2*d`5!)qXqJIgaCSsGCwI}90XP7j#M+ZeU>jE@VgHrKRxeesY zukL)PR%7B@UdAJ5L@122MR`-=(K`Vgv68uuPR&Cc?k7g2s! z7ic0xliszw$spF$RrD^cj>LuF^WeVn6nVe(7oC|488SMN2+I&0TJIDX=o)LM;x&Ea zlKi2vKVndQ)!SRdiM?$DCBBhL0XKglum^|GcEX`}$_CcK1+WHBOJ2hG;kkc{)bj04 zQ5Targ2R#N$w~VDZ?Me9luwV&FA=|&T{>=C3%fEtLkNrIWyEo<;O8*=(#50bW~gI_ zb-$*N2ckW=@L{-Vz8Kw`s<|eS|7X2L``=~`d0BLwNeqccQJ!N+n!J-FDen7!(cS;S zU)Ow-vGaHeH89AiuqjK^k%hOF;$U*L-b@zNo@ye`OzlGp?K96v)@GX{3Oo!r6ba=4 zju#}>b=+NEC0}51jSf6ghF7Uztf zh%wNp$SPRU^7be1PhJ)aM*!P#k>r2Y@j|l{HOag)YFF9s6_O{2{VTN*rMCj^+(mfZ zW&Tc1MQ;F~!`Wf`Q5aSSIuqy!Vl|Nff&jt+3Yv$g>9nPVq1+5q_j