diff --git a/Lexical/Core/Constants.swift b/Lexical/Core/Constants.swift index d3997431..d820b46b 100644 --- a/Lexical/Core/Constants.swift +++ b/Lexical/Core/Constants.swift @@ -70,14 +70,50 @@ public enum DirtyType { case fullReconcile } -@objc public enum TextFormatType: Int { - case bold - case italic - case underline - case strikethrough - case code - case subScript - case superScript +public struct TextFormatType: OptionSet, CaseIterable, Codable { + public let rawValue: UInt + + public static let bold = TextFormatType(rawValue: 1 << 0) + public static let italic = TextFormatType(rawValue: 1 << 1) + public static let strikethrough = TextFormatType(rawValue: 1 << 2) + public static let underline = TextFormatType(rawValue: 1 << 3) + public static let code = TextFormatType(rawValue: 1 << 4) + public static let subScript = TextFormatType(rawValue: 1 << 5) + public static let superScript = TextFormatType(rawValue: 1 << 6) + +// Not yet supported. Is here because Javacript library supports this. +// Once this is supported, add it to `allCases` too +// static let highlight = TextFormatType(rawValue: 1 << 7) + + static public var allCases: [TextFormatType] { + [.bold, .italic, .strikethrough, .underline, .code, .subScript, .superScript] + } + + public var description: String { + switch self { + case .bold: return "bold" + case .code: return "code" + case .italic: return "italic" + case .strikethrough: return "strikethrough" + case .subScript: return "subscript" + case .superScript: return "superscript" + case .underline: return "underline" + default: return "" + } + } + + public init(rawValue: UInt) { + self.rawValue = rawValue + } + + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + self.rawValue = try container.decode(UInt.self) + } + + public mutating func toggle(_ value: TextFormatType) { + self = symmetricDifference(value) + } } enum Direction: String, Codable { diff --git a/Lexical/Core/Events.swift b/Lexical/Core/Events.swift index 5854d17b..f73ec9af 100644 --- a/Lexical/Core/Events.swift +++ b/Lexical/Core/Events.swift @@ -182,7 +182,7 @@ internal func onSelectionChange(editor: Editor) { guard let anchorNode = try lexicalSelection.anchor.getNode() as? TextNode else { break } lexicalSelection.format = anchorNode.getFormat() case .element: - lexicalSelection.format = TextFormat() + lexicalSelection.format = TextFormatType() default: break } diff --git a/Lexical/Core/Nodes/CodeHighlightNode.swift b/Lexical/Core/Nodes/CodeHighlightNode.swift index 69c06226..0b11452b 100644 --- a/Lexical/Core/Nodes/CodeHighlightNode.swift +++ b/Lexical/Core/Nodes/CodeHighlightNode.swift @@ -49,7 +49,7 @@ public class CodeHighlightNode: TextNode { } // Prevent formatting (bold, underline, etc) - override public func setFormat(format: TextFormat) throws -> CodeHighlightNode { + override public func setFormat(format: TextFormatType) throws -> CodeHighlightNode { return try self.getWritable() } } diff --git a/Lexical/Core/Nodes/TextNode.swift b/Lexical/Core/Nodes/TextNode.swift index 986119b6..0d7ae755 100644 --- a/Lexical/Core/Nodes/TextNode.swift +++ b/Lexical/Core/Nodes/TextNode.swift @@ -11,133 +11,24 @@ public enum TextNodeThemeSubtype { public static let code = "code" } -public struct SerializedTextFormat: OptionSet, Codable { - public let rawValue: Int - - public static let bold = SerializedTextFormat(rawValue: 1 << 0) - public static let italic = SerializedTextFormat(rawValue: 1 << 1) - public static let strikethrough = SerializedTextFormat(rawValue: 1 << 2) - public static let underline = SerializedTextFormat(rawValue: 1 << 3) - public static let code = SerializedTextFormat(rawValue: 1 << 4) - public static let subScript = SerializedTextFormat(rawValue: 1 << 5) - public static let superScript = SerializedTextFormat(rawValue: 1 << 6) - - public init(rawValue: Int) { - self.rawValue = rawValue - } - - // On encode, convert from TextFormat -> SerializedTextFormat - public static func convertToSerializedTextFormat(from textFormat: TextFormat) -> SerializedTextFormat { - var serialTextFormat = SerializedTextFormat() - if textFormat.bold { - serialTextFormat.insert(.bold) - } - if textFormat.italic { - serialTextFormat.insert(.italic) - } - if textFormat.underline { - serialTextFormat.insert(.underline) - } - if textFormat.strikethrough { - serialTextFormat.insert(.strikethrough) - } - if textFormat.code { - serialTextFormat.insert(.code) - } - if textFormat.subScript { - serialTextFormat.insert(.subScript) - } - if textFormat.superScript { - serialTextFormat.insert(.superScript) - } - - return serialTextFormat - } - - // On decode, convert from SerializedTextFormat -> TextFormat - public static func convertToTextFormat(from serialTextFormat: SerializedTextFormat) -> TextFormat { - var textFormat = TextFormat() - if serialTextFormat.contains(.bold) { - textFormat.bold = true - } - if serialTextFormat.contains(.italic) { - textFormat.italic = true - } - if serialTextFormat.contains(.underline) { - textFormat.underline = true - } - if serialTextFormat.contains(.strikethrough) { - textFormat.strikethrough = true - } - if serialTextFormat.contains(.code) { - textFormat.code = true - } - if serialTextFormat.contains(.subScript) { - textFormat.subScript = true - } - if serialTextFormat.contains(.superScript) { - textFormat.superScript = true - } - - return textFormat - } -} - -public struct TextFormat: Equatable, Codable { - - public var bold: Bool - public var italic: Bool - public var underline: Bool - public var strikethrough: Bool - public var code: Bool - public var subScript: Bool - public var superScript: Bool - - public init() { - self.bold = false - self.italic = false - self.underline = false - self.strikethrough = false - self.code = false - self.subScript = false - self.superScript = false - } - - public func isTypeSet(type: TextFormatType) -> Bool { - switch type { - case .bold: - return bold - case .italic: - return italic - case .underline: - return underline - case .strikethrough: - return strikethrough - case .code: - return code - case .subScript: - return subScript - case .superScript: - return superScript - } - } - - public mutating func updateFormat(type: TextFormatType, value: Bool) { - switch type { - case .bold: - bold = value - case .italic: - italic = value - case .underline: - underline = value - case .strikethrough: - strikethrough = value +extension TextFormatType { + func makeAttributedStringKey(with theme: Theme) -> [NSAttributedString.Key: Any] { + switch self { + case .bold: return [.bold: true] + case .italic: return [.italic: true] + case .underline: return [.underlineStyle: NSUnderlineStyle.single.rawValue] + case .strikethrough: return [.strikethroughStyle: NSUnderlineStyle.single.rawValue] case .code: - code = value - case .subScript: - subScript = value - case .superScript: - superScript = value + if let themeDict = theme.getValue(.text, withSubtype: TextNodeThemeSubtype.code) { + return themeDict + } else { + return [ + .fontFamily: "Courier", + .backgroundColor: UIColor.lightGray + ] + } + default: + return [:] } } } @@ -194,7 +85,7 @@ open class TextNode: Node { private var text: String = "" public var mode: Mode = .normal - var format: TextFormat = TextFormat() + var format = TextFormatType() var detail = TextNodeDetail() var style: String = "" @@ -221,8 +112,7 @@ 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) + self.format = try container.decode(TextFormatType.self, forKey: .format) let serializedDetail = try container.decode(SerializedTextNodeDetail.self, forKey: .detail) self.detail = SerializedTextNodeDetail.convertToTextDetail(from: serializedDetail) self.style = try container.decode(String.self, forKey: .style) @@ -233,7 +123,7 @@ open class TextNode: Node { 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(self.format, forKey: .format) try container.encode(SerializedTextNodeDetail.convertToSerializedTextNodeDetail(from: self.detail).rawValue, forKey: .detail) try container.encode(self.style, forKey: .style) } @@ -261,12 +151,12 @@ open class TextNode: Node { public func setBold(_ isBold: Bool) throws { try errorOnReadOnly() - try getWritable().format.bold = isBold + try getWritable().format.toggle(.bold) } public func setItalic(_ isItalic: Bool) throws { try errorOnReadOnly() - try getWritable().format.italic = isItalic + try getWritable().format.toggle(.italic) } public func canInsertTextAfter() -> Bool { @@ -278,39 +168,25 @@ open class TextNode: Node { } override open func getAttributedStringAttributes(theme: Theme) -> [NSAttributedString.Key: Any] { - var attributeDictionary = super.getAttributedStringAttributes(theme: theme) + let attributeDictionary = super.getAttributedStringAttributes(theme: theme) // TODO: Remove this once codeHighlight node is implemented if let parent, let _ = getNodeByKey(key: parent) as? CodeNode { - format = TextFormat() + format = TextFormatType() } - 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 + return attributeDictionary.merging( + TextFormatType + .allCases + .filter { + format.contains($0) + } + .reduce([:]) { partialResult, type in + return partialResult.merging(type.makeAttributedStringKey(with: theme)) { _, new in new } + }, + uniquingKeysWith: { _, new in + return new + }) } public func isInert() -> Bool { @@ -395,13 +271,13 @@ open class TextNode: Node { return true } - public func getFormat() -> TextFormat { + public func getFormat() -> TextFormatType { let node = getLatest() as TextNode return node.format } @discardableResult - public func setFormat(format: TextFormat) throws -> TextNode { + public func setFormat(format: TextFormatType) throws -> TextNode { try errorOnReadOnly() let node = try getWritable() as TextNode node.format = format @@ -565,15 +441,24 @@ open class TextNode: Node { focusType: .text) } guard let selection = selection as? RangeSelection else { - return try makeRangeSelection(anchorKey: key, anchorOffset: updatedAnchorOffset, focusKey: key, focusOffset: updatedAnchorOffset, anchorType: .text, focusType: .text) + return try makeRangeSelection(anchorKey: key, + anchorOffset: updatedAnchorOffset, + focusKey: key, + focusOffset: updatedAnchorOffset, + anchorType: .text, + focusType: .text) } - selection.setTextNodeRange(anchorNode: self, anchorOffset: updatedAnchorOffset, focusNode: self, focusOffset: updatedFocusOffset) + selection.setTextNodeRange(anchorNode: self, + anchorOffset: updatedAnchorOffset, + focusNode: self, + focusOffset: updatedFocusOffset) return selection } - public func getFormatFlags(type: TextFormatType, alignWithFormat: TextFormat? = nil) -> TextFormat { + public func getFormatFlags(type: TextFormatType, + alignWithFormat: TextFormatType? = nil) -> TextFormatType { let node = getLatest() as TextNode let format = node.format return toggleTextFormatType(format: format, type: type, alignWithFormat: alignWithFormat) @@ -716,15 +601,12 @@ extension TextNode: CustomDebugStringConvertible { } } -extension TextFormat: CustomDebugStringConvertible { +extension TextFormatType: CustomDebugStringConvertible { public var debugDescription: String { - var debugStyleStatus = [String]() - if bold { debugStyleStatus.append("bold") } - if italic { debugStyleStatus.append("italic") } - if underline { debugStyleStatus.append("underline") } - if strikethrough { debugStyleStatus.append("strikeThrough") } - if code { debugStyleStatus.append("code") } - return debugStyleStatus.joined(separator: ", ") + TextFormatType.allCases.compactMap { + return self.contains($0) ? $0.description : nil + } + .joined(separator: ", ") } } diff --git a/Lexical/Core/Reconciler.swift b/Lexical/Core/Reconciler.swift index 36a549fc..cc71e70a 100644 --- a/Lexical/Core/Reconciler.swift +++ b/Lexical/Core/Reconciler.swift @@ -255,7 +255,9 @@ internal enum Reconciler { // marked text operation. let length = markedTextOperation.markedTextString.lengthAsNSString() let endPoint = Point(key: startPoint.key, offset: startPoint.offset + length, type: .text) - try frontend.updateNativeSelection(from: RangeSelection(anchor: startPoint, focus: endPoint, format: TextFormat())) + try frontend.updateNativeSelection(from: RangeSelection(anchor: startPoint, + focus: endPoint, + format: TextFormatType())) let attributedSubstring = markedTextAttributedString.attributedSubstring(from: NSRange(location: startPoint.offset, length: length)) editor.frontend?.setMarkedTextFromReconciler(attributedSubstring, selectedRange: markedTextOperation.markedTextInternalSelection) diff --git a/Lexical/Core/Selection/NodeSelection.swift b/Lexical/Core/Selection/NodeSelection.swift index 625372c4..b7354897 100644 --- a/Lexical/Core/Selection/NodeSelection.swift +++ b/Lexical/Core/Selection/NodeSelection.swift @@ -133,7 +133,7 @@ public class NodeSelection: BaseSelection { } let anchor = Point(key: parent.getKey(), offset: nodeIndexInParent, type: .element) let focus = Point(key: parent.getKey(), offset: nodeIndexInParent + 1, type: .element) - return RangeSelection(anchor: anchor, focus: focus, format: TextFormat()) + return RangeSelection(anchor: anchor, focus: focus, format: TextFormatType()) } } diff --git a/Lexical/Core/Selection/RangeSelection.swift b/Lexical/Core/Selection/RangeSelection.swift index 5168a100..162178a6 100644 --- a/Lexical/Core/Selection/RangeSelection.swift +++ b/Lexical/Core/Selection/RangeSelection.swift @@ -13,12 +13,12 @@ public class RangeSelection: BaseSelection { public var anchor: Point public var focus: Point public var dirty: Bool - public var format: TextFormat + public var format: TextFormatType public var style: String // TODO: add style support to iOS // MARK: - Init - public init(anchor: Point, focus: Point, format: TextFormat) { + public init(anchor: Point, focus: Point, format: TextFormatType) { self.anchor = anchor self.focus = focus self.dirty = false @@ -40,7 +40,7 @@ public class RangeSelection: BaseSelection { } public func hasFormat(type: TextFormatType) -> Bool { - return format.isTypeSet(type: type) + return format.contains(type) } public func getCharacterOffsets(selection: RangeSelection) -> (Int, Int) { @@ -1070,7 +1070,7 @@ public class RangeSelection: BaseSelection { self.anchor = anchor self.focus = focus self.dirty = false - self.format = TextFormat() + self.format = TextFormatType() self.style = "" } @@ -1083,7 +1083,7 @@ public class RangeSelection: BaseSelection { let selectedNodes = try getNodes() guard var firstNode = selectedNodes.first, let lastNode = selectedNodes.last else { return } - var firstNextFormat = TextFormat() + var firstNextFormat = TextFormatType() for node in selectedNodes { if let node = node as? TextNode { firstNextFormat = node.getFormatFlags(type: formatType) @@ -1218,12 +1218,12 @@ public class RangeSelection: BaseSelection { } internal func clearFormat() { - format = TextFormat() + format = TextFormatType() } // MARK: - Private - private func updateSelection(anchor: Point, focus: Point, format: TextFormat, isDirty: Bool) { + private func updateSelection(anchor: Point, focus: Point, format: TextFormatType, 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 diff --git a/Lexical/Core/Selection/SelectionUtils.swift b/Lexical/Core/Selection/SelectionUtils.swift index 5309c535..db3adc29 100644 --- a/Lexical/Core/Selection/SelectionUtils.swift +++ b/Lexical/Core/Selection/SelectionUtils.swift @@ -234,7 +234,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, format: TextFormatType()) } /// When we create a selection, we try to use the previous selection where possible, unless an actual user selection change has occurred. @@ -259,7 +259,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, format: TextFormatType()) } return nil @@ -285,7 +285,7 @@ func makeRangeSelection( let selection = RangeSelection( anchor: Point(key: anchorKey, offset: anchorOffset, type: anchorType), focus: Point(key: focusKey, offset: focusOffset, type: focusType), - format: TextFormat()) + format: TextFormatType()) selection.dirty = true editorState.selection = selection @@ -434,7 +434,10 @@ func moveSelectionPointToEnd(point: Point, node: Node) { } } -func transferStartingElementPointToTextPoint(start: Point, end: Point, format: TextFormat, style: String) throws { +func transferStartingElementPointToTextPoint(start: Point, + end: Point, + format: TextFormatType, + style: String) throws { guard let element = try start.getNode() as? ElementNode else { return } var placementNode = element.getChildAtIndex(index: start.offset) diff --git a/Lexical/Core/Utils.swift b/Lexical/Core/Utils.swift index e8ca7e16..ec26bb9a 100644 --- a/Lexical/Core/Utils.swift +++ b/Lexical/Core/Utils.swift @@ -138,32 +138,14 @@ public func createCodeHighlightNode(text: String, highlightType: String?) -> Cod CodeHighlightNode(text: text, highlightType: highlightType) } -public func toggleTextFormatType(format: TextFormat, type: TextFormatType, alignWithFormat: TextFormat?) -> TextFormat { - var activeFormat = format - let isStateFlagPresent = format.isTypeSet(type: type) - var flag = false - - if let alignWithFormat { - // remove the type from format - if isStateFlagPresent && !alignWithFormat.isTypeSet(type: type) { - flag = false - } - - // add the type to format - if alignWithFormat.isTypeSet(type: type) { - flag = true - } +public func toggleTextFormatType(format: TextFormatType, + type: TextFormatType, + alignWithFormat: TextFormatType?) -> TextFormatType { + if let alignWithFormat = alignWithFormat { + return alignWithFormat.contains(type) ? format.union(type) : format.subtracting(type) } else { - if isStateFlagPresent { - flag = false - } else { - flag = true - } + return format.symmetricDifference(type) } - - activeFormat.updateFormat(type: type, value: flag) - - return activeFormat } public func isElementNode(node: Node?) -> Bool { diff --git a/Lexical/Helper/CopyPasteHelpers.swift b/Lexical/Helper/CopyPasteHelpers.swift index 591ae8c9..3a4ea78c 100644 --- a/Lexical/Helper/CopyPasteHelpers.swift +++ b/Lexical/Helper/CopyPasteHelpers.swift @@ -126,23 +126,23 @@ internal func insertRTF(selection: RangeSelection, attributedString: NSAttribute if (attribute.attributes.first(where: { $0.key == .font })?.value as? UIFont)? .fontDescriptor.symbolicTraits.contains(.traitBold) ?? false { - textNode.format.bold = true + textNode.format.insert(.bold) } if (attribute.attributes.first(where: { $0.key == .font })?.value as? UIFont)? .fontDescriptor.symbolicTraits.contains(.traitItalic) ?? false { - textNode.format.italic = true + textNode.format.insert(.italic) } if let underlineAttribute = attribute.attributes[.underlineStyle] { if underlineAttribute as? NSNumber != 0 { - textNode.format.underline = true + textNode.format.insert(.underline) } } if let strikethroughAttribute = attribute.attributes[.strikethroughStyle] { if strikethroughAttribute as? NSNumber != 0 { - textNode.format.strikethrough = true + textNode.format.insert(.strikethrough) } } diff --git a/Lexical/TextView/TextView.swift b/Lexical/TextView/TextView.swift index c7e4504e..ad76f062 100644 --- a/Lexical/TextView/TextView.swift +++ b/Lexical/TextView/TextView.swift @@ -282,12 +282,16 @@ protocol LexicalTextViewDelegate: NSObjectProtocol { // find all nodes in selection. Mark dirty. Reconcile. This should correct all the attributes to be what we expect. do { try editor.update { - guard let anchor = try pointAtStringLocation(previousMarkedRange.location, searchDirection: .forward, rangeCache: editor.rangeCache), - let focus = try pointAtStringLocation(previousMarkedRange.location + previousMarkedRange.length, searchDirection: .forward, rangeCache: editor.rangeCache) else { + guard let anchor = try pointAtStringLocation(previousMarkedRange.location, + searchDirection: .forward, + rangeCache: editor.rangeCache), + let focus = try pointAtStringLocation(previousMarkedRange.location + previousMarkedRange.length, + searchDirection: .forward, + rangeCache: editor.rangeCache) else { return } - let markedRangeSelection = RangeSelection(anchor: anchor, focus: focus, format: TextFormat()) + let markedRangeSelection = RangeSelection(anchor: anchor, focus: focus, format: TextFormatType()) _ = try markedRangeSelection.getNodes().map { node in internallyMarkNodeAsDirty(node: node, cause: .userInitiated) }