Image builders support

commit_hash:13f6ef6aa582c24314dac2d1f087dfcefabdd017
This commit is contained in:
booster
2026-06-03 13:11:16 +03:00
parent 36f4e952ec
commit 0dfd74c5b7
9 changed files with 610 additions and 36 deletions
+3
View File
@@ -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",
@@ -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
)
}
}
@@ -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)
}
}
}
+137 -26
View File
@@ -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<Bool> // 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<Bool>? = 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<DivText.ImageBuilder.Prototype> =
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<DivTextAlignmentVertical>?
@@ -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<Double> // 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<Double>? = 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] {
@@ -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<ImageTemplate>?
public let selector: Field<Expression<Bool>>? // 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<ImageTemplate>? = nil,
selector: Field<Expression<Bool>>? = nil
) {
self.image = image
self.selector = selector
}
private static func resolveOnlyLinks(context: TemplatesContext, parent: PrototypeTemplate?) -> DeserializationResult<DivText.ImageBuilder.Prototype> {
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<DivText.ImageBuilder.Prototype> {
if useOnlyLinks {
return resolveOnlyLinks(context: context, parent: parent)
}
var imageValue: DeserializationResult<DivText.Image> = .noValue
var selectorValue: DeserializationResult<Expression<Bool>> = { 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<Expression<[Any]>>?
public let dataElementName: Field<String>? // 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<Expression<[Any]>>? = nil,
dataElementName: Field<String>? = nil,
prototypes: Field<[PrototypeTemplate]>? = nil
) {
self.data = data
self.dataElementName = dataElementName
self.prototypes = prototypes
}
private static func resolveOnlyLinks(context: TemplatesContext, parent: ImageBuilderTemplate?) -> DeserializationResult<DivText.ImageBuilder> {
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<DivText.ImageBuilder> {
if useOnlyLinks {
return resolveOnlyLinks(context: context, parent: parent)
}
var dataValue: DeserializationResult<Expression<[Any]>> = { parent?.data?.value() ?? .noValue }()
var dataElementNameValue: DeserializationResult<String> = { 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<String>?
public let imageBuilder: Field<ImageBuilderTemplate>?
public let images: Field<[ImageTemplate]>?
public let layoutProvider: Field<DivLayoutProviderTemplate>?
public let letterSpacing: Field<Expression<Double>>? // 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<String>? = nil,
imageBuilder: Field<ImageBuilderTemplate>? = nil,
images: Field<[ImageTemplate]>? = nil,
layoutProvider: Field<DivLayoutProviderTemplate>? = nil,
letterSpacing: Field<Expression<Double>>? = 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<String> = { parent?.id?.value() ?? .noValue }()
var imageBuilderValue: DeserializationResult<DivText.ImageBuilder> = .noValue
var imagesValue: DeserializationResult<[DivText.Image]> = .noValue
var layoutProviderValue: DeserializationResult<DivLayoutProvider> = .noValue
var letterSpacingValue: DeserializationResult<Expression<Double>> = { 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,
Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

+48
View File
@@ -190,6 +190,48 @@
],
"$description": "translations.json#/div_text_range_builder"
},
"image_builder": {
"type": "object",
"properties": {
"data": {
"type": "array",
"$description": "translations.json#/div_text_image_builder_data"
},
"data_element_name": {
"type": "string",
"default_value": "it",
"supports_expressions": false,
"$description": "translations.json#/div_text_image_builder_data_element_name"
},
"prototypes": {
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"properties": {
"selector": {
"$ref": "common.json#/boolean_int",
"default_value": "true",
"$description": "translations.json#/div_text_image_builder_prototype_selector"
},
"image": {
"$ref": "#/definitions/image",
"$description": "translations.json#/div_text_image_builder_prototype_image"
}
},
"required": [
"image"
]
},
"$description": "translations.json#/div_text_image_builder_prototypes"
}
},
"required": [
"data",
"prototypes"
],
"$description": "translations.json#/div_text_image_builder"
},
"image": {
"type": "object",
"platforms": [
@@ -470,6 +512,12 @@
"web"
]
},
"image_builder": {
"$ref": "#/definitions/image_builder",
"platforms": [
"ios"
]
},
"ellipsis": {
"$ref": "#/definitions/ellipsis"
},
+24
View File
@@ -2551,6 +2551,30 @@
"en": "A condition that is used to select the prototype for the next character range. If there is more than 1 true condition, the earlier prototype is selected. If none of the conditions are met, the element from `data` is skipped.",
"ru": "Условие, которое используется для выбора прототипа для очередного диапазона символов. В случае, если будет больше 1 истинного условия, будет выбран прототип, который находится раньше. Если не выполнено ни одно условие, элемент из `data` будет пропущен."
},
"div_text_image_builder": {
"en": "Sets inline images dynamically using `data` and `prototypes`.",
"ru": "Задает встроенные изображения динамически при помощи `data` и `prototypes`."
},
"div_text_image_builder_data": {
"en": "Data that will be used to create inline images.",
"ru": "Данные, которые будут использоваться для создания встроенных изображений."
},
"div_text_image_builder_data_element_name": {
"en": "Name for accessing the next `data` element in the prototype. Working with this element is the same as with dictionaries.",
"ru": "Имя для обращения к очередному элементу `data` в прототипе. Работа с этим элементом ведется так же, как со словарями."
},
"div_text_image_builder_prototypes": {
"en": "Array of `image` elements from which the inline images will be created.",
"ru": "Массив `image`, из которых будут созданы встроенные изображения."
},
"div_text_image_builder_prototype_image": {
"en": "`image` from which the inline image will be created. In `image`, you can use expressions using data from `data`. To access the next `data` element, you need to use the same name as in `data_element_name`.",
"ru": "`image`, из которого будет создано встроенное изображение. В `image` можно использовать выражения с использованием данных из `data`. Для обращения к очередному элементу `data` нужно использовать такое же имя, как и в `data_element_name`."
},
"div_text_image_builder_prototype_selector": {
"en": "A condition that is used to select the prototype for the next inline image. If there is more than 1 true condition, the earlier prototype is selected. If none of the conditions are met, the element from `data` is skipped.",
"ru": "Условие, которое используется для выбора прототипа для очередного встроенного изображения. В случае, если будет больше 1 истинного условия, будет выбран прототип, который находится раньше. Если не выполнено ни одно условие, элемент из `data` будет пропущен."
},
"div_text_range_actions": {
"en": "Action when clicking on text.",
"ru": "Действие при нажатии на текст."
@@ -0,0 +1,70 @@
{
"description": "Text with image_builder: inline images generated from a data array, with selector-based prototype choice.",
"platforms": [
"ios"
],
"card": {
"log_id": "snapshot_test_card",
"states": [
{
"state_id": 0,
"div": {
"type": "container",
"width": {
"type": "wrap_content"
},
"background": [
{
"type": "solid",
"color": "#FFF"
}
],
"items": [
{
"variables": [
{
"name": "source",
"type": "array",
"value": [
{ "pos": 2, "kind": "chess" },
{ "pos": 5, "kind": "star" },
{ "pos": 8, "kind": "chess" }
]
}
],
"type": "text",
"text": "0123456789",
"font_size": 20,
"width": {
"type": "wrap_content"
},
"image_builder": {
"data": "@{source}",
"data_element_name": "it",
"prototypes": [
{
"selector": "@{getStringFromDict(it, 'kind') == 'star'}",
"image": {
"start": "@{getIntegerFromDict(it, 'pos')}",
"url": "https://yastatic.net/s3/home/divkit/star.png",
"width": { "type": "fixed", "value": 24 },
"height": { "type": "fixed", "value": 24 }
}
},
{
"image": {
"start": "@{getIntegerFromDict(it, 'pos')}",
"url": "https://yastatic.net/s3/home/divkit/chess.png",
"width": { "type": "fixed", "value": 24 },
"height": { "type": "fixed", "value": 18 }
}
}
]
}
}
]
}
}
]
}
}