From 22233257edd21d8e663a02f25da98395efb84d67 Mon Sep 17 00:00:00 2001 From: Petrov Anatoly Date: Sat, 6 Jun 2020 19:10:32 +0700 Subject: [PATCH] Do not use regular expressions and double XML parsing for tspans --- Source/svg/SVGParser.swift | 198 +++++++++++--------------- Source/svg/SVGParserRegexHelper.swift | 18 --- 2 files changed, 81 insertions(+), 135 deletions(-) delete mode 100644 Source/svg/SVGParserRegexHelper.swift diff --git a/Source/svg/SVGParser.swift b/Source/svg/SVGParser.swift index 6fbf69f6..25b6b9d8 100644 --- a/Source/svg/SVGParser.swift +++ b/Source/svg/SVGParser.swift @@ -1044,28 +1044,19 @@ open class SVGParser { fontWeight: fontWeight, pos: pos) } else { - guard let matcher = SVGParserRegexHelper.getTextElementMatcher() else { - return .none - } - let elementString = element.description - let fullRange = NSRange(location: 0, length: elementString.count) - if let match = matcher.firstMatch(in: elementString, options: .reportCompletion, range: fullRange) { - let tspans = (elementString as NSString).substring(with: match.range(at: 1)) - let rect = Rect(x: getDoubleValue(element, attribute: "x") ?? 0, - y: getDoubleValue(element, attribute: "y") ?? 0) - let collectedTspans = collectTspans(tspans, - textAnchor: textAnchor, - fill: fill, - stroke: stroke, - opacity: opacity, - fontName: fontName, - fontSize: fontSize, - fontWeight: fontWeight, - bounds: rect) - return Group(contents: collectedTspans, place: pos, tag: getTag(element)) - } + let rect = Rect(x: getDoubleValue(element, attribute: "x") ?? 0, + y: getDoubleValue(element, attribute: "y") ?? 0) + let collectedTspans = collectTspans(element.children, + textAnchor: textAnchor, + fill: fill, + stroke: stroke, + opacity: opacity, + fontName: fontName, + fontSize: fontSize, + fontWeight: fontWeight, + bounds: rect) + return Group(contents: collectedTspans, place: pos, tag: getTag(element)) } - return .none } fileprivate func anchorToAlign(_ textAnchor: String?) -> Align { @@ -1105,9 +1096,7 @@ open class SVGParser { // REFACTOR - fileprivate func collectTspans(_ tspan: String, - collectedTspans: [Node] = [], - withWhitespace: Bool = false, + fileprivate func collectTspans(_ contents: [XMLContent], textAnchor: String?, fill: Fill?, stroke: Stroke?, @@ -1116,99 +1105,78 @@ open class SVGParser { fontSize: Int?, fontWeight: String?, bounds: Rect) -> [Node] { - let fullString = tspan.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) as NSString - // exit recursion - if fullString.isEqual(to: "") { - return collectedTspans - } - var collection = collectedTspans - let tagRange = fullString.range(of: " element - let closingTagRange = fullString.range(of: "".lowercased()) - let tspanString = fullString.substring(to: closingTagRange.location + closingTagRange.length) - let tspanXml = SWXMLHash.parse(tspanString) - guard let indexer = tspanXml.children.first, - let text = parseTspan(indexer, - withWhitespace: withWhitespace, - textAnchor: textAnchor, - fill: fill, - stroke: stroke, - opacity: opacity, - fontName: fontName, - fontSize: fontSize, - fontWeight: fontWeight, - bounds: bounds, - previousCollectedTspan: collection.last) else { + var collection: [Node] = [] + var bounds = bounds + // Whether to add a space before the next non-whitespace-only text. + var addWhitespace = false + // Whether to preserve leading whitespaces before the next text + // by adding a single space prefix. + var preserveWhitespace = false - // skip this element if it can't be parsed - return collectTspans(fullString.substring(from: closingTagRange.location + closingTagRange.length), - collectedTspans: collectedTspans, - textAnchor: textAnchor, - fill: fill, - stroke: stroke, - opacity: opacity, - fontName: fontName, - fontSize: fontSize, - fontWeight: fontWeight, - bounds: bounds) - } - collection.append(text) - let nextString = fullString.substring(from: closingTagRange.location + closingTagRange.length) as NSString - var withWhitespace = false - if nextString.rangeOfCharacter(from: CharacterSet.whitespacesAndNewlines).location == 0 { - withWhitespace = true - } - return collectTspans(fullString.substring(from: closingTagRange.location + closingTagRange.length), - collectedTspans: collection, - withWhitespace: withWhitespace, - textAnchor: textAnchor, - fill: fill, - stroke: stroke, - opacity: opacity, - fontName: fontName, - fontSize: fontSize, - fontWeight: fontWeight, - bounds: Rect(x: bounds.x, y: bounds.y, w: bounds.w + text.bounds.w, h: bounds.h)) + for element in contents { + let text: Text? + if let textElement = element as? TextElement { + // parse as regular text element + let textString = textElement.text + let hasLeadingWhitespace = textString.first?.isWhitespace == true + let hasTrailingWhitespace = textString.last?.isWhitespace == true + + var trimmedString = textString.trimmingCharacters(in: .whitespacesAndNewlines) + let isWhitespaceOnly = trimmedString.isEmpty + + if hasLeadingWhitespace && preserveWhitespace && !isWhitespaceOnly { + trimmedString = " " + trimmedString + } + + addWhitespace = preserveWhitespace && hasTrailingWhitespace + preserveWhitespace = false + + if trimmedString.isEmpty { + continue + } + + let place = Transform().move(dx: bounds.x + bounds.w, dy: bounds.y) + + text = Text(text: trimmedString, + font: getFont(fontName: fontName, fontWeight: fontWeight, fontSize: fontSize), + fill: fill, + stroke: stroke, + align: anchorToAlign(textAnchor), + baseline: .alphabetic, + place: place, + opacity: opacity) + } else if let tspanElement = element as? XMLElement, + tspanElement.name == "tspan" { + // parse as element + // ultimately skip it if it cannot be parsed + text = parseTspan(tspanElement, + withWhitespace: addWhitespace, + textAnchor: textAnchor, + fill: fill, + stroke: stroke, + opacity: opacity, + fontName: fontName, + fontSize: fontSize, + fontWeight: fontWeight, + bounds: bounds, + previousCollectedTspan: collection.last) + preserveWhitespace = true + addWhitespace = false + } else { + print("Skipped an unexpected element type: \(type(of: element)).") + text = nil + } + + if let text = text { + collection.append(text) + bounds = Rect(x: bounds.x, y: bounds.y, w: bounds.w + text.bounds.w, h: bounds.h) + } } - // parse as regular text element - var textString: NSString - if tagRange.location >= fullString.length { - textString = fullString - } else { - textString = fullString.substring(to: tagRange.location) as NSString - } - var nextStringWhitespace = false - var trimmedString = textString.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) - if trimmedString.count != textString.length { - nextStringWhitespace = true - } - trimmedString = withWhitespace ? " \(trimmedString)" : trimmedString - let text = Text(text: trimmedString, - font: getFont(fontName: fontName, fontWeight: fontWeight, fontSize: fontSize), - fill: fill, - stroke: stroke, - align: anchorToAlign(textAnchor), - baseline: .alphabetic, - place: Transform().move(dx: bounds.x + bounds.w, dy: bounds.y), opacity: opacity) - collection.append(text) - if tagRange.location >= fullString.length { // leave recursion - return collection - } - return collectTspans(fullString.substring(from: tagRange.location), - collectedTspans: collection, - withWhitespace: nextStringWhitespace, - textAnchor: textAnchor, - fill: fill, - stroke: stroke, - opacity: opacity, - fontName: fontName, - fontSize: fontSize, - fontWeight: fontWeight, - bounds: Rect(x: bounds.x, y: bounds.y, w: bounds.w + text.bounds.w, h: bounds.h)) + + return collection } - fileprivate func parseTspan(_ tspan: XMLIndexer, + fileprivate func parseTspan(_ element: XMLElement, withWhitespace: Bool = false, textAnchor: String?, fill: Fill?, @@ -1220,10 +1188,6 @@ open class SVGParser { bounds: Rect, previousCollectedTspan: Node?) -> Text? { - guard let element = tspan.element else { - return .none - } - let string = element.text var shouldAddWhitespace = withWhitespace let pos = getTspanPosition(element, diff --git a/Source/svg/SVGParserRegexHelper.swift b/Source/svg/SVGParserRegexHelper.swift deleted file mode 100644 index d55cd182..00000000 --- a/Source/svg/SVGParserRegexHelper.swift +++ /dev/null @@ -1,18 +0,0 @@ -import Foundation - -class SVGParserRegexHelper { - - fileprivate static let textElementPattern = "((?s:.*))<\\/text>" - fileprivate static var textElementMatcher: NSRegularExpression? - - class func getTextElementMatcher() -> NSRegularExpression? { - if self.textElementMatcher == nil { - do { - self.textElementMatcher = try NSRegularExpression(pattern: textElementPattern, options: .caseInsensitive) - } catch { - - } - } - return self.textElementMatcher - } -}