[2]: Editor Rule - GeekTree0101/VEditorKit GitHub Wiki

Intro

VEditorKit Text Styling built on BonMot https://github.com/Raizlabs/BonMot It is really beautiful, easy attributed strings 3rd-party library in Swift

VEditorKit의 텍스트 스타일을 다루는 모든 기능 및 요소에 대해서는 BonMot을 기반으로 개발되었습니다.

Principle

  • Input: Parse XMLString with Editor Rule
 <contnet><p>hello world</p><img src="....."/><p>end</p></content>
  • Output: Array of VEditorContent
[NSAttributedString, VEditorMediaContent, NSAttributedString]

VEditorKit offers 3 kind of content

  • NSAttributedString
  • VEditorMediaContent
  • VEditorPlaceholderContent
extension NSAttributedString: VEditorContent { }

public protocol VEditorMediaContent: VEditorContent {
    
    var xmlTag: String { get }
    init(_ xmlTag: String, attributes: [String: String])
    func parseAttributeToXML() -> [String: String] // parseto xml attribute from media content
}

public struct VEditorPlaceholderContent: VEditorContent {
    public var xmlTag: String
    public var model: Any
}

Rule

// MARK: - VEditorKit Editor Rule
public protocol VEditorRule {
    
    var allXML: [String] { get }
    var defaultStyleXMLTag: String { get }
    var linkStyleXMLTag: String? { get }
    func paragraphStyle(_ xmlTag: String, attributes: [String: String]) -> VEditorStyle?
    func build(_ xmlTag: String, attributes: [String: String]) -> VEditorMediaContent?
    func parseAttributeToXML(_ xmlTag: String, attributes: [NSAttributedString.Key: Any]) -> [String: String]?
    
    func enableTypingXMLs(_ inActiveXML: String) -> [String]?
    func disableTypingXMLs(_ activeXML: String) -> [String]?
    func inactiveTypingXMLs(_ activeXML: String) -> [String]?
    func activeTypingXMLs(_ inactiveXML: String) -> [String]?
}

Setting XML Tag name identifier

XML Tag를 정의합니다.

  • allXML: You have to return all of xml-tag cases (XML Builder와 Parser에 필요한 모든 XML Tag를 반환하세요.)
  • defaultStyleXMLTag: Default text style attribute (기본 텍스트 스타일에 해당하는 XML Tag값을 반환합니다.)
  • linkStyleXMLTag: Link(Article, ) XML Tag (기본 Link 스타일에 해당하는 XML Tag값을 반환합니다.)

XML Build & Parse, UITextView TypingAttributes Convenience generator methods

XML을 Build하거나 Parse할 때, 텍스트를 입력할 때 필요한 모든 Attributes를 제공 및 가공합니다.

  • paragraphStyle: make text text style attribute with xml-tag attributes base on xml-tag
  • build: make media content object(such as video, image, opengraph and so on) with xml-tag attributes base on xml-tag
  • parseAttributeToXML: Make xml-tag attributes with NSAttributedString attributes

Typing Control methods

  • enableTypingXMLs: return enableTypingXMLs base on inactiveXML (TypingControl will be enabled)
  • disableTypingXMLs: return disableTypingXMLs base on activeXML (TypingControl will be disabled)
  • inactiveTypingXMLs: return inactiveTypingXMLs base on activeXML (TypingControl will be non-selected)
  • activeTypingXMLs: return activeTypingXMLs base on inactiveXML (TypingControl will be selected)

Example

struct EditorRule: VEditorRule {
    
    // Convenience XML CaseIterable Enum
    enum XML: String, CaseIterable {
        
        case article = "a"
        case paragraph = "p"
        case bold = "b"
        case italic = "i"
        case heading = "h2"
        case quote = "blockquote"
        case image = "img"
        case video = "video"
        case opengraph = "og-object"
    }
    
    var defaultStyleXMLTag: String {
        return XML.paragraph.rawValue
    }
    
    var linkStyleXMLTag: String? {
        return XML.article.rawValue
    }
    
    var allXML: [String] {
        return XML.allCases.map({ $0.rawValue })
    }
    
