diff --git a/Sources/BuilderIO/Components/BuilderImage.swift b/Sources/BuilderIO/Components/BuilderImage.swift index 87f5f8c..385f7ce 100644 --- a/Sources/BuilderIO/Components/BuilderImage.swift +++ b/Sources/BuilderIO/Components/BuilderImage.swift @@ -34,11 +34,6 @@ struct BuilderImage: BuilderViewProtocol { (block.component?.options?.dictionaryValue?["fitContent"]?.boolValue ?? false) && !(block.children?.isEmpty ?? true) - if let imageBinding = block.codeBindings(for: "image")?.stringValue { - self.imageURL = URL( - string: imageBinding) - } - } var body: some View { diff --git a/Sources/BuilderIO/Components/BuilderText.swift b/Sources/BuilderIO/Components/BuilderText.swift index 1b55038..88f07e5 100644 --- a/Sources/BuilderIO/Components/BuilderText.swift +++ b/Sources/BuilderIO/Components/BuilderText.swift @@ -18,9 +18,6 @@ struct BuilderText: BuilderViewProtocol { self.responsiveStyles = getFinalStyle(responsiveStyles: block.responsiveStyles) - if let textBinding = block.codeBindings(for: "text") { - self.text = textBinding.stringValue - } } var body: some View { diff --git a/Sources/BuilderIO/Components/BuilderVideo.swift b/Sources/BuilderIO/Components/BuilderVideo.swift index 760b660..50a31e6 100644 --- a/Sources/BuilderIO/Components/BuilderVideo.swift +++ b/Sources/BuilderIO/Components/BuilderVideo.swift @@ -36,9 +36,6 @@ struct BuilderVideo: BuilderViewProtocol { if let videoString = options?["video"]?.stringValue { self.videoURL = URL(string: videoString) } - if let videoBinding = block.codeBindings(for: "video")?.stringValue { - self.videoURL = URL(string: videoBinding) - } // Video options self.autoPlay = options?["autoPlay"]?.boolValue ?? false diff --git a/Sources/BuilderIO/Schemas/BuilderBlockModel.swift b/Sources/BuilderIO/Schemas/BuilderBlockModel.swift index fdc8ddd..bea8090 100644 --- a/Sources/BuilderIO/Schemas/BuilderBlockModel.swift +++ b/Sources/BuilderIO/Schemas/BuilderBlockModel.swift @@ -1,115 +1,221 @@ -import Foundation // For Codable, Data, etc. +import Foundation -// Schema for Builder blocks +// MARK: - Core Data Models + +/// Represents a single Builder.io block, conforming to Codable and Identifiable. public struct BuilderBlockModel: Codable, Identifiable { public var id: String - public var properties: [String: String]? = [:] - public var bindings: [String: String]? = [:] - public var children: [BuilderBlockModel]? = [] - public var component: BuilderBlockComponent? = nil - public var responsiveStyles: BuilderBlockResponsiveStyles? = BuilderBlockResponsiveStyles() // for inner style of the component - public var actions: AnyCodable? = nil - public var code: AnyCodable? = nil - public var meta: AnyCodable? = nil - public var linkUrl: String? = nil - public var `repeat`: [String: String]? = [:] - - public var stateBoundObjectModel: StateModel? = nil - public var stateRepeatCollectionKey: StateRepeatCollectionKey? = nil - - public var locale: String? = nil // Optional locale for the block + public var properties: [String: String]? + public var bindings: [String: String]? + public var children: [BuilderBlockModel]? + public var component: BuilderBlockComponent? + public var responsiveStyles: BuilderBlockResponsiveStyles + public var actions: AnyCodable? + public var code: AnyCodable? + public var meta: AnyCodable? + public var linkUrl: String? + public var `repeat`: [String: String]? + public var locale: String? + + // Internal state properties + var stateBoundObjectModel: StateModel? + var stateRepeatCollectionKey: StateRepeatCollectionKey? + + public init( + id: String = UUID().uuidString, + properties: [String: String]? = nil, + bindings: [String: String]? = nil, + children: [BuilderBlockModel]? = nil, + component: BuilderBlockComponent? = nil, + responsiveStyles: BuilderBlockResponsiveStyles = .init(), + actions: AnyCodable? = nil, + code: AnyCodable? = nil, + meta: AnyCodable? = nil, + linkUrl: String? = nil, + `repeat`: [String: String]? = nil, + locale: String? = nil + ) { + self.id = id + self.properties = properties + self.bindings = bindings + self.children = children + self.component = component + self.responsiveStyles = responsiveStyles + self.actions = actions + self.code = code + self.meta = meta + self.linkUrl = linkUrl + self.repeat = `repeat` + self.locale = locale + } +} +/// Represents component-specific data for a block. +public struct BuilderBlockComponent: Codable { + public var name: String + public var options: AnyCodable? } +/// Defines responsive style properties for a block. +public struct BuilderBlockResponsiveStyles: Codable { + public var large: [String: String]? + public var medium: [String: String]? + public var small: [String: String]? + + public init( + large: [String: String]? = nil, + medium: [String: String]? = nil, + small: [String: String]? = nil + ) { + self.large = large + self.medium = medium + self.small = small + } +} + +/// Stores information for repeating a block based on a collection. public struct StateRepeatCollectionKey: Codable { public var index: Int public var collection: String - } +// MARK: - Extension for Logic + extension BuilderBlockModel { - /// Recursively sets the `stateBoundObjectModel` for this block and all its children. + /// Recursively propagates state and applies bindings to this block and its children. public mutating func propagateStateBoundObjectModel( _ model: StateModel?, stateRepeatCollectionKey: StateRepeatCollectionKey? = nil ) { self.stateBoundObjectModel = model self.stateRepeatCollectionKey = stateRepeatCollectionKey - if var children = self.children { - for index in children.indices { - children[index].propagateStateBoundObjectModel( - model, stateRepeatCollectionKey: stateRepeatCollectionKey) - } - self.children = children - } + applyCodeBindings() + propagateStateToChildren(model, stateRepeatCollectionKey: stateRepeatCollectionKey) } - public func codeBindings(for key: String) -> AnyCodable? { - guard let code = code, - let codeConfig = code.dictionaryValue, - let bindingsConfig = codeConfig["bindings"], - let bindings = bindingsConfig.dictionaryValue - else { - return nil + /// Sets the locale for the current block and all its children recursively. + public mutating func setLocaleRecursively(_ newLocale: String) { + self.locale = newLocale + self.id = UUID().uuidString // Reset ID for uniqueness after a locale change + + guard var children = children else { return } + for index in children.indices { + children[index].setLocaleRecursively(newLocale) } + self.children = children + } +} - if let stateModel = stateBoundObjectModel { +// MARK: - Private Helpers + +extension BuilderBlockModel { + /// Applies code bindings to component options like 'text' and 'image'. + fileprivate mutating func applyCodeBindings() { + var options = component?.options?.dictionaryValue + var styling = responsiveStyles.small ?? [:] + + if var options = options { + if let bindingText = evaluateCodeBinding(for: "text") { + options["text"] = bindingText + } + + if let bindingImage = evaluateCodeBinding(for: "image") { + options["image"] = bindingImage + } + + if let bindingVideo = evaluateCodeBinding(for: "video") { + options["video"] = bindingVideo + } - //binding is in a list - if let stateRepeatCollectionKey = stateRepeatCollectionKey { - let collection = stateModel.getCollectionFromStateData( - keyString: stateRepeatCollectionKey.collection) + self.component?.options = AnyCodable.dictionary(options) + } - let model = collection?[stateRepeatCollectionKey.index].dictionaryValue + if let bindingColor = evaluateCodeBinding(for: "color") { + styling["color"] = bindingColor.stringValue + } - for (bindingKey, value) in bindings { - let lastComponent = bindingKey.split(separator: ".").last.map(String.init) - if key == lastComponent { - if let lookupKey = value.stringValue { + if let backgroundColor = evaluateCodeBinding(for: "backgroundColor") { + styling["backgroundColor"] = backgroundColor.stringValue + } - return model?[lookupKey.split(separator: ".").last.map(String.init) ?? ""] - } + if let borderColor = evaluateCodeBinding(for: "borderColor") { + styling["borderColor"] = borderColor.stringValue + } - } - } - } else { + self.responsiveStyles.small = styling - for (bindingKey, value) in bindings { - let lastComponent = bindingKey.split(separator: ".").last.map(String.init) - if key == lastComponent { - return stateModel.getValueFromStateData( - keyString: value.stringValue ?? "") - } + } - } - } + /// Recursively propagates the state model to child blocks. + fileprivate mutating func propagateStateToChildren( + _ model: StateModel?, stateRepeatCollectionKey: StateRepeatCollectionKey? + ) { + guard var children = children else { return } + for index in children.indices { + children[index].propagateStateBoundObjectModel( + model, stateRepeatCollectionKey: stateRepeatCollectionKey) + } + self.children = children + } + /// Evaluates a single code binding for a given key. + fileprivate func evaluateCodeBinding(for key: String) -> AnyCodable? { + guard let bindings = getBindings() else { return nil } + + if let stateRepeatCollectionKey = stateRepeatCollectionKey, + let stateModel = stateBoundObjectModel + { + return resolveBindingInRepeatCollection( + key: key, bindings: bindings, stateModel: stateModel, + stateRepeatCollectionKey: stateRepeatCollectionKey) + } else if let stateModel = stateBoundObjectModel { + return resolveBindingInStandardState(key: key, bindings: bindings, stateModel: stateModel) } return nil } - public mutating func setLocaleRecursively(_ newLocale: String) { - self.locale = newLocale - self.id = UUID().uuidString // Reset ID to ensure uniqueness after locale change - if let children = self.children { - var newChildren = children - for i in 0.. [String: AnyCodable]? { + return code?.dictionaryValue?["bindings"]?.dictionaryValue } -} + /// Resolves a binding within a repeated collection. + fileprivate func resolveBindingInRepeatCollection( + key: String, bindings: [String: AnyCodable], stateModel: StateModel, + stateRepeatCollectionKey: StateRepeatCollectionKey + ) -> AnyCodable? { + guard + let collection = stateModel.getCollectionFromStateData( + keyString: stateRepeatCollectionKey.collection), + stateRepeatCollectionKey.index < collection.count, + let model = collection[stateRepeatCollectionKey.index].dictionaryValue + else { + return nil + } -public struct BuilderBlockComponent: Codable { - public var name: String - public var options: AnyCodable? = nil // Replaced JSON? with AnyCodable?, default to nil -} + for (bindingKey, value) in bindings { + if bindingKey.split(separator: ".").last.map(String.init) == key, + let lookupKey = value.stringValue, + let finalKey = lookupKey.split(separator: ".").last.map(String.init) + { + return model[finalKey] + } + } + return nil + } -public struct BuilderBlockResponsiveStyles: Codable { - var large: [String: String]? = [:] - var medium: [String: String]? = [:] - var small: [String: String]? = [:] + /// Resolves a binding from the standard state model. + fileprivate func resolveBindingInStandardState( + key: String, bindings: [String: AnyCodable], stateModel: StateModel + ) -> AnyCodable? { + for (bindingKey, value) in bindings { + if bindingKey.split(separator: ".").last.map(String.init) == key, + let keyString = value.stringValue + { + return stateModel.getValueFromStateData(keyString: keyString) + } + } + return nil + } }