/* Copyright (C) 2024 Rubén Beltrán del Río This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see https://map.tranquil.systems. */ import CoreGraphics import Foundation struct MapParser { static func parse(content: String) -> ParsedMap { let parsers = [ AnyMapParserStrategy(NoteParserStrategy()), AnyMapParserStrategy(VertexParserStrategy()), AnyMapParserStrategy(EdgeParserStrategy()), AnyMapParserStrategy(BlockerParserStrategy()), AnyMapParserStrategy(OpportunityParserStrategy()), AnyMapParserStrategy(StageParserStrategy()), AnyMapParserStrategy(GroupParserStrategy()), ] let builder = MapBuilder() let lines = content.split(whereSeparator: \.isNewline) for (index, line) in lines.enumerated() { for parser in parsers { if parser.canHandle(line: String(line)) { let (type, object) = parser.handle( index: index, line: String(line), vertices: builder.vertices) builder.addObjectToMap(type: type, object: object) break } } } return builder.build() } } // MARK: - Types struct MapParsingPatterns { static let vertex = try! NSRegularExpression( pattern: "^([^\\(\\[\\]]*?)[\\s]*\\([\\s]*([0-9]+.?[0-9]*)[\\s]*,[\\s]*([0-9]+.?[0-9]*)[\\s]*\\)[\\s]*(?:\\[(.*?)\\])?[\\s]*$", options: [.caseInsensitive, .anchorsMatchLines]) static let edge = try! NSRegularExpression( pattern: "^(.+?)[\\s]*-([->])[\\s]*(.+)", options: [.caseInsensitive, .anchorsMatchLines]) static let blocker = try! NSRegularExpression( pattern: "^\\[(Blocker)\\][\\s]*(.+)", options: [.caseInsensitive, .anchorsMatchLines]) static let opportunity = try! NSRegularExpression( pattern: "^\\[(Evolution)\\][\\s]*(.+)[\\s]+([-+])[\\s]*([0-9]+.?[0-9]*)", options: [.caseInsensitive, .anchorsMatchLines]) static let note = try! NSRegularExpression( pattern: "^\\[(Note)\\][\\s]*\\([\\s]*([0-9]+.?[0-9]*)[\\s]*,[\\s]*([0-9]+.?[0-9]*)[\\s]*\\)[\\s]*(.*)", options: [.caseInsensitive, .anchorsMatchLines]) static let stage = try! NSRegularExpression( pattern: "^\\[(I{1,3})\\][\\s]*([0-9]+.?[0-9]*)", options: [.caseInsensitive, .anchorsMatchLines]) static let group = try! NSRegularExpression( pattern: "^\\[(Group)\\][\\s]*(.+)", options: [.caseInsensitive, .anchorsMatchLines]) } struct ParsedMap { let vertices: [Vertex] let edges: [MapEdge] let blockers: [Blocker] let opportunities: [Opportunity] let notes: [Note] let stages: [CGFloat] let groups: [[Vertex]] static let empty: ParsedMap = ParsedMap( vertices: [], edges: [], blockers: [], opportunities: [], notes: [], stages: defaultDimensions, groups: []) } struct Vertex: Identifiable, Hashable { let id: Int let label: String let position: CGPoint var shape: VertexShape = .circle } struct Note { let id: Int let position: CGPoint let text: String } enum VertexShape: String { case circle case square case triangle case x } struct MapEdge { let id: Int let origin: CGPoint let destination: CGPoint let arrowhead: Bool } struct Blocker { let id: Int let position: CGPoint } struct Opportunity { let id: Int let origin: CGPoint let destination: CGPoint } struct StageDimensions { let index: Int let dimensions: CGFloat } private let defaultDimensions: [CGFloat] = [ 25.0, 50.0, 75.0, ] // MARK: - MapParserStrategy protocol protocol MapParserStrategy { func canHandle(line: String) -> Bool func handle(index: Int, line: String, vertices: [String: Vertex]) -> (Any.Type, Any) } struct AnyMapParserStrategy: MapParserStrategy { private let base: MapParserStrategy init(_ base: T) { self.base = base } func canHandle(line: String) -> Bool { return base.canHandle(line: line) } func handle(index: Int, line: String, vertices: [String: Vertex]) -> (Any.Type, Any) { return base.handle(index: index, line: line, vertices: vertices) } } // MARK: - Map Builder class MapBuilder { var vertices: [String: Vertex] = [:] private var edges: [MapEdge] = [] private var blockers: [Blocker] = [] private var opportunities: [Opportunity] = [] private var notes: [Note] = [] private var stages: [CGFloat] = defaultDimensions private var groups: [[Vertex]] = [] func addObjectToMap(type: Any.Type, object: Any) { if type == Vertex.self { let vertex = object as! Vertex vertices[vertex.label] = vertex } if type == MapEdge.self { let edge = object as! MapEdge edges.append(edge) } if type == Blocker.self { let blocker = object as! Blocker blockers.append(blocker) } if type == Opportunity.self { let opportunity = object as! Opportunity opportunities.append(opportunity) } if type == Note.self { let note = object as! Note notes.append(note) } if type == StageDimensions.self { let stageDimensions = object as! StageDimensions stages[stageDimensions.index] = stageDimensions.dimensions } if type == [Vertex].self { let group = object as! [Vertex] groups.append(group) } } func build() -> ParsedMap { let mappedVertices = vertices.map { label, vertex in return vertex } return ParsedMap( vertices: mappedVertices, edges: edges, blockers: blockers, opportunities: opportunities, notes: notes, stages: stages, groups: groups) } }