    func paragraphStyle(_ xmlTag: String, attributes: [String : String]) -> VEditorStyle? {
        guard let xml = XML.init(rawValue: xmlTag) else { return nil }
        
        switch xml {
        case .paragraph:
            return .init([.font(UIFont.systemFont(ofSize: 16)),
                          .minimumLineHeight(26.0),
                          .color(.black)])
        case .bold:
            return .init([.emphasis(.bold),
                          .font(UIFont.systemFont(ofSize: 16)),
                          .minimumLineHeight(26.0),
                          .color(.black)])
        case .italic:
            return .init([.emphasis(.italic),
                          .font(UIFont.systemFont(ofSize: 16)),
                          .minimumLineHeight(26.0),
                          .color(.black)])
        case .heading:
            return .init([.font(UIFont.systemFont(ofSize: 30, weight: .medium)),
                          .minimumLineHeight(40.0),
                          .color(.black)])
        case .quote:
            return .init([.font(UIFont.systemFont(ofSize: 20)),
                          .color(.gray),
                          .firstLineHeadIndent(19.0),
                          .minimumLineHeight(30.0),
                          .headIndent(19.0)])
        case .article:
            let style: VEditorStyle = .init([.font(UIFont.systemFont(ofSize: 16)),
                                             .minimumLineHeight(26.0),
                                             .color(.black),
                                             .underline(.single, .black)])
            if let url = URL(string: attributes["href"] ?? "") {
                return style.byAdding([.link(url)])
            } else {
                return style
            }
        
        default:
            return nil
        }
    }
    
    func build(_ xmlTag: String, attributes: [String : String]) -> VEditorMediaContent? {
        guard let xml = XML.init(rawValue: xmlTag) else { return nil }
        
        switch xml {
        case .image:
            return VImageContent(xmlTag, attributes: attributes)
        case .video:
            return VVideoContent(xmlTag, attributes: attributes)
        case .opengraph:
            return VOpenGraphContent(xmlTag, attributes: attributes)
        default:
            return nil
        }
    }
    
    func parseAttributeToXML(_ xmlTag: String,
                             attributes: [NSAttributedString.Key : Any]) -> [String : String]? {
        guard let xml = XML.init(rawValue: xmlTag) else { return nil }
        
        switch xml {
        case .article:
            if let url = attributes[.link] as? URL,
                case let urlString = url.absoluteString {
                return ["href": urlString]
            } else {
                return nil
            }
        default:
            return nil
        }
    }
    
    func enableTypingXMLs(_ inActiveXML: String) -> [String]? {
        guard let xml = XML.init(rawValue: inActiveXML) else { return nil }
        
        switch xml {
        case .heading, .quote:
            return [XML.bold.rawValue,
                    XML.italic.rawValue,
                    XML.paragraph.rawValue]
        default:
            return nil
        }
    }
    
    func disableTypingXMLs(_ activeXML: String) -> [String]? {
        guard let xml = XML.init(rawValue: activeXML) else { return nil }
        
        switch xml {
        case .heading, .quote:
            return [XML.bold.rawValue,
                    XML.italic.rawValue,
                    XML.paragraph.rawValue]
        default:
            return nil
        }
    }
    
    func inactiveTypingXMLs(_ activeXML: String) -> [String]? {
        guard let xml = XML.init(rawValue: activeXML) else { return nil }
        
        switch xml {
        case .heading:
            return [XML.quote.rawValue]
        case .quote:
            return [XML.heading.rawValue]
        default:
            return nil
        }
    }
    
    func activeTypingXMLs(_ inactiveXML: String) -> [String]? {
        return nil
    }
}

VEditorMediaContent

<img src=\"https://rawImage.intro.png\" width=\"500\" height=\"500\"/>

Image xml has 3 kinds of attributes such as width, height, sourceURL(src) At first you have to make VEditorMediaContent inherited content for image

class VImageContent: VEditorMediaContent {
    
    var xmlTag: String
    
    var url: URL?
    var width: CGFloat
    var height: CGFloat
    
    var ratio: CGFloat {
        return height / width
    }
    
    required init(_ xmlTag: String, attributes: [String : String]) {
        self.xmlTag = xmlTag
        self.url = URL(string: attributes["src"] ?? "")
        self.width = CGFloat(Int(attributes["width"] ?? "") ?? 1)
        self.height = CGFloat(Int(attributes["height"] ?? "") ?? 1)
    }
    
    func parseAttributeToXML() -> [String : String] {
        return ["src": url?.absoluteString ?? "",
                "width": "\(Int(width))",
                "height": "\(Int(height))"]
    }
}

And you just define it on Editor Rule build methods

    func build(_ xmlTag: String, attributes: [String : String]) -> VEditorMediaContent? {
      
        switch xmlTag {
        case "img":
            return VImageContent(xmlTag, attributes: attributes)
        default: return nil
        }
    }
⚠️ **GitHub.com Fallback** ⚠️