diff --git a/Aztec/Classes/Extensions/NSAttributedString+Attachments.swift b/Aztec/Classes/Extensions/NSAttributedString+Attachments.swift index bcf8c5960..9dfa8ce4b 100644 --- a/Aztec/Classes/Extensions/NSAttributedString+Attachments.swift +++ b/Aztec/Classes/Extensions/NSAttributedString+Attachments.swift @@ -2,6 +2,15 @@ import Foundation import UIKit +// MARK: - Constants +// +extension String { + /// String containing the NSTextAttachment Character + /// + static let textAttachment = String(UnicodeScalar(NSAttachmentCharacter)!) +} + + // MARK: - NSAttributedString Extension for Attachments // extension NSAttributedString @@ -11,11 +20,6 @@ extension NSAttributedString static let lengthOfTextAttachment = NSAttributedString(attachment: NSTextAttachment()).length - /// String containing the NSTextAttachment Character - /// - static let textAttachmentString = String(UnicodeScalar(NSAttachmentCharacter)!) - - /// Helper Initializer: returns an Attributed String, with the specified attachment, styled with a given /// collection of attributes. @@ -24,7 +28,7 @@ extension NSAttributedString var attributesWithAttachment = attributes attributesWithAttachment[.attachment] = attachment - self.init(string: NSAttributedString.textAttachmentString, attributes: attributesWithAttachment) + self.init(string: .textAttachment, attributes: attributesWithAttachment) } /// Loads any NSTextAttachment's lazy file reference, into a UIImage instance, in memory. diff --git a/Aztec/Classes/Libxml2/DOM/Data/ElementNode.swift b/Aztec/Classes/Libxml2/DOM/Data/ElementNode.swift index 04b9ee766..41f1f1a69 100644 --- a/Aztec/Classes/Libxml2/DOM/Data/ElementNode.swift +++ b/Aztec/Classes/Libxml2/DOM/Data/ElementNode.swift @@ -234,6 +234,32 @@ public class ElementNode: Node { } + /// If there's exactly just one child node, this method will return it's instance. Otherwise, nil will be returned + /// + func onlyChild() -> ElementNode? { + guard children.count == 1 else { + return nil + } + + return children.first as? ElementNode + } + + + /// Returns the child ElementNode of the specified nodeType -whenever there's a *single* child-, or nil otherwise. + /// + /// - Parameter type: Type of the 'single child' node to be retrieved. + /// + /// - Returns: the requested child (if it's the only children in the collection, and if the type matches), or nil otherwise. + /// + func onlyChild(ofType type: StandardElementType) -> ElementNode? { + guard let child = onlyChild(), child.isNodeType(type) else { + return nil + } + + return child + } + + /// Indicates whether the children of the specified node can be merged in, or not. /// /// - Parameters: diff --git a/Aztec/Classes/NSAttributedString/Conversions/AttributedStringSerializer.swift b/Aztec/Classes/NSAttributedString/Conversions/AttributedStringSerializer.swift index 6c7ed61da..8b7a5cc0c 100644 --- a/Aztec/Classes/NSAttributedString/Conversions/AttributedStringSerializer.swift +++ b/Aztec/Classes/NSAttributedString/Conversions/AttributedStringSerializer.swift @@ -358,9 +358,10 @@ extension AttributedStringSerializer { } } -private extension AttributedStringSerializer { - // MARK: - Implicit Representations +// MARK: - Implicit Representations +// +private extension AttributedStringSerializer { /// Some elements have an implicit representation that doesn't really follow the standard /// conversion logic. This method takes care of such specialized conversions. @@ -376,23 +377,15 @@ private extension AttributedStringSerializer { guard let elementType = element.standardName else { return nil } - - if let imgElement = linkedImageElement(for: element) { - var attributesWithoutLink = attributes - attributesWithoutLink[.link] = nil - attributesWithoutLink[.linkHtmlRepresentation] = nil - - let imgAttributes = self.attributes(for: imgElement, inheriting: attributesWithoutLink) - let attachment = imgAttributes[.attachment] as! ImageAttachment - let linkText = element.stringValueForAttribute(named: HTMLLinkAttribute.Href.rawValue) ?? "" - attachment.linkURL = URL(string: linkText) - - return implicitRepresentation(for: imgElement, inheriting: imgAttributes)! + + if let linkedImageAttributes = linkedImageAttributes(for: element, inheriting: attributes) { + return implicitRepresentation(for: .img, inheriting: linkedImageAttributes) } return implicitRepresentation(for: elementType, inheriting: attributes) } + /// Some element types have an implicit representation that doesn't really follow the standard /// conversion logic. This method takes care of such specialized conversions. /// @@ -406,33 +399,47 @@ private extension AttributedStringSerializer { switch elementType { case .hr, .img, .video: - return NSAttributedString(string: String(UnicodeScalar(NSAttachmentCharacter)!), attributes: attributes) + return NSAttributedString(string: .textAttachment, attributes: attributes) case .br: return NSAttributedString(.lineSeparator, attributes: attributes) default: return nil } } - - /// Checks if the specified node is of type '.a' with one child '.img' node. - /// If true, returns the '.img' node. + + + /// Whenever the `element`'s nodeType is `a` (link!), and there's a single child of the `.img` type, this method will return the + /// NSAttributedString attributes representing the 'Linked Image' Element. /// /// - Parameters: - /// - element: the node to get the information from + /// - element: The container element. + /// - attributes: NSAttributedString attributes, to be inherited. /// - /// - Returns: the child '.img' node if there is one + /// - Returns: The collection of 'Inherited Attributes' with it's internal ImageAttachment modified, so that it carries the 'linkElement' + /// target URL. /// - private func linkedImageElement(for element: ElementNode) -> ElementNode? { - guard element.isNodeType(.a) && element.children.count == 1, - let imgElement = element.children.first as? ElementNode, - imgElement.isNodeType(.img) else { - return nil + private func linkedImageAttributes(for element: ElementNode, inheriting attributes: [AttributedStringKey: Any]) -> [AttributedStringKey: Any]? { + guard element.isNodeType(.a), + let imgElement = element.onlyChild(ofType: .img) + else { + return nil } - - return imgElement + + var attributesWithoutLink = attributes + attributesWithoutLink[.link] = nil + attributesWithoutLink[.linkHtmlRepresentation] = nil + + let imgAttributes = self.attributes(for: imgElement, inheriting: attributesWithoutLink) + let linkText = element.stringValueForAttribute(named: HTMLLinkAttribute.Href.rawValue) ?? "" + + let attachment = imgAttributes[.attachment] as! ImageAttachment + attachment.linkURL = URL(string: linkText) + + return imgAttributes } } - + + // MARK: - Text Sanitization for Rendering private extension AttributedStringSerializer { diff --git a/AztecTests/HTML/Nodes/ElementNodeTests.swift b/AztecTests/HTML/Nodes/ElementNodeTests.swift index 3c2538069..62e0f3109 100644 --- a/AztecTests/HTML/Nodes/ElementNodeTests.swift +++ b/AztecTests/HTML/Nodes/ElementNodeTests.swift @@ -22,6 +22,7 @@ class ElementNodeTests: XCTestCase { XCTAssert(style1 == style1) } + /// Verifies that two different ElementNode(s) instances, with the same name and the exact same Children array /// return true when equality is checked. /// @@ -37,4 +38,34 @@ class ElementNodeTests: XCTestCase { XCTAssert(style1 == style2) XCTAssert(style1 !== style2) } + + + /// Verifies that `onlyChild` returns the receiver's only child, if it's type matches with the specified one. + /// + func testOnlyChildReturnsSingleChildrenIfItRepresentsAnImage() { + let image = ElementNode(type: .img) + let parent = ElementNode(type: .a, attributes: [], children: [image]) + + XCTAssertEqual(parent.onlyChild(ofType: .img), image) + } + + + /// Verifies that `onlyChild` returns nil, if there is more than one children, no matter if their type match with the specified one. + /// + func testOnlyChildReturnsNilIfThereIsMoreThanOneChild() { + let image = ElementNode(type: .img) + let parent = ElementNode(type: .a, attributes: [], children: [image, image]) + + XCTAssertNil(parent.onlyChild(ofType: .img)) + } + + + /// Verifies that `onlyChild` returns nil, if there is at least one child, but with different type. + /// + func testOnlyChildReturnsNilIfThereIsNoMatchingChild() { + let image = ElementNode(type: .img) + let parent = ElementNode(type: .a, attributes: [], children: [image]) + + XCTAssertNil(parent.onlyChild(ofType: .b)) + } }