yorkie-ios-sdk/Sources/Document/Json/JSONText.swift

214 lines
6.5 KiB
Swift

/*
* Copyright 2023 The Yorkie Authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License")
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import Combine
import Foundation
public class JSONText {
private var context: ChangeContext?
private var text: CRDTText?
public convenience init() {
self.init(context: nil, text: nil)
}
init(context: ChangeContext? = nil, text: CRDTText? = nil) {
self.context = context
self.text = text
}
/**
* `initialize` initialize this text with context and internal text.
*/
func initialize(context: ChangeContext, text: CRDTText) {
self.context = context
self.text = text
}
/**
* `id` returns the ID of this text.
*/
public var id: TimeTicket? {
self.text?.id
}
/**
* `edit` edits this text with the given content.
*/
@discardableResult
public func edit(_ fromIdx: Int, _ toIdx: Int, _ content: String, _ attributes: TextAttributes? = nil) -> Bool {
guard let context, let text else {
Logger.critical("it is not initialized yet")
return false
}
if fromIdx > toIdx {
Logger.critical("from should be less than or equal to to")
return false
}
guard let range = try? text.createRange(fromIdx, toIdx) else {
Logger.critical("can't create range")
return false
}
Logger.debug("EDIT: f:\(fromIdx)->\(range.0.structureAsString), t:\(toIdx)->\(range.1.structureAsString) c:\(content)")
let ticket = context.issueTimeTicket()
guard let maxCreatedAtMapByActor = try? text.edit(range, content, ticket, attributes) else {
Logger.critical("can't edit Text")
return false
}
context.push(
operation: EditOperation(parentCreatedAt: text.createdAt,
fromPos: range.0,
toPos: range.1,
maxCreatedAtMapByActor: maxCreatedAtMapByActor,
content: content,
attributes: attributes != nil ? stringifyAttributes(attributes!) : nil,
executedAt: ticket)
)
if range.0 != range.1 {
context.registerRemovedNodeTextElement(text)
}
return true
}
/**
* `setStyle` styles this text with the given attributes.
*/
@discardableResult
public func setStyle(_ fromIdx: Int, _ toIdx: Int, _ attributes: TextAttributes) -> Bool {
guard let context, let text else {
Logger.critical("it is not initialized yet")
return false
}
if fromIdx > toIdx {
Logger.critical("from should be less than or equal to to")
return false
}
guard let range = try? text.createRange(fromIdx, toIdx) else {
Logger.critical("can't create range")
return false
}
Logger.debug("STYL: f:\(fromIdx)->\(range.0.structureAsString), t:\(toIdx)->\(range.1.structureAsString) a:\(attributes)")
let ticket = context.issueTimeTicket()
do {
try text.setStyle(range, attributes, ticket)
} catch {
Logger.critical("can't set Style")
return false
}
context.push(operation: StyleOperation(parentCreatedAt: text.createdAt,
fromPos: range.0,
toPos: range.1,
attributes: stringifyAttributes(attributes),
executedAt: ticket))
return true
}
/**
* `select` selects the given range.
*/
@discardableResult
public func select(_ fromIdx: Int, _ toIdx: Int) -> Bool {
guard let context, let text else {
Logger.critical("it is not initialized yet")
return false
}
guard let range = try? text.createRange(fromIdx, toIdx) else {
Logger.critical("can't create range")
return false
}
Logger.debug("SELT: f:\(fromIdx)->\(range.0.structureAsString), t:\(toIdx)->\(range.1.structureAsString)")
let ticket = context.issueTimeTicket()
do {
try text.select(range, ticket)
} catch {
Logger.critical("\(error.localizedDescription)")
return false
}
context.push(operation: SelectOperation(parentCreatedAt: text.createdAt, fromPos: range.0, toPos: range.1, executedAt: ticket))
return true
}
/**
* `structureAsString` returns a String containing the meta data of the node
* for debugging purpose.
*/
public var structureAsString: String {
guard self.context != nil, let text else {
Logger.critical("it is not initialized yet")
return ""
}
return text.structureAsString
}
public var plainText: String {
self.text?.plainText ?? ""
}
/**
* `values` returns values of this text.
*/
public var values: [TextValue]? {
guard self.context != nil, let text else {
Logger.critical("it is not initialized yet")
return nil
}
return text.values
}
/**
* `setEventStream` registers a event Stream of TextChange events.
*/
public func setEventStream(eventStream: PassthroughSubject<[TextChange], Never>?) {
guard self.context != nil, let text else {
Logger.critical("it is not initialized yet")
return
}
text.eventStream = eventStream
}
/**
* `createRange` returns pair of RGATreeSplitNodePos of the given integer offsets.
*/
func createRange(_ fromIdx: Int, _ toIdx: Int) -> RGATreeSplitNodeRange? {
guard self.context != nil, let text else {
Logger.critical("it is not initialized yet")
return nil
}
return try? text.createRange(fromIdx, toIdx)
}
}