diff --git a/rich-text-editor.swift b/rich-text-editor.swift new file mode 100644 index 0000000..3ffe332 --- /dev/null +++ b/rich-text-editor.swift @@ -0,0 +1,227 @@ +// +// TextDataApiModelConverter.swift +// SttDictionary.iOS.NextGen +// +// Created by Peter Standret on 9/28/19. +// Copyright © 2019 Peter Standret. All rights reserved. +// + +import Foundation +import UIKit +import STT + +fileprivate extension UIFont { + + func withTraits(_ traits: UIFontDescriptor.SymbolicTraits) -> UIFont { + + if let fontDescriptor = fontDescriptor.withSymbolicTraits(traits) { + return UIFont(descriptor: fontDescriptor, size: pointSize) + } + + return self + } +} + +fileprivate extension String { + + func substring(location: Int, lenght: Int) -> Substring { + let startIndex = self.index(self.startIndex, offsetBy: location) + let endIndex = self.index(self.startIndex, offsetBy: location + lenght) + return self[startIndex.. NSAttributedString { + + let parserData = parameter as! TDConverterParameter + + let result = NSMutableAttributedString( + string: value.value, + attributes: Const.defaultAttributes + ) + + for data in value.metaData { + + // range + let start = data.range.location + let length = data.range.length + let range = NSRange(location: start, length: length) + + var attributes = Const.defaultAttributes + + attributes[.foregroundColor] = getColor(data: data) + attributes[.font] = UIFont.systemFont(ofSize: 18, weight: getFontWeight(data: data)) + + if data.fontDecoration == .underline { + attributes[.underlineStyle] = NSUnderlineStyle.single.rawValue + } + + if data.fontStyle == .italic { + attributes[.font] = (attributes[.font] as! UIFont).withTraits(.traitItalic) + } + else if data.fontStyle == .monospaced { + attributes[.font] = UIFont.monospacedSystemFont(ofSize: 18, weight: getFontWeight(data: data)) + } + + // subscript or superscript + if data.textType != .normal { + attributes[.font] = (attributes[.font] as! UIFont).withSize(10) + attributes[.baselineOffset] = data.textType == .subScript ? -7 : 7 + } + + result.setAttributes(attributes, range: range) + } + + if parserData.rawOutput { + return result + } + + return getFormattedString(from: result, parserData: parserData) + } + + private func getFormattedString( + from string: NSAttributedString, + parserData: TDConverterParameter + ) -> NSAttributedString { + + let closeDeletionRanges = string.string.ranges(for: Const.closeDeletionDelimeter) + guard !closeDeletionRanges.isEmpty else { + return string + } + + let result = NSMutableAttributedString() + + var previusEnd = 0 + for range in closeDeletionRanges { + if previusEnd < range.location { + result.append(string.attributedSubstring(from: + NSRange(location: previusEnd, length: range.location - previusEnd)) + ) + } + + let closeDeletion = string.attributedSubstring( + from: NSRange(location: range.location, length: range.length) + ) + if let delimeterRange = closeDeletion.string.ranges(for: Const.closeHintDelimeter).first { + let hint = closeDeletion.attributedSubstring(from: + NSRange(location: 2, length: delimeterRange.location - 2) + ) + + let value = closeDeletion.attributedSubstring(from: + NSRange( + location: delimeterRange.location + delimeterRange.length, + length: closeDeletion.length - delimeterRange.location - delimeterRange.length - 2 + )) + + let valueToAppend = parserData.isOpenCloseDeletion ? value : hint + + let startLocation = result.length + result.append(valueToAppend) + result.setAttributes( + Const.defaultCLDAttributes, + range: NSRange(location: startLocation, length: valueToAppend.length) + ) + } + else { + if parserData.isOpenCloseDeletion { + let valueToAppend = closeDeletion.attributedSubstring(from: + NSRange(location: 2, length: closeDeletion.length - 4) + ) + + let startLocation = result.length + result.append(valueToAppend) + result.setAttributes( + Const.defaultCLDAttributes, + range: NSRange(location: startLocation, length: valueToAppend.length) + ) + } + else { + result.append(NSAttributedString(string: "[...]", attributes: Const.defaultCLDAttributes)) + } + } + + previusEnd = range.location + range.length + } + + if previusEnd < string.length { + result.append(string.attributedSubstring(from: + NSRange(location: previusEnd, length: string.length - previusEnd)) + ) + } + + return result + } + + private func getColor(data: TextMetaDataApiModel) -> UIColor { + + switch data.colorType { + case .default: return Asset.NightColors.textColor.color + case .highlihted: return Asset.NightColors.mainYellow.color + case .custom: + if let customColor = data.color { + return UIColor( + red: CGFloat(CFloat(customColor.red)) / 255.0, + green: CGFloat(CFloat(customColor.green)) / 255.0, + blue: CGFloat(CFloat(customColor.blue)) / 255.0, + alpha: CGFloat(CFloat(customColor.alpha)) / 255.0 + ) + } + return Asset.NightColors.textColor.color + } + } + + private func getFontWeight(data: TextMetaDataApiModel) -> UIFont.Weight { + switch data.fontWeight { + case .black: return .black + case .regular: return .regular + case .thin: return .thin + case .medium: return .medium + case .bold: return .bold + } + } +} + +final class TextDataApiModelConverterCombiner: ConverterType { + + let converter = TextDataApiModelConverter() + + func convert(value: [TextDataApiModel], parameter: Any?) -> NSAttributedString { + + let parserData = parameter as! TDConverterParameter + let result = NSMutableAttributedString() + + for (index, text) in value.enumerated() { + result.append(converter.convert(value: text, parameter: parserData)) + if index < value.count - 1 { + result.append(NSAttributedString(string: "\n")) + } + } + + return result + } +}