Skip to content

Commit

Permalink
Add the ability to merge attributed strings
Browse files Browse the repository at this point in the history
  • Loading branch information
vincode-io committed Dec 4, 2023
1 parent c0b355f commit d323ec3
Show file tree
Hide file tree
Showing 4 changed files with 67 additions and 28 deletions.
53 changes: 44 additions & 9 deletions VinCloudKit/Sources/VinCloudKit/VCKModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import OrderedCollections
enum VCKMergeScenario {
case clientWins
case serverWins
case merge
case threeWayMerge

static func evaluate<T>(client: T?, ancestor: T?, server: T?) -> Self where T:Equatable {
if let ancestor {
Expand All @@ -23,7 +23,7 @@ enum VCKMergeScenario {
guard client != nil else { return .serverWins }

// We have all 3 values and need to do a 3 way merge
return .merge
return .threeWayMerge
} else {
if server == nil {
return .clientWins
Expand Down Expand Up @@ -54,10 +54,45 @@ public extension VCKModel {
return client
case .serverWins:
return server
case .merge:
#warning("This should be changed to be smart enough to merge Strings and AttributedStrings")
// It should be possible to merge using the raw strings and iterating over the diffs to select the attributes
// to create a merged NSAttributedString
case .threeWayMerge:
return client
}
}

func merge(client: Data?, ancestor: Data?, server: Data?) -> Data? {
switch VCKMergeScenario.evaluate(client: client, ancestor: ancestor, server: server) {
case .clientWins:
return client
case .serverWins:
return server
case .threeWayMerge:
guard let clientAttrString = client?.toAttributedString(),
let serverAttrString = server?.toAttributedString(),
let ancestorAttrString = ancestor?.toAttributedString() else {
fatalError("We should always have all 3 values for a 3 way merge.")
}

let diff = serverAttrString.string.difference(from: ancestorAttrString.string).inferringMoves()
if !diff.isEmpty {
let merged = NSMutableAttributedString(attributedString: clientAttrString)

for change in diff {
switch change {
case .insert(let offset, _, let associated):
guard offset < clientAttrString.length else { continue }
let serverAttrString = serverAttrString.attributedSubstring(from: NSRange(location: associated ?? offset, length: 1))
merged.insert(serverAttrString, at: offset)
case .remove(let offset, _, _):
guard offset < clientAttrString.length else { continue }
merged.deleteCharacters(in: NSRange(location: offset, length: 1))
}
}

return merged.toData()
} else {
// TODO: Merge attributes only...
}

return client
}
}
Expand All @@ -78,9 +113,9 @@ public extension VCKModel {
return client
case .serverWins:
return server
case .merge:
guard let client, let server else { fatalError("We should always have both a server or client if we are merging") }
let diff = server.difference(from: ancestor ?? [])
case .threeWayMerge:
guard let client, let server, let ancestor else { fatalError("We should always have all 3 values for a 3 way merge.") }
let diff = server.difference(from: ancestor)
guard let merged = client.applying(diff) else {
return client
}
Expand Down
14 changes: 4 additions & 10 deletions VinOutlineKit/Sources/VinOutlineKit/Row.swift
Original file line number Diff line number Diff line change
Expand Up @@ -173,9 +173,7 @@ public final class Row: NSObject, NSCopying, RowContainer, Codable, Identifiable
get {
guard let topic = topicData else { return nil }
if topicCache == nil {
topicCache = try? NSAttributedString(data: topic,
options: [.documentType: NSAttributedString.DocumentType.rtf, .characterEncoding: String.Encoding.utf8.rawValue],
documentAttributes: nil)
topicCache = topic.toAttributedString()
topicCache = replaceImages(attrString: topicCache, isNotes: false)
}
return topicCache
Expand All @@ -184,8 +182,7 @@ public final class Row: NSObject, NSCopying, RowContainer, Codable, Identifiable
if let attrText = newValue {
let (cleanAttrText, newImages) = splitOffImages(attrString: attrText, isNotes: false)
cleanAttrText.trimCharacters(in: .whitespaces)

topicData = try? cleanAttrText.data(from: .init(location: 0, length: cleanAttrText.length), documentAttributes: [.documentType: NSAttributedString.DocumentType.rtf])
topicData = cleanAttrText.toData()

var notesImages = images?.filter { $0.isInNotes ?? false } ?? [Image]()
notesImages.append(contentsOf: newImages)
Expand All @@ -208,9 +205,7 @@ public final class Row: NSObject, NSCopying, RowContainer, Codable, Identifiable
get {
guard let note = noteData else { return nil }
if noteCache == nil {
noteCache = try? NSAttributedString(data: note,
options: [.documentType: NSAttributedString.DocumentType.rtf, .characterEncoding: String.Encoding.utf8.rawValue],
documentAttributes: nil)
noteCache = note.toAttributedString()
noteCache = replaceImages(attrString: noteCache, isNotes: true)
}
return noteCache
Expand All @@ -219,8 +214,7 @@ public final class Row: NSObject, NSCopying, RowContainer, Codable, Identifiable
if let attrText = newValue {
let (cleanAttrText, newImages) = splitOffImages(attrString: attrText, isNotes: true)
cleanAttrText.trimCharacters(in: .whitespaces)

noteData = try? cleanAttrText.data(from: .init(location: 0, length: cleanAttrText.length), documentAttributes: [.documentType: NSAttributedString.DocumentType.rtf])
noteData = cleanAttrText.toData()

var topicImages = images?.filter { !($0.isInNotes ?? false) } ?? [Image]()
topicImages.append(contentsOf: newImages)
Expand Down
15 changes: 11 additions & 4 deletions VinUtility/Sources/VinUtility/Data+VU.swift
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
//
// File.swift
//
//
///
// Created by Maurice Parker on 12/4/23.
//

import Foundation

public extension Data {

func toAttributedString() -> NSAttributedString? {
return try? NSAttributedString(data: self,
options: [.documentType: NSAttributedString.DocumentType.rtf, .characterEncoding: String.Encoding.utf8.rawValue],
documentAttributes: nil)
}

}
13 changes: 8 additions & 5 deletions VinUtility/Sources/VinUtility/NSAttributedString+VU.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,18 @@

import Foundation

extension NSAttributedString {
public extension NSAttributedString {

#if canImport(UIKit)
public convenience init(linkText: String, linkURL: URL) {
convenience init(linkText: String, linkURL: URL) {
let attrString = NSMutableAttributedString(string: linkText)
let range = NSRange(location: 0, length: attrString.length)
attrString.addAttribute(.link, value: linkURL, range: range)
self.init(attributedString: attrString)
}
#endif

public static func isOptionalStringsEqual(lhs: NSAttributedString?, rhs: NSAttributedString?) -> Bool {
static func isOptionalStringsEqual(lhs: NSAttributedString?, rhs: NSAttributedString?) -> Bool {
if lhs == nil && rhs == nil {
return true
}
Expand All @@ -28,10 +28,13 @@ extension NSAttributedString {
return lhs!.isEqual(to: rhs!)
}

public func trimmingCharacters(in charSet: CharacterSet) -> NSAttributedString {
func trimmingCharacters(in charSet: CharacterSet) -> NSAttributedString {
let modifiedString = NSMutableAttributedString(attributedString: self)
modifiedString.trimCharacters(in: charSet)
return NSAttributedString(attributedString: modifiedString)
}


func toData() -> Data? {
return try? self.data(from: .init(location: 0, length: self.length), documentAttributes: [.documentType: NSAttributedString.DocumentType.rtf])
}
}

0 comments on commit d323ec3

Please sign in to comment.