diff --git a/Lexical/Core/Constants.swift b/Lexical/Core/Constants.swift index d3997431..524bfcfd 100644 --- a/Lexical/Core/Constants.swift +++ b/Lexical/Core/Constants.swift @@ -70,6 +70,7 @@ public enum DirtyType { case fullReconcile } +@available(*, deprecated, message: "Use new styles system") @objc public enum TextFormatType: Int { case bold case italic diff --git a/Lexical/Core/Editor.swift b/Lexical/Core/Editor.swift index 4e2e9e3c..f41aa23f 100644 --- a/Lexical/Core/Editor.swift +++ b/Lexical/Core/Editor.swift @@ -94,6 +94,9 @@ public class Editor: NSObject { internal var nodeTransforms: [NodeType: [(Int, NodeTransform)]] = [:] + // Styles. For all methods to manipulate these, see Styles.swift + internal var registeredStyles: StylesRegistrationDict = [:] + // Used to help co-ordinate selection and events internal var compositionKey: NodeKey? public var dirtyType: DirtyType = .noDirtyNodes // TODO: I made this public to work around an issue in playground. @amyworrall @@ -161,6 +164,7 @@ public class Editor: NSObject { public static func createHeadless(editorConfig: EditorConfig) -> Editor { let editor = Editor(editorConfig: editorConfig) editor.headless = true + registerRichText(editor: editor) return editor } @@ -398,7 +402,7 @@ public class Editor: NSObject { if selection != nil { try paragraph.select(anchorOffset: nil, focusOffset: nil) if let selection = selection as? RangeSelection { - selection.clearFormat() + selection.clearStoredStyles() } } } diff --git a/Lexical/Core/EditorState.swift b/Lexical/Core/EditorState.swift index c75dd3d6..7bdc8043 100644 --- a/Lexical/Core/EditorState.swift +++ b/Lexical/Core/EditorState.swift @@ -102,19 +102,17 @@ public class EditorState: NSObject { The JSON string is designed to be interoperable with Lexical JavaScript (subject to the individual node classes using matching keys). */ public func toJSON() throws -> String { - let string: String? = try read { - guard let rootNode = getRootNode() else { - throw LexicalError.invariantViolation("Could not get RootNode") - } - let persistedEditorState = SerializedEditorState(rootNode: rootNode) - let encodedData = try JSONEncoder().encode(persistedEditorState) - guard let jsonString = String(data: encodedData, encoding: .utf8) else { return "" } - return jsonString + guard getActiveEditor() != nil else { + throw LexicalError.invariantViolation("Requires editor to export JSON") } - if let string { - return string + guard let rootNode = getRootNode() else { + throw LexicalError.invariantViolation("Could not get RootNode") } - throw LexicalError.invariantViolation("Expected string") + let persistedEditorState = SerializedEditorState(rootNode: rootNode) + let encodedData = try JSONEncoder().encode(persistedEditorState) + guard let jsonString = String(data: encodedData, encoding: .utf8) else { throw LexicalError.invariantViolation("Expected string") } + + return jsonString } /** diff --git a/Lexical/Core/Errors.swift b/Lexical/Core/Errors.swift index 80a6d335..47c5a1dd 100644 --- a/Lexical/Core/Errors.swift +++ b/Lexical/Core/Errors.swift @@ -13,4 +13,5 @@ public enum LexicalError: Error { case sanityCheck(errorMessage: String, textViewText: String, fullReconcileText: String) case reconciler(String) case rangeCacheSearch(String) + case styleValidation(String) } diff --git a/Lexical/Core/Events.swift b/Lexical/Core/Events.swift index 5854d17b..f660aa1f 100644 --- a/Lexical/Core/Events.swift +++ b/Lexical/Core/Events.swift @@ -176,16 +176,8 @@ internal func onSelectionChange(editor: Editor) { } try lexicalSelection.applyNativeSelection(nativeSelection) + lexicalSelection.styles = (try? lexicalSelection.anchor.getNode().getStyles()) ?? [:] - switch lexicalSelection.anchor.type { - case .text: - guard let anchorNode = try lexicalSelection.anchor.getNode() as? TextNode else { break } - lexicalSelection.format = anchorNode.getFormat() - case .element: - lexicalSelection.format = TextFormat() - default: - break - } editor.dispatchCommand(type: .selectionChange, payload: nil) } } catch { @@ -217,6 +209,10 @@ internal func handleIndentAndOutdent(insertTab: (Node) -> Void, indentOrOutdent: public func registerRichText(editor: Editor) { + // Style defaults and commands are handled in Styles.swift + registerDefaultStyles(editor: editor) + registerStyleCommands(editor: editor) + _ = editor.registerCommand(type: .insertLineBreak, listener: { [weak editor] payload in guard let editor else { return false } do { diff --git a/Lexical/Core/Nodes/CodeHighlightNode.swift b/Lexical/Core/Nodes/CodeHighlightNode.swift index aa0ed0d2..df2b718a 100644 --- a/Lexical/Core/Nodes/CodeHighlightNode.swift +++ b/Lexical/Core/Nodes/CodeHighlightNode.swift @@ -16,13 +16,11 @@ public class CodeHighlightNode: TextNode { override public init() { super.init() - self.type = NodeType.codeHighlight } required init(text: String, highlightType: String?, key: NodeKey? = nil) { super.init(text: text, key: key) self.highlightType = highlightType - self.type = NodeType.codeHighlight } public required init(from decoder: Decoder) throws { @@ -30,12 +28,19 @@ public class CodeHighlightNode: TextNode { try super.init(from: decoder) self.highlightType = try container.decode(String.self, forKey: .highlightType) - self.type = NodeType.codeHighlight } public required convenience init(text: String, key: NodeKey?) { self.init(text: text, highlightType: nil, key: key) } + + public required init(styles: StylesDict, key: NodeKey?) { + fatalError("init(styles:key:) has not been implemented") + } + + public override class func getType() -> NodeType { + return .codeHighlight + } override public func encode(to encoder: Encoder) throws { try super.encode(to: encoder) @@ -48,7 +53,12 @@ public class CodeHighlightNode: TextNode { } // Prevent formatting (bold, underline, etc) + @available(*, deprecated) override public func setFormat(format: TextFormat) throws -> CodeHighlightNode { return try self.getWritable() } + + override public func setStyles(_ stylesDict: StylesDict) throws {} + + override public func setStyle(_ style: T.Type, _ value: T.StyleValueType?) throws where T : Style {} } diff --git a/Lexical/Core/Nodes/CodeNode.swift b/Lexical/Core/Nodes/CodeNode.swift index 581051b7..be619021 100644 --- a/Lexical/Core/Nodes/CodeNode.swift +++ b/Lexical/Core/Nodes/CodeNode.swift @@ -44,13 +44,11 @@ public class CodeNode: ElementNode { override public init() { super.init() - self.type = NodeType.code } required init(language: String, key: NodeKey? = nil) { super.init(key) self.language = language - self.type = NodeType.code } public required init(from decoder: Decoder) throws { @@ -58,7 +56,14 @@ public class CodeNode: ElementNode { try super.init(from: decoder) self.language = try container.decode(String.self, forKey: .language) - self.type = NodeType.code + } + + public required init(styles: StylesDict, key: NodeKey?) { + super.init(styles: styles, key: key) + } + + public override class func getType() -> NodeType { + return .code } override public func encode(to encoder: Encoder) throws { diff --git a/Lexical/Core/Nodes/DecoratorNode.swift b/Lexical/Core/Nodes/DecoratorNode.swift index 190e63ae..f3e54737 100644 --- a/Lexical/Core/Nodes/DecoratorNode.swift +++ b/Lexical/Core/Nodes/DecoratorNode.swift @@ -49,18 +49,22 @@ import UIKit */ open class DecoratorNode: Node { - override public init() { - super.init() + public init() { + super.init(styles: [:], key: nil) } - override public required init(_ key: NodeKey?) { - super.init(key) + public required init(_ key: NodeKey?) { + super.init(styles: [:], key: key) } public required init(from decoder: Decoder) throws { try super.init(from: decoder) } - + + public required init(styles: StylesDict, key: NodeKey?) { + super.init(styles: styles, key: key) + } + override open func encode(to encoder: Encoder) throws { try super.encode(to: encoder) } diff --git a/Lexical/Core/Nodes/ElementNode.swift b/Lexical/Core/Nodes/ElementNode.swift index bdae28ed..9b943e21 100644 --- a/Lexical/Core/Nodes/ElementNode.swift +++ b/Lexical/Core/Nodes/ElementNode.swift @@ -24,12 +24,12 @@ open class ElementNode: Node { return direction } - override public init() { - super.init() + public init() { + super.init(styles: [:], key: nil) } - override public init(_ key: NodeKey?) { - super.init(key) + public init(_ key: NodeKey?) { + super.init(styles: [:], key: key) } public required init(from decoder: Decoder) throws { @@ -73,14 +73,17 @@ open class ElementNode: Node { node.parent = self.key } } - + + public required init(styles: StylesDict, key: NodeKey?) { + super.init(styles: styles, key: key) + } + override open func encode(to encoder: Encoder) throws { try super.encode(to: encoder) var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(self.getChildren(), forKey: .children) try container.encode(self.direction, forKey: .direction) try container.encode(self.indent, forKey: .indent) - try container.encode("", forKey: .format) } @discardableResult diff --git a/Lexical/Core/Nodes/HeadingNode.swift b/Lexical/Core/Nodes/HeadingNode.swift index 41728789..45775075 100644 --- a/Lexical/Core/Nodes/HeadingNode.swift +++ b/Lexical/Core/Nodes/HeadingNode.swift @@ -36,23 +36,28 @@ public class HeadingNode: ElementNode { self.tag = tag super.init() - self.type = NodeType.heading } public required init(_ key: NodeKey?, tag: HeadingTagType) { self.tag = tag super.init(key) - self.type = NodeType.heading + } + + override class public func getType() -> NodeType { + return .heading } public required init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.tag = try container.decode(HeadingTagType.self, forKey: .tag) try super.init(from: decoder) - - self.type = NodeType.heading } - + + public required init(styles: StylesDict, key: NodeKey?) { + tag = .h1 + super.init(styles: styles, key: key) + } + override public func encode(to encoder: Encoder) throws { try super.encode(to: encoder) var container = encoder.container(keyedBy: CodingKeys.self) diff --git a/Lexical/Core/Nodes/LineBreakNode.swift b/Lexical/Core/Nodes/LineBreakNode.swift index 6b44210e..3fda5e93 100644 --- a/Lexical/Core/Nodes/LineBreakNode.swift +++ b/Lexical/Core/Nodes/LineBreakNode.swift @@ -6,19 +6,20 @@ */ public class LineBreakNode: Node { - override public init() { - super.init() - self.type = NodeType.linebreak + public init() { + super.init(styles: [:], key: nil) } - override required init(_ key: NodeKey?) { - super.init(key) - self.type = NodeType.linebreak + required public init(styles: StylesDict, key: NodeKey?) { + super.init(styles: styles, key: key) } public required init(from decoder: Decoder) throws { try super.init(from: decoder) - self.type = NodeType.linebreak + } + + public override class func getType() -> NodeType { + .linebreak } override public func encode(to encoder: Encoder) throws { @@ -26,7 +27,7 @@ public class LineBreakNode: Node { } override public func clone() -> Self { - Self(key) + Self(styles: styles, key: key) } override public func getPostamble() -> String { @@ -34,6 +35,6 @@ public class LineBreakNode: Node { } public func createLineBreakNode() -> LineBreakNode { - return LineBreakNode() + return LineBreakNode(styles: [:], key: nil) } } diff --git a/Lexical/Core/Nodes/Node.swift b/Lexical/Core/Nodes/Node.swift index fe0e6255..ed73a4d4 100644 --- a/Lexical/Core/Nodes/Node.swift +++ b/Lexical/Core/Nodes/Node.swift @@ -18,24 +18,23 @@ open class Node: Codable { enum CodingKeys: String, CodingKey { case type case version + case styles } public var key: NodeKey var parent: NodeKey? - public var type: NodeType public var version: Int - public init() { - self.type = Node.getType() - self.version = 1 - self.key = LexicalConstants.uninitializedNodeKey - - _ = try? generateKey(node: self) - } + // Use style APIs to manipulate styles: see Styles.swift + // This is public only so that it can be accessed in e.g. clone methods of subclasses. + public var styles: StylesDict = [:] - public init(_ key: NodeKey?) { - self.type = Node.getType() + // This change will be a code-breaking change for subclasses, which must now all implement this method. However, + // the effort required to update each class will be minimal. The rationale for this breaking change is to avoid + // bugs where styles disappear when nodes are copied, etc. + public required init(styles: StylesDict, key: NodeKey?) { self.version = 1 + self.styles = styles if let key, key != LexicalConstants.uninitializedNodeKey { self.key = key @@ -45,21 +44,54 @@ open class Node: Codable { } } + public convenience init() { + self.init(styles: [:], key: nil) + } + /// Used when initialising node from JSON public required init(from decoder: Decoder) throws { let values = try decoder.container(keyedBy: CodingKeys.self) key = LexicalConstants.uninitializedNodeKey - type = try NodeType(rawValue: values.decode(String.self, forKey: .type)) version = try values.decode(Int.self, forKey: .version) + // styles + if let styleContainer = try? values.nestedContainer(keyedBy: StyleCodingKeys.self, forKey: .styles) { + guard let editor = getActiveEditor() else { + throw LexicalError.internal("Could not get active editor") + } + // try each style and see if there's a value + var newStyles: StylesDict = [:] + for (styleName, style) in editor.registeredStyles { + guard let styleKey = StyleCodingKeys(stringValue: styleName.rawValue) else { continue } + guard let superDecoder = try? styleContainer.superDecoder(forKey: styleKey) else { continue } + if let value = try? styleValueFromDecoder(style, decoder: superDecoder) { + newStyles[styleName] = value + } + } + self.styles = newStyles + } + _ = try? generateKey(node: self) } /// Used when serialising node to JSON open func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode(self.type.rawValue, forKey: .type) try container.encode(self.version, forKey: .version) + try container.encode(self.type.rawValue, forKey: .type) + + // styles + if styles.count > 0 { + var stylesContainer = container.nestedContainer(keyedBy: StyleCodingKeys.self, forKey: .styles) + guard let editor = getActiveEditor() else { + throw LexicalError.internal("Could not get active editor") + } + for (styleName, style) in editor.registeredStyles { + guard let styleValue = self.getStyle(style), + let styleKey = StyleCodingKeys(stringValue: styleName.rawValue) else { continue } + try stylesContainer.encode(styleValue, forKey: styleKey) + } + } } /** @@ -68,13 +100,16 @@ open class Node: Codable { */ open func didMoveTo(newEditor editor: Editor) {} - // This is an initial value for `type`. - // static methods cannot be overridden in swift so, - // each subclass needs to assign the type property in their init method - static func getType() -> NodeType { + open class func getType() -> NodeType { NodeType.unknown } + public var type: NodeType { + get { + Self.getType() + } + } + /// Provides the **preamble** part of the node's content. Typically the preamble is used for control characters to represent embedded objects (see ``DecoratorNode``). /// /// In Lexical iOS, a node's content is split into four parts: preamble, children, text, postamble. ``ElementNode`` subclasses can implement preamble/postamble, and TextNode subclasses can implement the text part. @@ -112,7 +147,7 @@ open class Node: Codable { /// Returns the latest version of the node from the active EditorState. This is used to avoid getting values from stale node references. public func getLatest() -> Self { guard let latest: Self = getNodeByKey(key: key) else { - fatalError() + return self } return latest } @@ -124,7 +159,17 @@ open class Node: Codable { /// Lets the node provide attributes for TextKit to use to render the node's content. open func getAttributedStringAttributes(theme: Theme) -> [NSAttributedString.Key: Any] { - [:] + let node = getLatest() + let attributesDict = theme.getValue(node.type, withSubtype: nil) ?? [:] + guard let editor = getActiveEditor() else { + return attributesDict + } + let styleAttributeDicts: [Theme.AttributeDict] = node.styles.map { (key: StyleName, value: Any) in + guard let styleType = editor.registeredStyles[key] else { return [:] } + return styleAttributesDictFor(node: node, style: styleType, theme: theme) + } + let dicts = [attributesDict] + styleAttributeDicts + return dicts.reduce([:]) { $0.merging($1) { (_, next) in next } } } /** @@ -162,7 +207,7 @@ open class Node: Codable { if let latestNode = latestNode as? ElementNode, let mutableNode = mutableNode as? ElementNode { mutableNode.children = latestNode.children } else if let latestNode = latestNode as? TextNode, let mutableNode = mutableNode as? TextNode { - mutableNode.format = latestNode.format + mutableNode.styles = latestNode.styles mutableNode.mode = latestNode.mode } @@ -782,6 +827,44 @@ open class Node: Codable { public func isSameNode(_ node: Node) -> Bool { return self.getKey() == node.getKey() } + + // MARK: - Styles + + public func getStyle(_ style: T.Type) -> T.StyleValueType? { + let styleVal = self.getLatest().styles[style.name] + if let styleVal = styleVal as? T.StyleValueType { + return styleVal + } + return nil + } + + public func setStyle(_ style: T.Type, _ value: T.StyleValueType?) throws { + try validateStyleOrThrow(style) + try getWritable().styles[style.name] = value + } + + public func getStyles() -> StylesDict { + return getLatest().styles + } + + public func setStyles(_ stylesDict: StylesDict) throws { + // TODO: validate all! + try getWritable().styles = stylesDict + } + + public func toggleStyle(_ style: T.Type) throws where T.StyleValueType == Bool { + let currentValue = getStyle(style) ?? false + try setStyle(style, !currentValue) + } + + public func validateStyle(_ style: T.Type, value: T.StyleValueType?) -> T.StyleValueType? { + do { + try validateStyleOrThrow(style) + } catch { + return nil + } + return value + } } extension Node: Hashable { diff --git a/Lexical/Core/Nodes/ParagraphNode.swift b/Lexical/Core/Nodes/ParagraphNode.swift index 63c6cf36..51a9d092 100644 --- a/Lexical/Core/Nodes/ParagraphNode.swift +++ b/Lexical/Core/Nodes/ParagraphNode.swift @@ -10,17 +10,22 @@ import UIKit public class ParagraphNode: ElementNode { override public init() { super.init() - self.type = NodeType.paragraph } override required init(_ key: NodeKey?) { super.init(key) - self.type = NodeType.paragraph } public required init(from decoder: Decoder) throws { try super.init(from: decoder) - self.type = NodeType.paragraph + } + + public required init(styles: StylesDict, key: NodeKey?) { + super.init(styles: styles, key: key) + } + + public override class func getType() -> NodeType { + return .paragraph } override public func encode(to encoder: Encoder) throws { diff --git a/Lexical/Core/Nodes/QuoteNode.swift b/Lexical/Core/Nodes/QuoteNode.swift index 64e99a38..bade61c6 100644 --- a/Lexical/Core/Nodes/QuoteNode.swift +++ b/Lexical/Core/Nodes/QuoteNode.swift @@ -10,18 +10,22 @@ import UIKit public class QuoteNode: ElementNode { override public init() { super.init() - self.type = NodeType.quote } override public required init(_ key: NodeKey?) { super.init(key) - self.type = NodeType.quote } public required init(from decoder: Decoder) throws { try super.init(from: decoder) + } + + public required init(styles: StylesDict, key: NodeKey?) { + super.init(styles: styles, key: key) + } - self.type = NodeType.quote + public override class func getType() -> NodeType { + return .quote } override public func encode(to encoder: Encoder) throws { diff --git a/Lexical/Core/Nodes/RootNode.swift b/Lexical/Core/Nodes/RootNode.swift index 49115a3c..083ebb7c 100644 --- a/Lexical/Core/Nodes/RootNode.swift +++ b/Lexical/Core/Nodes/RootNode.swift @@ -9,14 +9,16 @@ import UIKit public class RootNode: ElementNode { - override required init() { - super.init(kRootNodeKey) - self.type = NodeType.root + override init() { + super.init(styles: [:], key: kRootNodeKey) + } + + public required init(styles: StylesDict, key: NodeKey?) { + super.init(styles: styles, key: key ?? kRootNodeKey) } public required init(from decoder: Decoder) throws { try super.init(from: decoder) - self.type = NodeType.root } override public func encode(to encoder: Encoder) throws { @@ -24,7 +26,11 @@ public class RootNode: ElementNode { } override public func clone() -> Self { - Self() + Self(styles: self.styles, key: kRootNodeKey) + } + + override static public func getType() -> NodeType { + return .root } override public func getAttributedStringAttributes(theme: Theme) -> [NSAttributedString.Key: Any] { diff --git a/Lexical/Core/Nodes/TextNode.swift b/Lexical/Core/Nodes/TextNode.swift index 581199c8..f0967459 100644 --- a/Lexical/Core/Nodes/TextNode.swift +++ b/Lexical/Core/Nodes/TextNode.swift @@ -11,6 +11,7 @@ public enum TextNodeThemeSubtype { public static let code = "code" } +@available(*, deprecated, message: "Use new styles system") public struct SerializedTextFormat: OptionSet, Codable { public let rawValue: Int @@ -83,6 +84,7 @@ public struct SerializedTextFormat: OptionSet, Codable { } } +@available(*, deprecated, message: "use new styles system") public struct TextFormat: Equatable, Codable { public var bold: Bool @@ -187,26 +189,25 @@ open class TextNode: Node { enum CodingKeys: String, CodingKey { case text case mode - case format case detail - case style + case format // for compatibility } private var text: String = "" public var mode: Mode = .normal - var format: TextFormat = TextFormat() var detail = TextNodeDetail() - var style: String = "" - override public init() { - super.init() - self.type = NodeType.text + init() { + super.init(styles: [:], key: nil) } public required init(text: String, key: NodeKey?) { - super.init(key) + super.init(styles: [:], key: key) self.text = text - self.type = NodeType.text + } + + open override class func getType() -> NodeType { + return .text } public convenience init(text: String) { @@ -219,22 +220,27 @@ open class TextNode: Node { self.text = try container.decode(String.self, forKey: .text) self.mode = try container.decode(Mode.self, forKey: .mode) - let serializedFormat = try container.decode(SerializedTextFormat.self, forKey: .format) - self.format = SerializedTextFormat.convertToTextFormat(from: serializedFormat) + + if let serializedFormat = try container.decodeIfPresent(SerializedTextFormat.self, forKey: .format) { + let textFormat = SerializedTextFormat.convertToTextFormat(from: serializedFormat) + let styles = compatibilityStylesFromFormat(textFormat) + self.styles = self.styles.merging(styles, uniquingKeysWith: { _, new in new }) + } + let serializedDetail = try container.decode(SerializedTextNodeDetail.self, forKey: .detail) self.detail = SerializedTextNodeDetail.convertToTextDetail(from: serializedDetail) - self.style = try container.decode(String.self, forKey: .style) - self.type = NodeType.text } - + + public required init(styles: StylesDict, key: NodeKey?) { + super.init(styles: styles, key: key) + } + override open func encode(to encoder: Encoder) throws { try super.encode(to: encoder) var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(self.text, forKey: .text) try container.encode(self.mode, forKey: .mode) - try container.encode(SerializedTextFormat.convertToSerializedTextFormat(from: self.format).rawValue, forKey: .format) try container.encode(SerializedTextNodeDetail.convertToSerializedTextNodeDetail(from: self.detail).rawValue, forKey: .detail) - try container.encode(self.style, forKey: .style) } override public func getTextPart() -> String { @@ -260,12 +266,12 @@ open class TextNode: Node { public func setBold(_ isBold: Bool) throws { try errorOnReadOnly() - try getWritable().format.bold = isBold + try getWritable().setStyle(Styles.Bold.self, isBold) } public func setItalic(_ isItalic: Bool) throws { try errorOnReadOnly() - try getWritable().format.italic = isItalic + try getWritable().setStyle(Styles.Italic.self, isItalic) } public func canInsertTextAfter() -> Bool { @@ -276,42 +282,6 @@ open class TextNode: Node { return Self(text: text, key: key) } - override open func getAttributedStringAttributes(theme: Theme) -> [NSAttributedString.Key: Any] { - var attributeDictionary = super.getAttributedStringAttributes(theme: theme) - - // TODO: Remove this once codeHighlight node is implemented - if let parent, let _ = getNodeByKey(key: parent) as? CodeNode { - format = TextFormat() - } - - if format.bold { - attributeDictionary[.bold] = true - } - - if format.italic { - attributeDictionary[.italic] = true - } - - if format.underline { - attributeDictionary[.underlineStyle] = NSUnderlineStyle.single.rawValue - } - - if format.strikethrough { - attributeDictionary[.strikethroughStyle] = NSUnderlineStyle.single.rawValue - } - - if format.code { - if let themeDict = theme.getValue(.text, withSubtype: TextNodeThemeSubtype.code) { - attributeDictionary.merge(themeDict) { (_, new) in new } - } else { - attributeDictionary[NSAttributedString.Key.fontFamily] = "Courier" - attributeDictionary[NSAttributedString.Key.backgroundColor] = UIColor.lightGray - } - } - - return attributeDictionary - } - public func isInert() -> Bool { let node = getLatest() as TextNode return node.mode == .inert @@ -363,7 +333,7 @@ open class TextNode: Node { try writableNode.setText("\(prefixText)\(newText)\(postText)") - let selection = try getSelection(allowInvalidPositions: true) + let selection = try getSelection() if moveSelection, let selection = selection as? RangeSelection { let newOffset = offset + newText.lengthAsNSString() selection.setTextNodeRange( @@ -394,28 +364,29 @@ open class TextNode: Node { return true } + @available(*, deprecated, message: "Use new styles system") public func getFormat() -> TextFormat { let node = getLatest() as TextNode - return node.format + return compatibilityFormatFromStyles(node.styles) } + @available(*, deprecated, message: "Use new styles system") @discardableResult public func setFormat(format: TextFormat) throws -> TextNode { try errorOnReadOnly() let node = try getWritable() as TextNode - node.format = format + let newStyles = compatibilityStylesFromFormat(format) + node.styles = compatibilityMergeStylesAssumingAllFormats(old: node.styles, newFormats: newStyles) return node } + @available(*, deprecated, message: "Use new styles system") public func getStyle() -> String { - let node = getLatest() as TextNode - return node.style + return "" } - public func setStyle(_ style: String) throws { - let writable = try getWritable() - writable.style = style - } + @available(*, deprecated, message: "Use new styles system") + public func setStyle(_ style: String) throws {} public func splitText(splitOffsets: [Int]) throws -> [TextNode] { try errorOnReadOnly() @@ -456,8 +427,7 @@ open class TextNode: Node { // Create a new TextNode writableNode = createTextNode(text: firstPart) writableNode.parent = parentKey - writableNode.format = format - writableNode.style = style + writableNode.styles = styles writableNode.detail = detail hasReplacedSelf = true } else { @@ -475,8 +445,7 @@ open class TextNode: Node { let part = parts[i] let partSize = part.lengthAsNSString() let sibling = try createTextNode(text: part).getWritable() - sibling.format = format - sibling.style = style + sibling.styles = styles sibling.detail = detail let siblingKey = sibling.key let nextTextSize = textSize + partSize @@ -572,9 +541,10 @@ open class TextNode: Node { return selection } + @available(*, deprecated, message: "Use new styles system") public func getFormatFlags(type: TextFormatType, alignWithFormat: TextFormat? = nil) -> TextFormat { let node = getLatest() as TextNode - let format = node.format + let format = compatibilityFormatFromStyles(node.getStyles()) return toggleTextFormatType(format: format, type: type, alignWithFormat: alignWithFormat) } @@ -648,15 +618,14 @@ open class TextNode: Node { static func canSimpleTextNodesBeMerged(node1: TextNode, node2: TextNode) -> Bool { let node1Mode = node1.mode - let node1Format = node1.format - let node1Style = node1.style + let node1Style = node1.styles let node2Mode = node2.mode - let node2Format = node2.format - let node2Style = node2.style + let node2Style = node2.styles + + guard let editor = getActiveEditor() else { return false } return node1Mode == node2Mode && - node1Format == node2Format && - node1Style == node2Style + stylesDictsAreEqual(node1Style, node2Style, editor: editor) } static func mergeTextNodes(node1: TextNode, node2: TextNode) throws -> TextNode { diff --git a/Lexical/Core/Nodes/UnknownNode.swift b/Lexical/Core/Nodes/UnknownNode.swift index 7aa18164..1f4f5dd4 100644 --- a/Lexical/Core/Nodes/UnknownNode.swift +++ b/Lexical/Core/Nodes/UnknownNode.swift @@ -116,22 +116,15 @@ public class UnknownNode: Node { data = try container.decode(SupportedValue.self) - typealias Keys = Node.CodingKeys - // NB: As consuming keys in a coding container is a stateful operation, certain keys are // re-extracted and initialized similarly to the super class here. - super.init() - - if case let .object(values) = data { - - if case let .string(type) = values[Keys.type.rawValue] { - self.type = NodeType(rawValue: type) - } else { - throw LexicalError.invariantViolation("No type passed to UnknownNode") - } - } + super.init(styles: [:], key: nil) } - + + public required init(styles: StylesDict, key: NodeKey?) { + fatalError("init(styles:key:) has not been implemented") + } + override open func encode(to encoder: Encoder) throws { var container = encoder.singleValueContainer() diff --git a/Lexical/Core/Selection/BaseSelection.swift b/Lexical/Core/Selection/BaseSelection.swift index f430c843..0aeb3b4a 100644 --- a/Lexical/Core/Selection/BaseSelection.swift +++ b/Lexical/Core/Selection/BaseSelection.swift @@ -62,4 +62,7 @@ public protocol BaseSelection: AnyObject, CustomDebugStringConvertible { /// Handles user-provided text to insert, applying a series of insertion heuristics based on the selection type and position. func insertText(_ text: String) throws + + /// Applies a style to the text nodes in the selection, splitting if necessary + func applyTextStyle(_ style: T.Type, value: T.StyleValueType?) throws } diff --git a/Lexical/Core/Selection/GridSelection.swift b/Lexical/Core/Selection/GridSelection.swift index 3215c4b8..a052379e 100644 --- a/Lexical/Core/Selection/GridSelection.swift +++ b/Lexical/Core/Selection/GridSelection.swift @@ -75,6 +75,10 @@ public class GridSelection: BaseSelection { public func insertText(_ text: String) throws { // TODO } + + public func applyTextStyle(_ style: T.Type, value: T.StyleValueType?) throws where T : Style { + // TODO + } } extension GridSelection: CustomDebugStringConvertible { diff --git a/Lexical/Core/Selection/NodeSelection.swift b/Lexical/Core/Selection/NodeSelection.swift index 625372c4..60c157f4 100644 --- a/Lexical/Core/Selection/NodeSelection.swift +++ b/Lexical/Core/Selection/NodeSelection.swift @@ -135,6 +135,10 @@ public class NodeSelection: BaseSelection { let focus = Point(key: parent.getKey(), offset: nodeIndexInParent + 1, type: .element) return RangeSelection(anchor: anchor, focus: focus, format: TextFormat()) } + + public func applyTextStyle(_ style: T.Type, value: T.StyleValueType?) throws where T : Style { + // TODO + } } extension NodeSelection: CustomDebugStringConvertible { diff --git a/Lexical/Core/Selection/RangeSelection.swift b/Lexical/Core/Selection/RangeSelection.swift index d099ebb8..1748e23b 100644 --- a/Lexical/Core/Selection/RangeSelection.swift +++ b/Lexical/Core/Selection/RangeSelection.swift @@ -9,26 +9,32 @@ import Foundation import UIKit public class RangeSelection: BaseSelection { - public var anchor: Point public var focus: Point public var dirty: Bool - public var format: TextFormat - public var style: String // TODO: add style support to iOS + + /// The styles that will be applied to typed text at this selection location. Note that only text styles should be put here, + /// as element styles can always be applied directly to the element in question. + public var styles: StylesDict = [:] // MARK: - Init - public init(anchor: Point, focus: Point, format: TextFormat) { + public init(anchor: Point, focus: Point, styles: StylesDict) { self.anchor = anchor self.focus = focus self.dirty = false - self.format = format - self.style = "" + self.styles = styles anchor.selection = self focus.selection = self } + @available(*, deprecated, renamed: "init(anchor:focus:styles:)") + public convenience init(anchor: Point, focus: Point, format: TextFormat) { + let styles = compatibilityStylesFromFormat(format) + self.init(anchor: anchor, focus: focus, styles: styles) + } + // MARK: - Public public func isBackward() throws -> Bool { @@ -39,8 +45,14 @@ public class RangeSelection: BaseSelection { return anchor == focus } + @available(*, deprecated, message: "Use new styles system") public func hasFormat(type: TextFormatType) -> Bool { - return format.isTypeSet(type: type) + let style = compatibilityStyleFromFormatType(type) + let value = styles[style.name] + if let value = value as? Bool { + return value + } + return false } public func getCharacterOffsets(selection: RangeSelection) -> (Int, Int) { @@ -92,7 +104,7 @@ public class RangeSelection: BaseSelection { let selectionFocus = createPoint(key: focus.key, offset: focus.offset, type: focus.type) - return RangeSelection(anchor: selectionAnchor, focus: selectionFocus, format: format) + return RangeSelection(anchor: selectionAnchor, focus: selectionFocus, styles: styles) } public func setTextNodeRange(anchorNode: TextNode, @@ -249,13 +261,12 @@ public class RangeSelection: BaseSelection { let focus = focus let anchorIsBefore = try anchor.isBefore(point: focus) let isBefore = isCollapsed() || anchorIsBefore - let format = format - let style = style + let styles = styles if isBefore && anchor.type == .element { - try transferStartingElementPointToTextPoint(start: anchor, end: focus, format: format, style: style) + try transferStartingElementPointToTextPoint(start: anchor, end: focus, styles: styles) } else if !isBefore && focus.type == .element { - try transferStartingElementPointToTextPoint(start: focus, end: anchor, format: format, style: style) + try transferStartingElementPointToTextPoint(start: focus, end: anchor, styles: styles) } let selectedNodes = try getNodes() @@ -285,7 +296,7 @@ public class RangeSelection: BaseSelection { isTokenOrSegmented(nextSibling) { nextSibling = TextNode() if let nextSibling { - try nextSibling.setFormat(format: format) + try nextSibling.setStyles(styles) if !firstNodeParent.canInsertTextAfter() { try firstNodeParent.insertAfter(nodeToInsert: nextSibling) } else { @@ -311,7 +322,7 @@ public class RangeSelection: BaseSelection { if prevSibling == nil || isTokenOrSegmented(prevSibling) { prevSibling = TextNode() if let prevSibling { - try prevSibling.setFormat(format: format) + try prevSibling.setStyles(styles) if !firstNodeParent.canInsertTextBefore() { try firstNodeParent.insertBefore(nodeToInsert: prevSibling) } else { @@ -329,7 +340,7 @@ public class RangeSelection: BaseSelection { } } else if firstNode.isSegmented() && startOffset != firstNodeTextLength { let textNode = TextNode(text: firstNode.getTextPart()) - try textNode.setFormat(format: format) + try textNode.setStyles(styles) try firstNode.replace(replaceWith: textNode) firstNode = textNode } else if !isCollapsed() && text.lengthAsNSString() > 0 { @@ -358,17 +369,14 @@ public class RangeSelection: BaseSelection { try firstNode.replace(replaceWith: textNode) return } - let firstNodeFormat = firstNode.getFormat() - let firstNodeStyle = firstNode.getStyle() + let firstNodeStyles = firstNode.getStyles() - if startOffset == endOffset && (firstNodeFormat != format || firstNodeStyle != style) { + if startOffset == endOffset, let editor = getActiveEditor(), (!stylesDictsAreEqual(styles, firstNodeStyles, editor: editor)) { if firstNode.getTextPart().lengthAsNSString() == 0 { - try firstNode.setFormat(format: format) - try firstNode.setStyle(style) + try firstNode.setStyles(styles) } else { let textNode = TextNode(text: text) - try textNode.setFormat(format: format) - try textNode.setStyle(style) + try textNode.setStyles(styles) try textNode.select(anchorOffset: nil, focusOffset: nil) if startOffset == 0 { try firstNode.insertBefore(nodeToInsert: textNode) @@ -396,8 +404,7 @@ public class RangeSelection: BaseSelection { // we correctly replace that right range. self.anchor.offset -= text.lengthAsNSString() } else { - self.format = firstNodeFormat - self.style = firstNodeStyle + self.styles = firstNodeStyles } } } else { @@ -1070,171 +1077,184 @@ public class RangeSelection: BaseSelection { self.anchor = anchor self.focus = focus self.dirty = false - self.format = TextFormat() - self.style = "" + self.styles = [:] } - internal func formatText(formatType: TextFormatType) throws { + public func testApply(_ style: T.Type) throws {} + + public func applyTextStyle(_ style: T.Type, value: T.StyleValueType?) throws { if isCollapsed() { - toggleFormat(type: formatType) + // If selection is collapsed (i.e. anchor == focus), simply set this style on the selection. + // Assigning nil will remove the style from selection. + styles[style.name] = value return } - + let selectedNodes = try getNodes() - guard var firstNode = selectedNodes.first, let lastNode = selectedNodes.last else { return } + let selectedTextNodes: [TextNode] = selectedNodes.compactMap { node in + return node as? TextNode + } - var firstNextFormat = TextFormat() - for node in selectedNodes { - if let node = node as? TextNode { - firstNextFormat = node.getFormatFlags(type: formatType) - break - } + guard selectedTextNodes.count > 0 else { + styles[style.name] = value + return } - let isBefore = try anchor.isBefore(point: focus) - var startOffset = isBefore ? anchor.offset : focus.offset - var endOffset = isBefore ? focus.offset : anchor.offset - - // This is the case where the user only selected the very end of the - // first node so we don't want to include it in the formatting change. - if startOffset == firstNode.getTextPartSize() { - if let nextSibling = firstNode.getNextSibling() as? TextNode { - // we basically make the second node the firstNode, changing offsets accordingly - anchor.offset = 0 - startOffset = 0 - firstNode = nextSibling - firstNextFormat = nextSibling.getFormat() - } + let anchor = self.anchor + let focus = self.focus + let isBackward = try self.isBackward() + let startPoint = isBackward ? focus : anchor + let endPoint = isBackward ? anchor : focus + + var firstIndex = 0 + var firstNode = selectedTextNodes.first + var startOffset = startPoint.type == .element ? 0 : startPoint.offset + + // In case selection started at the end of text node use next text node + if let currentFirstNode = firstNode, startPoint.type == .text, startOffset == currentFirstNode.getTextContentSize() { + firstIndex = 1 + firstNode = selectedTextNodes[1] + startOffset = 0 } - // This is the case where we only selected a single node - if firstNode === lastNode { - if let textNode = firstNode as? TextNode { - if anchor.type == .element && focus.type == .element { - try textNode.setFormat(format: firstNextFormat) - let newSelection = try textNode.select(anchorOffset: startOffset, focusOffset: endOffset) - updateSelection( - anchor: newSelection.anchor, - focus: newSelection.focus, - format: firstNextFormat, - isDirty: newSelection.dirty) - return - } + guard var firstNode else { + return + } - startOffset = anchor.offset > focus.offset ? focus.offset : anchor.offset - endOffset = anchor.offset > focus.offset ? anchor.offset : focus.offset + var firstNextStyles = firstNode.getStyles() + firstNextStyles[style.name] = firstNode.validateStyle(style, value: value) - // No actual text is selected, so do nothing. - if startOffset == endOffset { - return - } + let lastIndex = selectedTextNodes.count - 1 + var lastNode = selectedTextNodes[lastIndex] + let endOffset = endPoint.type == .text ? endPoint.offset : lastNode.getTextContentSize() - // The entire node is selected, so just format it - if startOffset == 0 && endOffset == textNode.getTextPartSize() { - try textNode.setFormat(format: firstNextFormat) - let newSelection = try textNode.select(anchorOffset: startOffset, focusOffset: endOffset) - updateSelection( - anchor: newSelection.anchor, - focus: newSelection.focus, - format: firstNextFormat, - isDirty: newSelection.dirty) - } else { - // node is partially selected, so split it into two nodes and style the selected one. - let splitNodes = try textNode.splitText(splitOffsets: [startOffset, endOffset]) - let replacement = startOffset == 0 ? splitNodes[0] : splitNodes[1] - try replacement.setFormat(format: firstNextFormat) - let newSelection = try replacement.select(anchorOffset: 0, focusOffset: endOffset - startOffset) - updateSelection( - anchor: newSelection.anchor, - focus: newSelection.focus, - format: firstNextFormat, - isDirty: newSelection.dirty) + // Single node selected + if firstNode.isSameNode(lastNode) { + if startOffset == endOffset { + return + } + // The entire node is selected, so just format it + if startOffset == 0 && endOffset == firstNode.getTextContentSize() { + try firstNode.setStyle(style, value) + } else { + // Node is partially selected, so split it into two nodes + // add style the selected one. + let splitNodes = try firstNode.splitText(splitOffsets: [startOffset, endOffset]) + let replacement = startOffset == 0 ? splitNodes[0] : splitNodes[1] + try replacement.setStyles(firstNextStyles) + + // Update selection only if starts/ends on text node + if startPoint.type == .text { + startPoint.updatePoint(key: replacement.getKey(), offset: 0, type: .text) } + if endPoint.type == .text { + endPoint.updatePoint(key: replacement.getKey(), offset: endOffset - startOffset, type: .text) + } + } - format = firstNextFormat + self.styles = firstNextStyles + return + } + + // Multiple nodes selected + // The entire first node isn't selected, so split it + if startOffset != 0 { + let splits = try firstNode.splitText(splitOffsets: [startOffset]) + guard splits.count > 1 else { + return } - } else { - // multiple nodes selected - if var textNode = firstNode as? TextNode { - if startOffset != 0 { - // the entire first node isn't selected, so split it - let splitNodes = try textNode.splitText(splitOffsets: [startOffset]) - if splitNodes.count >= 1 { - textNode = splitNodes[1] - } + firstNode = splits[1] + startOffset = 0 + } + try firstNode.setStyles(firstNextStyles) - startOffset = 0 - } - try textNode.setFormat(format: firstNextFormat) + var lastNextStyles = lastNode.getStyles() + lastNextStyles[style.name] = lastNode.validateStyle(style, value: value) - // update selection - if isBefore { - anchor.updatePoint(key: textNode.key, offset: startOffset, type: .text) - } else { - focus.updatePoint(key: textNode.key, offset: startOffset, type: .text) - } + // If the offset is 0, it means no actual characters are selected, + // so we skip formatting the last node altogether. + if endOffset > 0 { + if endOffset != lastNode.getTextContentSize() { + lastNode = try lastNode.splitText(splitOffsets: [endOffset])[0] + } + try lastNode.setStyles(lastNextStyles) + } - format = firstNextFormat + // Process all text nodes in between + for textNode in selectedTextNodes[(firstIndex+1)..= 1 { - textNode = lastNodes[0] - } - } + // Update selection only if starts/ends on text node + if startPoint.type == .text { + startPoint.updatePoint(key: firstNode.getKey(), offset: startOffset, type: .text) + } + if endPoint.type == .text { + endPoint.updatePoint(key: lastNode.getKey(), offset: endOffset, type: .text) + } - try textNode.setFormat(format: lastNextFormat) - // update selection - if isBefore { - focus.updatePoint(key: textNode.key, offset: endOffset, type: .text) - } else { - anchor.updatePoint(key: textNode.key, offset: endOffset, type: .text) - } - } - } + self.styles = firstNextStyles + } - // deal with all the nodes in between - for index in 1.. 0 else { + styles[style.name] = true + return + } + + let anchor = self.anchor + let focus = self.focus + let isBackward = try self.isBackward() + let startPoint = isBackward ? focus : anchor + /*let endPoint*/ _ = isBackward ? anchor : focus + + var firstNode = selectedTextNodes.first + let startOffset = startPoint.type == .element ? 0 : startPoint.offset + + // In case selection started at the end of text node use next text node + if let currentFirstNode = firstNode, startPoint.type == .text, startOffset == currentFirstNode.getTextContentSize(), selectedTextNodes.count > 1 { + firstNode = selectedTextNodes[1] } + + guard let firstNode else { + styles[style.name] = true + return + } + + // Now we have our node to derive style, we can work out which direction to toggle it. + let currentValue: Bool = firstNode.getStyle(style) ?? false + let newValue = !currentValue + try applyBoolStyle(style, value: newValue) + } + + // necessary to support the generics in formatText() + private func applyBoolStyle>(_ style: T.Type, value: T.StyleValueType) throws { + try applyTextStyle(style, value: value) } - internal func clearFormat() { - format = TextFormat() + internal func clearStoredStyles() { + styles = [:] } // MARK: - Private - private func updateSelection(anchor: Point, focus: Point, format: TextFormat, isDirty: Bool) { + private func updateSelection(anchor: Point, focus: Point, styles: StylesDict, isDirty: Bool) { self.anchor.updatePoint(key: anchor.key, offset: anchor.offset, type: anchor.type) self.focus.updatePoint(key: focus.key, offset: focus.offset, type: focus.type) - self.format = format + self.styles = styles self.dirty = isDirty } - private func toggleFormat(type: TextFormatType) { - format = toggleTextFormatType(format: format, type: type, alignWithFormat: nil) - dirty = true - } - private func swapPoints() { let anchorKey = anchor.key let anchorOffset = anchor.offset @@ -1268,18 +1288,19 @@ public class RangeSelection: BaseSelection { } public func isSelection(_ selection: BaseSelection) -> Bool { - guard let selection = selection as? RangeSelection else { + guard let selection = selection as? RangeSelection, let editor = getActiveEditor() else { return false } - return anchor == selection.anchor && focus == selection.focus && format == selection.format + return anchor == selection.anchor && focus == selection.focus && stylesDictsAreEqual(styles, selection.styles, editor: editor) } } extension RangeSelection: Equatable { public static func == (lhs: RangeSelection, rhs: RangeSelection) -> Bool { + guard let editor = getActiveEditor() else { return false } return lhs.anchor == rhs.anchor && lhs.focus == rhs.focus && - lhs.format == rhs.format + stylesDictsAreEqual(lhs.styles, rhs.styles, editor: editor) } } diff --git a/Lexical/Core/Selection/SelectionUtils.swift b/Lexical/Core/Selection/SelectionUtils.swift index 2f256714..0fd53327 100644 --- a/Lexical/Core/Selection/SelectionUtils.swift +++ b/Lexical/Core/Selection/SelectionUtils.swift @@ -26,39 +26,9 @@ func selectPointOnNode(point: Point, node: Node) { point.updatePoint(key: node.key, offset: updatedOffset, type: point.type) } -/// Returns the current Lexical selection, generating it from the UITextView if necessary. -/// - Parameters: -/// - allowInvalidPositions: In most cases, it is desirable to regenerate the selection whenever -/// the current selection does not refer to valid positions in valid nodes. However, there are some -/// situations, such as if you're getting the selection when preparing to modify it to be valid using your -/// own validity rules, when you just want to fetch the current selection whatever it is. -public func getSelection(allowInvalidPositions: Bool = false) throws -> BaseSelection? { - let editorState = getActiveEditorState() - let selection = editorState?.selection - - if let selection { - if allowInvalidPositions == true { - return selection - } - if sanityCheckSelection(selection) { - return selection - } - getActiveEditor()?.log(.other, .warning, "Selection failed sanity check") - } - - if let editor = getActiveEditor() { - do { - let selection = try createSelection(editor: editor) - editorState?.selection = selection - return selection - } catch { - editor.log(.other, .warning, "Exception while creating range selection") - return nil - } - } - - // Could not get active editor. This is unexpected, but we can't log since logging requires editor! - throw LexicalError.invariantViolation("called getSelection() without an active editor") +/// Returns the current Lexical selection +public func getSelection() throws -> BaseSelection? { + return getActiveEditorState()?.selection } private func sanityCheckSelection(_ selection: BaseSelection) -> Bool { @@ -234,7 +204,7 @@ func createEmptyRangeSelection() -> RangeSelection { let anchor = Point(key: kRootNodeKey, offset: 0, type: .element) let focus = Point(key: kRootNodeKey, offset: 0, type: .element) - return RangeSelection(anchor: anchor, focus: focus, format: TextFormat()) + return RangeSelection(anchor: anchor, focus: focus, styles: [:]) } /// When we create a selection, we try to use the previous selection where possible, unless an actual user selection change has occurred. @@ -259,7 +229,7 @@ func createSelection(editor: Editor) throws -> BaseSelection? { if let anchor = try pointAtStringLocation(range.location, searchDirection: nativeSelection.affinity, rangeCache: editor.rangeCache), let focus = try pointAtStringLocation(range.location + range.length, searchDirection: nativeSelection.affinity, rangeCache: editor.rangeCache) { - return RangeSelection(anchor: anchor, focus: focus, format: TextFormat()) + return RangeSelection(anchor: anchor, focus: focus, styles: [:]) } return nil @@ -285,7 +255,7 @@ func makeRangeSelection( let selection = RangeSelection( anchor: Point(key: anchorKey, offset: anchorOffset, type: anchorType), focus: Point(key: focusKey, offset: focusOffset, type: focusType), - format: TextFormat()) + styles: [:]) selection.dirty = true editorState.selection = selection @@ -434,11 +404,12 @@ func moveSelectionPointToEnd(point: Point, node: Node) { } } -func transferStartingElementPointToTextPoint(start: Point, end: Point, format: TextFormat, style: String) throws { +func transferStartingElementPointToTextPoint(start: Point, end: Point, styles: StylesDict) throws { guard let element = try start.getNode() as? ElementNode else { return } var placementNode = element.getChildAtIndex(index: start.offset) - let textNode = try createTextNode(text: nil).setFormat(format: format) + let textNode = createTextNode(text: nil) + try textNode.setStyles(styles) var target: Node if isRootNode(node: element) { @@ -449,8 +420,6 @@ func transferStartingElementPointToTextPoint(start: Point, end: Point, format: T target = textNode } - _ = try textNode.setFormat(format: format) - if placementNode == nil { try element.append([target]) } else { diff --git a/Lexical/Core/StyleEvents.swift b/Lexical/Core/StyleEvents.swift index a41a2e75..1eec67db 100644 --- a/Lexical/Core/StyleEvents.swift +++ b/Lexical/Core/StyleEvents.swift @@ -7,6 +7,7 @@ import Foundation +@available(*, deprecated, message: "Please use the new Styles system (see Styles.swift)") public func updateTextFormat(type: TextFormatType, editor: Editor) throws { guard getActiveEditor() != nil else { throw LexicalError.invariantViolation("Must have editor") diff --git a/Lexical/Core/Styles.swift b/Lexical/Core/Styles.swift new file mode 100644 index 00000000..afa7d451 --- /dev/null +++ b/Lexical/Core/Styles.swift @@ -0,0 +1,342 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import UIKit + +// MARK: - Style Swift types + +public struct StyleName: Hashable, RawRepresentable { + public init(rawValue: String) { + self.rawValue = rawValue + } + public var rawValue: String +} + +public enum StyleDisplayConstraint { + case inline + case block + case none +} + +public enum StyleContainerConstraint { + case element + case leaf + case none +} + +public protocol Style { + static var name: StyleName { get } + associatedtype StyleValueType: Codable & Equatable + + // Constraints + static var allowedNodes: [NodeType]? { get } // nil for allow any node type + static var displayConstraint: StyleDisplayConstraint { get } + static var containerConstraint: StyleContainerConstraint { get } + + // Rendering + static func attributes(for value: StyleValueType, theme: Theme) -> Theme.AttributeDict +} + +public extension Style { + static func attributes(for value: StyleValueType, theme: Theme) -> Theme.AttributeDict { + return theme.getStyleConvertedValue(self, value: value) ?? [:] + } +} + +public typealias StylesRegistrationDict = [StyleName : any Style.Type] +public typealias StylesDict = [StyleName : Codable] + +public func stylesDictsAreEqual(_ dict1: StylesDict, _ dict2: StylesDict, editor: Editor) -> Bool { + if dict1.keys != dict2.keys { + return false + } + for key in dict1.keys { + guard let style = editor.registeredStyles[key], let val1 = dict1[key], let val2 = dict2[key] else { + return false + } + return stylesValuesAreEqual(val1, val2, style: style) + } + return true +} + +private func stylesValuesAreEqual(_ val1: Any, _ val2: Any, style: T.Type) -> Bool { + guard let val1 = val1 as? T.StyleValueType, let val2 = val2 as? T.StyleValueType else { + return false + } + return val1 == val2 +} + +// MARK: - Convenience place to put styles + +public enum Styles {} + +// MARK: - Editor API for registering styles + +public extension Editor { + func register(style: T.Type) { + registeredStyles[style.name] = style + } +} + +// MARK: - Storage and manipulation of styles + +internal extension Node { + func validateStyleOrThrow(_ style: T.Type) throws { + if let allowedNodes = style.allowedNodes, !allowedNodes.contains(self.type) { + throw LexicalError.styleValidation("Node not in allowlist") + } + + if style.containerConstraint == .element && !(self is ElementNode) { + throw LexicalError.styleValidation("Style requires element node") + } + if style.containerConstraint == .leaf && self is ElementNode { + throw LexicalError.styleValidation("Style requires leaf node") + } + + if style.displayConstraint == .block { + if let element = self as? ElementNode { + if element.isInline() { + throw LexicalError.styleValidation("Style requires block (aka not inline)") + } + } else { + throw LexicalError.styleValidation("Style requires block, which must be element") + } + } + + if style.displayConstraint == .inline, let element = self as? ElementNode, !element.isInline() { + throw LexicalError.styleValidation("Style requires inline") + } + + if style.containerConstraint == .leaf && self is ElementNode { + throw LexicalError.styleValidation("Style requires leaf node") + } + } +} + +// MARK: - helpers + +// This function exists to facilitate Swift's type conversion from some to any +internal func styleAttributesDictFor(node: Node, style: T.Type, theme: Theme) -> Theme.AttributeDict { + guard let value = node.getStyle(style) else { return [:] } + return style.attributes(for: value, theme: theme) +} + +// MARK: - Commands + +public typealias ApplyStyleCommandPayload = (T.Type, T.StyleValueType?) +public extension CommandType { + static let applyTextStyle = CommandType(rawValue: "applyTextStyle") + static let applyBlockStyle = CommandType(rawValue: "applyBlockStyle") +} + +private typealias ApplyStyleCommandFirstUnpackPayload = (any Style.Type, Any?) +internal func registerStyleCommands(editor: Editor) { + _ = editor.registerCommand(type: .applyTextStyle, listener: { payload in + guard let firstUnpackPayload = payload as? ApplyStyleCommandFirstUnpackPayload else { return false } + return processTextStylePayload(style: firstUnpackPayload.0, payload: firstUnpackPayload) + }) +} + +// This function exists to facilitate Swift's type conversion from some to any +private func processTextStylePayload(style: T.Type, payload: ApplyStyleCommandFirstUnpackPayload) -> Bool { + guard let payload = payload as? ApplyStyleCommandPayload, + let selection = try? getSelection() + else { return false } + + let style = payload.0 + let value = payload.1 + + do { + try selection.applyTextStyle(style, value: value) + } catch { + return false + } + return true +} + +// MARK: - Default styles + +internal func registerDefaultStyles(editor: Editor) { + editor.register(style: Styles.Bold.self) + editor.register(style: Styles.Italic.self) + editor.register(style: Styles.Underline.self) + editor.register(style: Styles.Strikethrough.self) + editor.register(style: Styles.Code.self) + editor.register(style: Styles.SuperScript.self) + editor.register(style: Styles.SubScript.self) +} + +extension StyleName { + static let bold = StyleName(rawValue: "bold") + static let italic = StyleName(rawValue: "italic") + static let underline = StyleName(rawValue: "underline") + static let strikethrough = StyleName(rawValue: "strikethrough") + static let code = StyleName(rawValue: "code") + static let superScript = StyleName(rawValue: "superScript") + static let subScript = StyleName(rawValue: "subScript") +} + +extension Styles { + struct Bold: Style { + typealias StyleValueType = Bool + static var name: StyleName = .bold + static var allowedNodes: [NodeType]? = nil + static var displayConstraint: StyleDisplayConstraint = .inline + static var containerConstraint: StyleContainerConstraint = .leaf + static func attributes(for value: StyleValueType, theme: Theme) -> Theme.AttributeDict { + return theme.getStyleConvertedValue(self, value: value) ?? [.bold : value] + } + } + struct Italic: Style { + typealias StyleValueType = Bool + static var name: StyleName = .italic + static var allowedNodes: [NodeType]? = nil + static var displayConstraint: StyleDisplayConstraint = .inline + static var containerConstraint: StyleContainerConstraint = .leaf + static func attributes(for value: StyleValueType, theme: Theme) -> Theme.AttributeDict { + return theme.getStyleConvertedValue(self, value: value) ?? [.italic : value] + } + } + struct Underline: Style { + typealias StyleValueType = Bool + static var name: StyleName = .underline + static var allowedNodes: [NodeType]? = nil + static var displayConstraint: StyleDisplayConstraint = .inline + static var containerConstraint: StyleContainerConstraint = .leaf + static func attributes(for value: StyleValueType, theme: Theme) -> Theme.AttributeDict { + return theme.getStyleConvertedValue(self, value: value) ?? [.underlineStyle : value ? NSUnderlineStyle.single.rawValue : []] + } + } + struct Strikethrough: Style { + typealias StyleValueType = Bool + static var name: StyleName = .strikethrough + static var allowedNodes: [NodeType]? = nil + static var displayConstraint: StyleDisplayConstraint = .inline + static var containerConstraint: StyleContainerConstraint = .leaf + static func attributes(for value: StyleValueType, theme: Theme) -> Theme.AttributeDict { + return theme.getStyleConvertedValue(self, value: value) ?? [.strikethroughStyle : value ? NSUnderlineStyle.single.rawValue : []] + } + } + struct Code: Style { + typealias StyleValueType = Bool + static var name: StyleName = .code + static var allowedNodes: [NodeType]? = nil + static var displayConstraint: StyleDisplayConstraint = .inline + static var containerConstraint: StyleContainerConstraint = .leaf + static func attributes(for value: StyleValueType, theme: Theme) -> Theme.AttributeDict { + return theme.getStyleConvertedValue(self, value: value) ?? (value ? [.fontFamily : "Courier", .backgroundColor : UIColor.lightGray] : [:]) + } + } + struct SuperScript: Style { + typealias StyleValueType = Bool + static var name: StyleName = .superScript + static var allowedNodes: [NodeType]? = nil + static var displayConstraint: StyleDisplayConstraint = .inline + static var containerConstraint: StyleContainerConstraint = .leaf + static func attributes(for value: StyleValueType, theme: Theme) -> Theme.AttributeDict { + return theme.getStyleConvertedValue(self, value: value) ?? [:] + } + } + struct SubScript: Style { + typealias StyleValueType = Bool + static var name: StyleName = .subScript + static var allowedNodes: [NodeType]? = nil + static var displayConstraint: StyleDisplayConstraint = .inline + static var containerConstraint: StyleContainerConstraint = .leaf + static func attributes(for value: StyleValueType, theme: Theme) -> Theme.AttributeDict { + return theme.getStyleConvertedValue(self, value: value) ?? [:] + } + } +} + + +// MARK: - Compatibility with previous versions + +public func compatibilityFormatFromStyles(_ stylesDict: StylesDict) -> TextFormat { + var format = TextFormat() + format.bold = (stylesDict[Styles.Bold.name] as? Bool) ?? false + format.italic = (stylesDict[Styles.Italic.name] as? Bool) ?? false + format.underline = (stylesDict[Styles.Underline.name] as? Bool) ?? false + format.strikethrough = (stylesDict[Styles.Strikethrough.name] as? Bool) ?? false + format.code = (stylesDict[Styles.Code.name] as? Bool) ?? false + format.superScript = (stylesDict[Styles.SuperScript.name] as? Bool) ?? false + format.subScript = (stylesDict[Styles.SubScript.name] as? Bool) ?? false + return format +} + +public func compatibilityStylesFromFormat(_ format: TextFormat) -> StylesDict { + var styles: StylesDict = [:] + if format.isTypeSet(type: .bold) { + styles[Styles.Bold.name] = true + } + if format.isTypeSet(type: .italic) { + styles[Styles.Italic.name] = true + } + if format.isTypeSet(type: .underline) { + styles[Styles.Underline.name] = true + } + if format.isTypeSet(type: .strikethrough) { + styles[Styles.Strikethrough.name] = true + } + if format.isTypeSet(type: .code) { + styles[Styles.Code.name] = true + } + if format.isTypeSet(type: .superScript) { + styles[Styles.SuperScript.name] = true + } + if format.isTypeSet(type: .subScript) { + styles[Styles.SubScript.name] = true + } + return styles +} + +public func compatibilityStyleFromFormatType(_ format: TextFormatType) -> any Style.Type { + switch format { + case .bold: + return Styles.Bold.self + case .italic: + return Styles.Italic.self + case .underline: + return Styles.Underline.self + case .strikethrough: + return Styles.Strikethrough.self + case .code: + return Styles.Code.self + case .subScript: + return Styles.SubScript.self + case .superScript: + return Styles.SuperScript.self + } +} + +public func compatibilityMergeStylesAssumingAllFormats(old: StylesDict, newFormats: StylesDict) -> StylesDict { + var new = old + new[Styles.Bold.name] = newFormats[Styles.Bold.name] ?? false + new[Styles.Italic.name] = newFormats[Styles.Italic.name] ?? false + new[Styles.Underline.name] = newFormats[Styles.Underline.name] ?? false + new[Styles.Strikethrough.name] = newFormats[Styles.Strikethrough.name] ?? false + new[Styles.Code.name] = newFormats[Styles.Code.name] ?? false + new[Styles.SubScript.name] = newFormats[Styles.SubScript.name] ?? false + new[Styles.SuperScript.name] = newFormats[Styles.SuperScript.name] ?? false + return new +} + +// MARK: - JSON serialization + +internal struct StyleCodingKeys: CodingKey { + var stringValue: String + var intValue: Int? + init?(intValue: Int) { + self.stringValue = "\(intValue)" + self.intValue = intValue + } + init?(stringValue: String) { self.stringValue = stringValue } +} + +internal func styleValueFromDecoder(_ style: T.Type, decoder: Decoder) throws -> T.StyleValueType? { + return try style.StyleValueType.init(from: decoder) +} diff --git a/Lexical/Core/Utils.swift b/Lexical/Core/Utils.swift index e8ca7e16..10f1c16c 100644 --- a/Lexical/Core/Utils.swift +++ b/Lexical/Core/Utils.swift @@ -138,6 +138,7 @@ public func createCodeHighlightNode(text: String, highlightType: String?) -> Cod CodeHighlightNode(text: text, highlightType: highlightType) } +@available(*, deprecated, message: "Use new styles system") public func toggleTextFormatType(format: TextFormat, type: TextFormatType, alignWithFormat: TextFormat?) -> TextFormat { var activeFormat = format let isStateFlagPresent = format.isTypeSet(type: type) @@ -265,18 +266,19 @@ public func getNodeHierarchy(editorState: EditorState?) throws -> String { let indentation = (0..(node: T) throws -> Node { let latest = node.getLatest() let clone = latest.clone() clone.parent = latest.parent + clone.styles = latest.styles if let latestTextNode = latest as? TextNode, let latestCloneNode = clone as? TextNode { - latestCloneNode.format = latestTextNode.format - latestCloneNode.style = latestTextNode.style latestCloneNode.mode = latestTextNode.mode latestCloneNode.detail = latestTextNode.detail return latestCloneNode @@ -23,8 +22,7 @@ public func cloneWithProperties(node: T) throws -> Node { let latestCloneNode = clone as? ElementNode { latestCloneNode.children = latestElementNode.children latestCloneNode.direction = latestElementNode.direction - // latestCloneNode.indent = latestElementNode.indent - // latestCloneNode.format = latestElementNode.format + latestCloneNode.indent = latestElementNode.indent return latestCloneNode // } else if ($isDecoratorNode(latest) && $isDecoratorNode(clone)) { // clone.state = latest.state diff --git a/Lexical/Helper/Theme.swift b/Lexical/Helper/Theme.swift index fbf8850e..8d52f0d4 100644 --- a/Lexical/Helper/Theme.swift +++ b/Lexical/Helper/Theme.swift @@ -96,4 +96,25 @@ import Foundation public func setBlockLevelAttributes(_ nodeType: NodeType, value: BlockLevelAttributes?) { blockLevelAttributes[nodeType] = value } + + // Style theme + + typealias StyleValueBlock = (Any) -> AttributeDict + private var styleAttributes: [StyleName: StyleValueBlock] = [:] + + public func getStyleConvertedValue(_ style: T.Type, value: T.StyleValueType) -> AttributeDict? { + if let converter = styleAttributes[style.name] { + return converter(value) + } + return nil + } + + public func setStyleValueConverter(_ style: T.Type, converter: @escaping (T.StyleValueType) -> AttributeDict) { + styleAttributes[style.name] = { input in + if let input = input as? T.StyleValueType { + return converter(input) + } + return [:] + } + } } diff --git a/LexicalTests/Helpers/TestAttributesNode.swift b/LexicalTests/Helpers/TestAttributesNode.swift index c2ab81cb..6f3f6ba1 100644 --- a/LexicalTests/Helpers/TestAttributesNode.swift +++ b/LexicalTests/Helpers/TestAttributesNode.swift @@ -12,12 +12,18 @@ import Lexical class TestAttributesNode: ElementNode { override required init() { super.init() - self.type = NodeType(rawValue: "TestNode") } public required init(from decoder: Decoder) throws { try super.init(from: decoder) - self.type = NodeType(rawValue: "TestNode") + } + + required init(styles: StylesDict, key: NodeKey?) { + fatalError("init(styles:key:) has not been implemented") + } + + override class func getType() -> NodeType { + return NodeType(rawValue: "TestNode") } override open func getAttributedStringAttributes(theme: Theme) -> [NSAttributedString.Key: Any] { diff --git a/LexicalTests/Tests/AttributesUtilsTests.swift b/LexicalTests/Tests/AttributesUtilsTests.swift index ecd64833..6b3bfc6a 100644 --- a/LexicalTests/Tests/AttributesUtilsTests.swift +++ b/LexicalTests/Tests/AttributesUtilsTests.swift @@ -84,7 +84,7 @@ class AttributesUtilsTests: XCTestCase { try editor.update { let textNode = TextNode() try textNode.setText("hello world") - textNode.format.bold = true + try textNode.setStyle(Styles.Bold.self, true) let paragraphNode = ParagraphNode() try paragraphNode.append([textNode]) @@ -103,7 +103,7 @@ class AttributesUtilsTests: XCTestCase { XCTAssertTrue(font.fontDescriptor.symbolicTraits.contains(.traitBold), "Font should contain the bold trait") - textNode.format.bold = false + try textNode.setStyle(Styles.Bold.self, false) // let attributedString = NSMutableAttributedString(string: textNode.getTextPart()) let newStyledAttrStr = AttributeUtils.attributedStringByAddingStyles(attributedString, from: textNode, state: editorState, theme: editor.getTheme()) let newFont = firstFontInAttributedString(attrStr: newStyledAttrStr) @@ -119,7 +119,7 @@ class AttributesUtilsTests: XCTestCase { try editor.update { let textNode = TextNode() try textNode.setText("hello world") - textNode.format.italic = true + try textNode.setStyle(Styles.Italic.self, true) let paragraphNode = ParagraphNode() try paragraphNode.append([textNode]) @@ -138,7 +138,7 @@ class AttributesUtilsTests: XCTestCase { XCTAssertTrue(font.fontDescriptor.symbolicTraits.contains(.traitItalic), "Font should contain the italic trait") - textNode.format.italic = false + try textNode.setStyle(Styles.Italic.self, false) let newStyledAttrStr = AttributeUtils.attributedStringByAddingStyles(attributedString, from: textNode, state: editorState, theme: editor.getTheme()) let newFont = firstFontInAttributedString(attrStr: newStyledAttrStr) @@ -153,8 +153,8 @@ class AttributesUtilsTests: XCTestCase { try editor.update { let textNode = TextNode() try textNode.setText("hello world") - textNode.format.bold = true - textNode.format.italic = true + try textNode.setStyle(Styles.Bold.self, true) + try textNode.setStyle(Styles.Italic.self, true) let paragraphNode = ParagraphNode() try paragraphNode.append([textNode]) @@ -174,7 +174,7 @@ class AttributesUtilsTests: XCTestCase { XCTAssertTrue(font.fontDescriptor.symbolicTraits.contains(.traitBold), "Font should contain the bold trait") XCTAssertTrue(font.fontDescriptor.symbolicTraits.contains(.traitItalic), "Font should contain the italic trait") - textNode.format.bold = false + try textNode.setStyle(Styles.Bold.self, false) let newStyledAttrStr = AttributeUtils.attributedStringByAddingStyles(attributedString, from: textNode, state: editorState, theme: editor.getTheme()) let newFont = firstFontInAttributedString(attrStr: newStyledAttrStr) diff --git a/LexicalTests/Tests/DecoratorNodeTests.swift b/LexicalTests/Tests/DecoratorNodeTests.swift index 638d0a8c..94a1ea03 100644 --- a/LexicalTests/Tests/DecoratorNodeTests.swift +++ b/LexicalTests/Tests/DecoratorNodeTests.swift @@ -31,7 +31,11 @@ class TestDecoratorNode: DecoratorNode { required init(from decoder: Decoder) throws { fatalError("init(from:) has not been implemented") } - + + required init(styles: StylesDict, key: NodeKey?) { + fatalError("init(styles:key:) has not been implemented") + } + override public func clone() -> Self { Self(numTimes: numberOfTimesDecorateHasBeenCalled, key: key) } diff --git a/LexicalTests/Tests/NodeTests.swift b/LexicalTests/Tests/NodeTests.swift index d39d6a2f..116da2f5 100644 --- a/LexicalTests/Tests/NodeTests.swift +++ b/LexicalTests/Tests/NodeTests.swift @@ -32,7 +32,7 @@ class NodeTests: XCTestCase { try editor.update { let node = Node() let node2 = Node() - let node3 = Node(LexicalConstants.uninitializedNodeKey) + let node3 = Node(styles: [:], key: LexicalConstants.uninitializedNodeKey) XCTAssertNotNil(node) XCTAssertNotNil(node2) XCTAssertEqual(node.key, "1") @@ -128,34 +128,17 @@ class NodeTests: XCTestCase { } } - func testTextNodeFormatSerialization() throws { - try editor.update { - let textNode = TextNode() - try textNode.setText("hello world") - var textFormat = TextFormat() - textFormat.bold = true - textFormat.underline = true - textNode.format = textFormat - - let encoder = JSONEncoder() - let data = try encoder.encode(textNode) - guard let jsonString = String(data: data, encoding: .utf8) else { return } - print(jsonString) - XCTAssertTrue(jsonString.contains("\"format\":9")) - } - } - func testTextNodeFormatDeserialization() throws { let jsonString = "{\"format\":9,\"detail\":0,\"style\":\"\",\"mode\":\"normal\",\"text\":\"hello world\",\"version\":1,\"type\":\"text\"}" let decoder = JSONDecoder() let decodedNode = try decoder.decode(TextNode.self, from: (jsonString.data(using: .utf8) ?? Data())) - XCTAssertTrue(decodedNode.format.bold) - XCTAssertTrue(decodedNode.format.underline) - XCTAssertFalse(decodedNode.format.strikethrough) - XCTAssertFalse(decodedNode.format.code) - XCTAssertFalse(decodedNode.format.superScript) - XCTAssertFalse(decodedNode.format.subScript) + XCTAssertTrue(decodedNode.getStyle(Styles.Bold.self) ?? false) + XCTAssertTrue(decodedNode.getStyle(Styles.Underline.self) ?? false) + XCTAssertFalse(decodedNode.getStyle(Styles.Strikethrough.self) ?? false) + XCTAssertFalse(decodedNode.getStyle(Styles.Code.self) ?? false) + XCTAssertFalse(decodedNode.getStyle(Styles.SuperScript.self) ?? false) + XCTAssertFalse(decodedNode.getStyle(Styles.SubScript.self) ?? false) } func testParagraphNode() throws { @@ -1324,7 +1307,7 @@ class NodeTests: XCTestCase { let textNode = TextNode() try textNode.setText("hello ") - textNode.format = TextFormat() + try textNode.setStyles([:]) let paragraphNode = ParagraphNode() try paragraphNode.append([textNode]) @@ -1346,7 +1329,7 @@ class NodeTests: XCTestCase { let textNode = TextNode() try textNode.setText("hello ") - textNode.format = TextFormat() + try textNode.setStyles([:]) let paragraphNode = ParagraphNode() try paragraphNode.append([textNode]) @@ -1383,9 +1366,8 @@ class NodeTests: XCTestCase { textNode2 = try textNode.setFormat(format: format) - XCTAssertEqual(textNode2.getAttributedStringAttributes(theme: editor.getTheme()).count, 1) - XCTAssertTrue(textNode2.getAttributedStringAttributes(theme: editor.getTheme()).contains(where: { $0.key == .bold })) - XCTAssertFalse(textNode2.getAttributedStringAttributes(theme: editor.getTheme()).contains(where: { $0.key == .italic })) + XCTAssertEqual(textNode2.getAttributedStringAttributes(theme: editor.getTheme())[.bold] as? Bool ?? false, true) + XCTAssertEqual(textNode2.getAttributedStringAttributes(theme: editor.getTheme())[.italic] as? Bool ?? false, false) } } @@ -1707,12 +1689,8 @@ class NodeTests: XCTestCase { try textNode2.setText("hello ") textNode2.mode = .inert - let textNode3 = TextNode() - textNode3.type = NodeType.paragraph - XCTAssertTrue(textNode.isSimpleText()) XCTAssertFalse(textNode2.isSimpleText()) - XCTAssertFalse(textNode3.isSimpleText()) } } @@ -2062,7 +2040,7 @@ class NodeTests: XCTestCase { _ = TextNode.canSimpleTextNodesBeMerged(node1: textNode, node2: textNode2) XCTAssertFalse(textNode.mode == textNode2.mode) - XCTAssertEqual(textNode.format, textNode2.format) + XCTAssertTrue(stylesDictsAreEqual(textNode.styles, textNode2.styles, editor: editor)) } } diff --git a/LexicalTests/Tests/SelectionTests.swift b/LexicalTests/Tests/SelectionTests.swift index 56062b5a..1fd9e68d 100644 --- a/LexicalTests/Tests/SelectionTests.swift +++ b/LexicalTests/Tests/SelectionTests.swift @@ -757,13 +757,20 @@ class SelectionTests: XCTestCase { let start = createPoint(key: "1", offset: 5, type: .text) let end = createPoint(key: "1", offset: 15, type: .text) selection = RangeSelection(anchor: start, focus: end, format: TextFormat()) - XCTAssertEqual(selection.format.bold, false) + getActiveEditorState()?.selection = selection + XCTAssertEqual(selection.styles[Styles.Bold.name] as? Bool ?? false, false) try selection.formatText(formatType: .bold) - XCTAssertEqual(selection.format.bold, true) + XCTAssertEqual(selection.styles[Styles.Bold.name] as? Bool ?? false, true) + + XCTAssertEqual(getSelectionAssumingRangeSelection(), selection) + XCTAssertEqual(getSelectionAssumingRangeSelection().styles[Styles.Bold.name] as? Bool ?? false, true) + } try editor.read { + XCTAssertEqual(getSelectionAssumingRangeSelection().styles[Styles.Bold.name] as? Bool ?? false, true) + // 2 new textNodes should have been created var textNodes = editor.getEditorState().nodeMap.values.filter { $0.type == NodeType.text && $0.parent != nil } textNodes = textNodes.sorted(by: { $0.key < $1.key }) @@ -781,15 +788,20 @@ class SelectionTests: XCTestCase { return } - XCTAssertEqual(textNode0.format.bold, false) - XCTAssertEqual(textNode1.format.bold, true) - XCTAssertEqual(textNode2.format.bold, false) + XCTAssertEqual(getSelectionAssumingRangeSelection().styles[Styles.Bold.name] as? Bool ?? false, true) + + + XCTAssertEqual(textNode0.getStyle(Styles.Bold.self) ?? false, false) + XCTAssertEqual(textNode1.getStyle(Styles.Bold.self) ?? false, true) + XCTAssertEqual(textNode2.getStyle(Styles.Bold.self) ?? false, false) } try editor.update { + XCTAssertEqual(getSelectionAssumingRangeSelection().styles[Styles.Italic.name] as? Bool ?? false, false) + XCTAssertEqual(getSelectionAssumingRangeSelection().styles[Styles.Bold.name] as? Bool ?? false, true) try updateTextFormat(type: .italic, editor: editor) - XCTAssertEqual(getSelectionAssumingRangeSelection().format.italic, true) - XCTAssertEqual(getSelectionAssumingRangeSelection().format.bold, true) + XCTAssertEqual(getSelectionAssumingRangeSelection().styles[Styles.Italic.name] as? Bool ?? false, true) + XCTAssertEqual(getSelectionAssumingRangeSelection().styles[Styles.Bold.name] as? Bool ?? false, true) } } @@ -810,12 +822,12 @@ class SelectionTests: XCTestCase { let start = createPoint(key: "1", offset: 4, type: .text) let end = createPoint(key: "3", offset: 7, type: .text) selection = RangeSelection(anchor: start, focus: end, format: TextFormat()) - XCTAssertEqual(selection.format.bold, false) + XCTAssertEqual(selection.styles[Styles.Bold.name] as? Bool ?? false, false) try selection.formatText(formatType: .bold) XCTAssertEqual(selection.anchor.offset, 0) XCTAssertEqual(selection.focus.offset, 7) - XCTAssertEqual(selection.format.bold, true) + XCTAssertEqual(selection.styles[Styles.Bold.name] as? Bool ?? false, true) } try editor.read { @@ -919,11 +931,11 @@ class SelectionTests: XCTestCase { let start = createPoint(key: "1", offset: 5, type: .text) let end = createPoint(key: "1", offset: 15, type: .text) selection = RangeSelection(anchor: start, focus: end, format: TextFormat()) - XCTAssertEqual(selection.format.bold, false) + XCTAssertEqual(selection.styles[Styles.Bold.name] as? Bool ?? false, false) // make "testing to" to bold try selection.formatText(formatType: .bold) - XCTAssertEqual(selection.format.bold, true) + XCTAssertEqual(selection.styles[Styles.Bold.name] as? Bool ?? false, true) } // make "ing to verify" underlined @@ -933,10 +945,10 @@ class SelectionTests: XCTestCase { let start = createPoint(key: "3", offset: 4, type: .text) let end = createPoint(key: "4", offset: 7, type: .text) selection = RangeSelection(anchor: start, focus: end, format: TextFormat()) - XCTAssertEqual(selection.format.bold, false) + XCTAssertEqual(selection.styles[Styles.Bold.name] as? Bool ?? false, false) try selection.formatText(formatType: .underline) - XCTAssertEqual(selection.format.underline, true) + XCTAssertEqual(selection.styles[Styles.Underline.name] as? Bool ?? false, true) } try editor.read { @@ -953,30 +965,30 @@ class SelectionTests: XCTestCase { // "I am" should not have any format if let textNode = textNodes[0] as? TextNode { - XCTAssertFalse(textNode.format.bold) + XCTAssertFalse(textNode.getStyle(Styles.Bold.self) ?? false) } // "test" should be bold if let textNode = textNodes[1] as? TextNode { - XCTAssertTrue(textNode.format.bold) + XCTAssertTrue(textNode.getStyle(Styles.Bold.self) ?? false) } // " verify" should be underlined if let textNode = textNodes[2] as? TextNode { - XCTAssertTrue(textNode.format.underline) - XCTAssertFalse(textNode.format.bold) + XCTAssertTrue(textNode.getStyle(Styles.Underline.self) ?? false) + XCTAssertFalse(textNode.getStyle(Styles.Bold.self) ?? false) } // "ing to" should bold and underline if let textNode = textNodes[3] as? TextNode { - XCTAssertTrue(textNode.format.bold) - XCTAssertTrue(textNode.format.underline) + XCTAssertTrue(textNode.getStyle(Styles.Bold.self) ?? false) + XCTAssertTrue(textNode.getStyle(Styles.Underline.self) ?? false) } // "format updates !!" shouldn't have any format if let textNode = textNodes[4] as? TextNode { - XCTAssertFalse(textNode.format.bold) - XCTAssertFalse(textNode.format.underline) + XCTAssertFalse(textNode.getStyle(Styles.Bold.self) ?? false) + XCTAssertFalse(textNode.getStyle(Styles.Underline.self) ?? false) } } @@ -1004,24 +1016,24 @@ class SelectionTests: XCTestCase { // "I am" should not have any format if let textNode = textNodes[0] as? TextNode { - XCTAssertFalse(textNode.format.bold) + XCTAssertFalse(textNode.getStyle(Styles.Bold.self) ?? false) } // "test" should be bold if let textNode = textNodes[1] as? TextNode { - XCTAssertTrue(textNode.format.bold) + XCTAssertTrue(textNode.getStyle(Styles.Bold.self) ?? false) } // "ing to verify" should be underlined but not bold if let textNode = textNodes[2] as? TextNode { - XCTAssertTrue(textNode.format.underline) - XCTAssertFalse(textNode.format.bold) + XCTAssertTrue(textNode.getStyle(Styles.Underline.self) ?? false) + XCTAssertFalse(textNode.getStyle(Styles.Bold.self) ?? false) } // "format updates !!" shouldn't have any format if let textNode = textNodes[3] as? TextNode { - XCTAssertFalse(textNode.format.bold) - XCTAssertFalse(textNode.format.underline) + XCTAssertFalse(textNode.getStyle(Styles.Bold.self) ?? false) + XCTAssertFalse(textNode.getStyle(Styles.Underline.self) ?? false) } } } @@ -1114,7 +1126,7 @@ class SelectionTests: XCTestCase { XCTAssertEqual(textNode1.getTextPart(), "Hello world ") XCTAssertEqual(textNode2.getTextPart(), "again") - XCTAssertTrue(textNode2.format.bold) + XCTAssertTrue(textNode2.getStyle(Styles.Bold.self) ?? false) } } diff --git a/LexicalTests/Tests/SelectionUtilsTests.swift b/LexicalTests/Tests/SelectionUtilsTests.swift index 805feb09..745071fb 100644 --- a/LexicalTests/Tests/SelectionUtilsTests.swift +++ b/LexicalTests/Tests/SelectionUtilsTests.swift @@ -533,7 +533,7 @@ class SelectionUtilsTests: XCTestCase { var textFormat = TextFormat() textFormat.bold = true - newSelection2.format = textFormat + newSelection2.styles = compatibilityStylesFromFormat(textFormat) XCTAssertFalse(editor.getEditorState().selection?.isSelection(newSelection2) ?? false) } @@ -561,8 +561,7 @@ class SelectionUtilsTests: XCTestCase { try transferStartingElementPointToTextPoint( start: startPoint, end: endPoint, - format: TextFormat(), - style: "" + styles: [:] ) ) } diff --git a/LexicalTests/Tests/SerializationTests.swift b/LexicalTests/Tests/SerializationTests.swift index af7468b7..5eddc091 100644 --- a/LexicalTests/Tests/SerializationTests.swift +++ b/LexicalTests/Tests/SerializationTests.swift @@ -86,56 +86,66 @@ class SerializationTests: XCTestCase { func testWebFormatJSONImporting() throws { try editor.update { let decoder = JSONDecoder() - let decodedNodeArray = try decoder.decode(SerializedEditorState.self, from: (jsonString.data(using: .utf8) ?? Data())) - guard let rootNode = decodedNodeArray.rootNode else { - XCTFail("Failed to decode RootNode") - return + do { + let decodedNodeArray = try decoder.decode(SerializedEditorState.self, from: (jsonString.data(using: .utf8) ?? Data())) + + guard let rootNode = decodedNodeArray.rootNode else { + XCTFail("Failed to decode RootNode") + return + } + guard let selection = try getSelection() as? RangeSelection else { + XCTFail("Could not get selection") + return + } + + _ = try insertGeneratedNodes(editor: editor, nodes: rootNode.getChildren(), selection: selection) + + } catch { + XCTFail("Error in decoding \(error)") } - - guard let selection = try getSelection() as? RangeSelection else { - XCTFail("Could not get selection") - return - } - - _ = try insertGeneratedNodes(editor: editor, nodes: rootNode.getChildren(), selection: selection) } try editor.read { - let rootNode = editor.getEditorState().getRootNode() - XCTAssertEqual(rootNode?.children.count, 4) + guard let rootNode = getRoot() else { + XCTFail("No root") + return + } + let children = rootNode.children + XCTAssertEqual(children.count, 4) - guard let firstParagraph = rootNode?.getChildren()[0] as? ParagraphNode else { + guard let firstParagraph = rootNode.getChildren()[0] as? ParagraphNode else { XCTFail("Could not get first ParagraphNode") return } XCTAssertEqual(firstParagraph.children.count, 7) XCTAssertEqual(firstParagraph.getTextContent(), "This is bold italic underline text in the first paragraph.\n") - XCTAssertTrue((firstParagraph.getChildren()[1] as? TextNode)?.format.bold ?? false) - XCTAssertTrue((firstParagraph.getChildren()[3] as? TextNode)?.format.italic ?? false) - XCTAssertTrue((firstParagraph.getChildren()[5] as? TextNode)?.format.underline ?? false) + XCTAssertTrue((firstParagraph.getChildren()[1] as? TextNode)?.getStyle(Styles.Bold.self) ?? false) + XCTAssertTrue((firstParagraph.getChildren()[3] as? TextNode)?.getStyle(Styles.Italic.self) ?? false) + XCTAssertTrue((firstParagraph.getChildren()[5] as? TextNode)?.getStyle(Styles.Underline.self) ?? false) - guard let secondPargraph = rootNode?.getChildren()[1] as? ParagraphNode else { + guard let secondPargraph = rootNode.getChildren()[1] as? ParagraphNode else { XCTFail("Could not get second ParagraphNode") return } XCTAssertEqual(secondPargraph.children.count, 1) XCTAssertEqual(secondPargraph.getTextContent(), "This is another paragraph.\n") - guard let childrenSize = rootNode?.getChildrenSize(), childrenSize >= 3, let thirdParagraph = rootNode?.getChildren()[2] as? ParagraphNode else { + let childrenSize = rootNode.getChildrenSize() + guard childrenSize >= 3, let thirdParagraph = rootNode.getChildren()[2] as? ParagraphNode else { XCTFail("Could not get third ParagraphNode") return } XCTAssertEqual(thirdParagraph.children.count, 1) XCTAssertEqual(thirdParagraph.getTextContent(), "This is a code line.\n") - XCTAssertTrue((thirdParagraph.getChildren().first as? TextNode)?.format.code ?? false) + XCTAssertTrue((thirdParagraph.getChildren().first as? TextNode)?.getStyle(Styles.Code.self) ?? false) - guard let fourthParagraph = rootNode?.getChildren()[3] as? ParagraphNode else { + guard let fourthParagraph = rootNode.getChildren()[3] as? ParagraphNode else { XCTFail("Could not get fourth ParagraphNode") return } XCTAssertEqual(fourthParagraph.children.count, 2) XCTAssertEqual(fourthParagraph.getTextContent(), "This is strikethrough") - XCTAssertTrue((fourthParagraph.getChildren().last as? TextNode)?.format.strikethrough ?? false) + XCTAssertTrue((fourthParagraph.getChildren().last as? TextNode)?.getStyle(Styles.Strikethrough.self) ?? false) } } @@ -165,9 +175,11 @@ class SerializationTests: XCTestCase { XCTAssertEqual(text, "This is bold italic underline text in the first paragraph.\nThis is another paragraph.\nThis is a code line.\nThis is strikethrough") } + let roundtripJSONWithStyles = "{\"root\":{\"direction\":\"ltr\",\"indent\":0,\"children\":[{\"type\":\"paragraph\",\"children\":[{\"type\":\"text\",\"mode\":\"normal\",\"text\":\"This is \",\"detail\":0,\"version\":1},{\"detail\":0,\"type\":\"text\",\"mode\":\"normal\",\"text\":\"bold\",\"styles\":{\"bold\":true},\"version\":1},{\"text\":\" \",\"version\":1,\"type\":\"text\",\"mode\":\"normal\",\"detail\":0},{\"styles\":{\"italic\":true},\"version\":1,\"text\":\"italic\",\"mode\":\"normal\",\"detail\":0,\"type\":\"text\"},{\"mode\":\"normal\",\"text\":\" \",\"detail\":0,\"version\":1,\"type\":\"text\"},{\"type\":\"text\",\"detail\":0,\"styles\":{\"underline\":true},\"text\":\"underline\",\"version\":1,\"mode\":\"normal\"},{\"text\":\" text in the first paragraph.\",\"version\":1,\"type\":\"text\",\"mode\":\"normal\",\"detail\":0}],\"indent\":0,\"direction\":\"ltr\",\"version\":1},{\"version\":1,\"children\":[{\"type\":\"text\",\"detail\":0,\"text\":\"This is another paragraph.\",\"version\":1,\"mode\":\"normal\"}],\"direction\":\"ltr\",\"indent\":0,\"type\":\"paragraph\"},{\"direction\":\"ltr\",\"children\":[{\"detail\":0,\"text\":\"This is a code line.\",\"version\":1,\"styles\":{\"code\":true},\"type\":\"text\",\"mode\":\"normal\"}],\"indent\":0,\"version\":1,\"type\":\"paragraph\"},{\"version\":1,\"direction\":\"ltr\",\"indent\":0,\"type\":\"paragraph\",\"children\":[{\"detail\":0,\"text\":\"This is \",\"type\":\"text\",\"version\":1,\"mode\":\"normal\"},{\"type\":\"text\",\"text\":\"strikethrough\",\"mode\":\"normal\",\"detail\":0,\"version\":1,\"styles\":{\"strikethrough\":true}}]}],\"type\":\"root\",\"version\":1}}" + func testFromToJSONMethods() throws { let headlessEditor = Editor.createHeadless(editorConfig: EditorConfig(theme: Theme(), plugins: [])) - let editorState = try EditorState.fromJSON(json: jsonString, editor: headlessEditor) + let editorState = try EditorState.fromJSON(json: roundtripJSONWithStyles, editor: headlessEditor) try headlessEditor.read { XCTAssertEqual(getRoot()?.getTextContent(), "", "Expected empty string") @@ -180,15 +192,15 @@ class SerializationTests: XCTestCase { try headlessEditor.setEditorState(editorState) + var jsonResult: String = "" try headlessEditor.read { let text = getRoot()?.getTextContent() XCTAssertEqual(text, "This is bold italic underline text in the first paragraph.\nThis is another paragraph.\nThis is a code line.\nThis is strikethrough", "Expected text in editor") + jsonResult = try editorState.toJSON() } - let jsonResult = try editorState.toJSON() - // test json equality - guard let comparisonJSONData = jsonString.data(using: .utf8), let outputJSONData = jsonResult.data(using: .utf8) else { + guard let comparisonJSONData = roundtripJSONWithStyles.data(using: .utf8), let outputJSONData = jsonResult.data(using: .utf8) else { XCTFail("couldn't convert to data") return } diff --git a/LexicalTests/Tests/TextViewTests.swift b/LexicalTests/Tests/TextViewTests.swift index 3c5ab0d7..9452610d 100644 --- a/LexicalTests/Tests/TextViewTests.swift +++ b/LexicalTests/Tests/TextViewTests.swift @@ -293,9 +293,9 @@ class TextViewTests: XCTestCase { XCTAssertTrue((nodemap["0"] as? ParagraphNode)?.children.count == 2) XCTAssertTrue((nodemap["1"] as? TextNode)?.getTextPart() == "Hello world") XCTAssertTrue((nodemap["2"] as? TextNode)?.getTextPart() == "Test") - XCTAssertTrue((nodemap["2"] as? TextNode)?.format.underline ?? false) + XCTAssertTrue((nodemap["2"] as? TextNode)?.getStyle(Styles.Underline.self) ?? false) XCTAssertTrue((nodemap["3"] as? TextNode)?.getTextPart() == "Text") - XCTAssertTrue((nodemap["3"] as? TextNode)?.format.underline ?? false) + XCTAssertTrue((nodemap["3"] as? TextNode)?.getStyle(Styles.Underline.self) ?? false) XCTAssertTrue((nodemap["4"] as? ParagraphNode)?.children.count == 1) } } diff --git a/LexicalTests/Tests/UpdatesTests.swift b/LexicalTests/Tests/UpdatesTests.swift index 9abf2eaa..4cceff96 100644 --- a/LexicalTests/Tests/UpdatesTests.swift +++ b/LexicalTests/Tests/UpdatesTests.swift @@ -18,7 +18,7 @@ class UpdatesTests: XCTestCase { try editor.update { let editorState = getActiveEditorState() XCTAssertNotNil(editorState) - node = Node() + node = Node(styles: [:], key: nil) guard let node else { XCTFail("should have node") diff --git a/LexicalTests/Tests/UtilsTests.swift b/LexicalTests/Tests/UtilsTests.swift index 4ac5985c..c686f202 100644 --- a/LexicalTests/Tests/UtilsTests.swift +++ b/LexicalTests/Tests/UtilsTests.swift @@ -111,7 +111,7 @@ class UtilsTests: XCTestCase { } selection = newSelection } - XCTAssertEqual(selection?.format.bold, true) + XCTAssertEqual(selection?.styles[Styles.Bold.name] as? Bool, true) try view.textView.defaultClearEditor() @@ -123,6 +123,6 @@ class UtilsTests: XCTestCase { selection = newSelection } print("updatedSelection: \(selection.debugDescription)") - XCTAssertEqual(selection?.format.bold, false) + XCTAssertEqual(selection?.styles[Styles.Bold.name] as? Bool ?? false, false) } } diff --git a/Playground/LexicalPlayground/ExportOutputViewController.swift b/Playground/LexicalPlayground/ExportOutputViewController.swift index 7b495191..7bcb1e52 100644 --- a/Playground/LexicalPlayground/ExportOutputViewController.swift +++ b/Playground/LexicalPlayground/ExportOutputViewController.swift @@ -40,8 +40,12 @@ class ExportOutputViewController: UIViewController { func generateJSON(editor: Editor) { let currentEditorState = editor.getEditorState() - if let jsonString = try? currentEditorState.toJSON() { - output = jsonString + do { + try editor.read { + self.output = try currentEditorState.toJSON() + } + } catch { + print("JSON error: \(error)") } } diff --git a/Plugins/LexicalInlineImagePlugin/LexicalInlineImagePlugin/Nodes/ImageNode.swift b/Plugins/LexicalInlineImagePlugin/LexicalInlineImagePlugin/Nodes/ImageNode.swift index 905395e0..0cf9ddbb 100644 --- a/Plugins/LexicalInlineImagePlugin/LexicalInlineImagePlugin/Nodes/ImageNode.swift +++ b/Plugins/LexicalInlineImagePlugin/LexicalInlineImagePlugin/Nodes/ImageNode.swift @@ -23,27 +23,30 @@ public class ImageNode: DecoratorNode { var size = CGSize.zero var sourceID: String = "" + public override class func getType() -> NodeType { + return .image + } + public required init(url: String, size: CGSize, sourceID: String, key: NodeKey? = nil) { super.init(key) self.url = URL(string: url) self.size = size - self.type = NodeType.image self.sourceID = sourceID } required init(_ key: NodeKey? = nil) { super.init(key) - - self.type = NodeType.image } public required init(from decoder: Decoder) throws { try super.init(from: decoder) - - self.type = NodeType.image } - + + required init(styles: StylesDict, key: NodeKey?) { + super.init(styles: [:], key: key) + } + override public func encode(to encoder: Encoder) throws { try super.encode(to: encoder) } diff --git a/Plugins/LexicalInlineImagePlugin/LexicalInlineImagePlugin/Nodes/SelectableImageNode.swift b/Plugins/LexicalInlineImagePlugin/LexicalInlineImagePlugin/Nodes/SelectableImageNode.swift index a51ab229..75157c07 100644 --- a/Plugins/LexicalInlineImagePlugin/LexicalInlineImagePlugin/Nodes/SelectableImageNode.swift +++ b/Plugins/LexicalInlineImagePlugin/LexicalInlineImagePlugin/Nodes/SelectableImageNode.swift @@ -25,22 +25,25 @@ public class SelectableImageNode: SelectableDecoratorNode { self.url = URL(string: url) self.size = size - self.type = NodeType.image self.sourceID = sourceID } required init(_ key: NodeKey? = nil) { super.init(key) - - self.type = NodeType.image } public required init(from decoder: Decoder) throws { try super.init(from: decoder) - - self.type = NodeType.image + } + + public override class func getType() -> NodeType { + return .selectableImage } + required init(styles: StylesDict, key: NodeKey?) { + super.init(styles: styles, key: key) + } + override public func encode(to encoder: Encoder) throws { try super.encode(to: encoder) } diff --git a/Plugins/LexicalLinkPlugin/LexicalLinkPlugin/Nodes/LinkNode.swift b/Plugins/LexicalLinkPlugin/LexicalLinkPlugin/Nodes/LinkNode.swift index fbeb239f..70264efa 100644 --- a/Plugins/LexicalLinkPlugin/LexicalLinkPlugin/Nodes/LinkNode.swift +++ b/Plugins/LexicalLinkPlugin/LexicalLinkPlugin/Nodes/LinkNode.swift @@ -19,15 +19,9 @@ open class LinkNode: ElementNode { public var url: String = "" - override public init() { - super.init() - self.type = NodeType.link - } - public required init(url: String, key: NodeKey?) { - super.init(key) + super.init(styles: [:], key: key) self.url = url - self.type = NodeType.link } public required init(from decoder: Decoder) throws { @@ -35,7 +29,14 @@ open class LinkNode: ElementNode { try super.init(from: decoder) self.url = try container.decode(String.self, forKey: .url) - self.type = NodeType.link + } + + required public init(styles: StylesDict, key: NodeKey?) { + fatalError("init(styles:key:) has not been implemented") + } + + open override class func getType() -> NodeType { + return .link } override open func encode(to encoder: Encoder) throws { diff --git a/Plugins/LexicalListPlugin/LexicalListPlugin/ListItemNode.swift b/Plugins/LexicalListPlugin/LexicalListPlugin/ListItemNode.swift index 73c03cde..dc23121f 100644 --- a/Plugins/LexicalListPlugin/LexicalListPlugin/ListItemNode.swift +++ b/Plugins/LexicalListPlugin/LexicalListPlugin/ListItemNode.swift @@ -17,27 +17,28 @@ public class ListItemNode: ElementNode { private var value: Int = 0 - override public init() { - super.init() - self.type = NodeType.listItem - } - - override public required init(_ key: NodeKey?) { - super.init(key) - self.type = NodeType.listItem + public convenience override init() { + self.init(styles: [:], key: nil) } public required init(from decoder: Decoder) throws { try super.init(from: decoder) - self.type = NodeType.listItem } + public override class func getType() -> NodeType { + return .listItem + } + + required init(styles: StylesDict, key: NodeKey?) { + super.init(styles: styles, key: key) + } + override open func encode(to encoder: Encoder) throws { try super.encode(to: encoder) } override public func clone() -> Self { - Self(key) + Self(styles: styles, key: key) } public func getValue() -> Int { @@ -168,7 +169,7 @@ public class ListItemNode: ElementNode { } override public func insertNewAfter(selection: RangeSelection?) throws -> Node? { - let newElement = ListItemNode() + let newElement = ListItemNode(styles: [:], key: nil) _ = try self.insertAfter(nodeToInsert: newElement) return newElement diff --git a/Plugins/LexicalListPlugin/LexicalListPlugin/ListNode.swift b/Plugins/LexicalListPlugin/LexicalListPlugin/ListNode.swift index 82cbb237..bbe4528a 100644 --- a/Plugins/LexicalListPlugin/LexicalListPlugin/ListNode.swift +++ b/Plugins/LexicalListPlugin/LexicalListPlugin/ListNode.swift @@ -23,24 +23,22 @@ public class ListNode: ElementNode { private var start: Int = 1 public required convenience init(listType: ListType, start: Int, key: NodeKey? = nil) { - self.init(key) + self.init(styles: [:], key: key) self.listType = listType self.start = start } - override public init() { - super.init() - self.type = NodeType.list - } - - override public init(_ key: NodeKey?) { - super.init(key) - self.type = NodeType.list - } - public required init(from decoder: Decoder) throws { try super.init(from: decoder) - self.type = NodeType.list + // TODO: coding keys + } + + required init(styles: StylesDict, key: NodeKey?) { + fatalError("init(styles:key:) has not been implemented") + } + + public override class func getType() -> NodeType { + return .list } // MARK: getters/setters