B539516C25CB0C9300959F72 /* Store.swift in Sources */ = {isa = PBXBuildFile; fileRef = B539516B25CB0C9200959F72 /* Store.swift */; };
B539517425CB0CA400959F72 /* AppState.swift in Sources */ = {isa = PBXBuildFile; fileRef = B539517325CB0CA400959F72 /* AppState.swift */; };
B539518125CB2D7A00959F72 /* MapTextEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = B539518025CB2D7A00959F72 /* MapTextEditor.swift */; };
+ B5CF75C925CC19FC003BFF3D /* EvolutionPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5CF75C825CC19FC003BFF3D /* EvolutionPicker.swift */; };
+ B5CF75CF25CC7965003BFF3D /* MapParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5CF75CE25CC7965003BFF3D /* MapParser.swift */; };
+ B5CF75D825CC79BC003BFF3D /* VertexParserStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5CF75D725CC79BC003BFF3D /* VertexParserStrategy.swift */; };
+ B5CF75DD25CC79D7003BFF3D /* EdgeParserStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5CF75DC25CC79D7003BFF3D /* EdgeParserStrategy.swift */; };
+ B5CF75E225CC79ED003BFF3D /* BlockerParserStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5CF75E125CC79ED003BFF3D /* BlockerParserStrategy.swift */; };
+ B5CF75EA25CC7A13003BFF3D /* OpportunityParserStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5CF75E925CC7A13003BFF3D /* OpportunityParserStrategy.swift */; };
+ B5CF75EF25CC7A4A003BFF3D /* StageParserStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5CF75EE25CC7A4A003BFF3D /* StageParserStrategy.swift */; };
+ B5CF75F725CC97CA003BFF3D /* Debouncer.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5CF75F625CC97CA003BFF3D /* Debouncer.swift */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
B539516B25CB0C9200959F72 /* Store.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Store.swift; sourceTree = "<group>"; };
B539517325CB0CA400959F72 /* AppState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppState.swift; sourceTree = "<group>"; };
B539518025CB2D7A00959F72 /* MapTextEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapTextEditor.swift; sourceTree = "<group>"; };
+ B5CF75C825CC19FC003BFF3D /* EvolutionPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EvolutionPicker.swift; sourceTree = "<group>"; };
+ B5CF75CE25CC7965003BFF3D /* MapParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapParser.swift; sourceTree = "<group>"; };
+ B5CF75D725CC79BC003BFF3D /* VertexParserStrategy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VertexParserStrategy.swift; sourceTree = "<group>"; };
+ B5CF75DC25CC79D7003BFF3D /* EdgeParserStrategy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EdgeParserStrategy.swift; sourceTree = "<group>"; };
+ B5CF75E125CC79ED003BFF3D /* BlockerParserStrategy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockerParserStrategy.swift; sourceTree = "<group>"; };
+ B5CF75E925CC7A13003BFF3D /* OpportunityParserStrategy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpportunityParserStrategy.swift; sourceTree = "<group>"; };
+ B5CF75EE25CC7A4A003BFF3D /* StageParserStrategy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StageParserStrategy.swift; sourceTree = "<group>"; };
+ B5CF75F625CC97CA003BFF3D /* Debouncer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Debouncer.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
B523C76625CA071B00C44061 /* MapVertices.swift */,
B523C76B25CA0DFA00C44061 /* MapEdges.swift */,
B523C77025CA121300C44061 /* MapBlockers.swift */,
- B523C77525CA181100C44061 /* MapColor.swift */,
B523C77D25CA294C00C44061 /* MapOpportunities.swift */,
);
path = MapRenderComponents;
B526257025C874F9003E73B7 /* Map */ = {
isa = PBXGroup;
children = (
+ B523C77525CA181100C44061 /* MapColor.swift */,
+ B5CF75CD25CC7953003BFF3D /* MapParser */,
B539516A25CB0C7800959F72 /* State */,
B52625B425C87D54003E73B7 /* Extensions */,
B539517925CB0D6100959F72 /* Views */,
B526258025C874FA003E73B7 /* Map.entitlements */,
B526257C25C874FA003E73B7 /* Map.xcdatamodeld */,
B526257725C874FA003E73B7 /* Preview Content */,
+ B5CF75F625CC97CA003BFF3D /* Debouncer.swift */,
);
path = Map;
sourceTree = "<group>";
B539516A25CB0C7800959F72 /* State */ = {
isa = PBXGroup;
children = (
+ B52625C525C8BD2A003E73B7 /* Stage.swift */,
B526257A25C874FA003E73B7 /* Persistence.swift */,
B539516B25CB0C9200959F72 /* Store.swift */,
B539517325CB0CA400959F72 /* AppState.swift */,
B526257125C874F9003E73B7 /* MapApp.swift */,
B526257325C874F9003E73B7 /* ContentView.swift */,
B52625A525C876C3003E73B7 /* MapDetail.swift */,
- B52625C525C8BD2A003E73B7 /* Stage.swift */,
B523C74A25C9C1BA00C44061 /* DefaultMapView.swift */,
B539518025CB2D7A00959F72 /* MapTextEditor.swift */,
B52625AF25C87C14003E73B7 /* MapRender.swift */,
+ B5CF75C825CC19FC003BFF3D /* EvolutionPicker.swift */,
);
path = Views;
sourceTree = "<group>";
};
+ B5CF75CD25CC7953003BFF3D /* MapParser */ = {
+ isa = PBXGroup;
+ children = (
+ B5CF75D625CC79A4003BFF3D /* Strategies */,
+ B5CF75CE25CC7965003BFF3D /* MapParser.swift */,
+ );
+ path = MapParser;
+ sourceTree = "<group>";
+ };
+ B5CF75D625CC79A4003BFF3D /* Strategies */ = {
+ isa = PBXGroup;
+ children = (
+ B5CF75D725CC79BC003BFF3D /* VertexParserStrategy.swift */,
+ B5CF75DC25CC79D7003BFF3D /* EdgeParserStrategy.swift */,
+ B5CF75E125CC79ED003BFF3D /* BlockerParserStrategy.swift */,
+ B5CF75E925CC7A13003BFF3D /* OpportunityParserStrategy.swift */,
+ B5CF75EE25CC7A4A003BFF3D /* StageParserStrategy.swift */,
+ );
+ path = Strategies;
+ sourceTree = "<group>";
+ };
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
B52625B025C87C14003E73B7 /* MapRender.swift in Sources */,
B523C77E25CA294C00C44061 /* MapOpportunities.swift in Sources */,
B52625AB25C87909003E73B7 /* Date+format.swift in Sources */,
+ B5CF75D825CC79BC003BFF3D /* VertexParserStrategy.swift in Sources */,
+ B5CF75EA25CC7A13003BFF3D /* OpportunityParserStrategy.swift in Sources */,
B523C77125CA121300C44061 /* MapBlockers.swift in Sources */,
B523C76725CA071B00C44061 /* MapVertices.swift in Sources */,
+ B5CF75CF25CC7965003BFF3D /* MapParser.swift in Sources */,
+ B5CF75E225CC79ED003BFF3D /* BlockerParserStrategy.swift in Sources */,
B539517425CB0CA400959F72 /* AppState.swift in Sources */,
B523C75A25C9FD4900C44061 /* MapAxes.swift in Sources */,
B539518125CB2D7A00959F72 /* MapTextEditor.swift in Sources */,
B52625C625C8BD2A003E73B7 /* Stage.swift in Sources */,
B523C73D25C98D9800C44061 /* NSImage+writePNG.swift in Sources */,
B526257B25C874FA003E73B7 /* Persistence.swift in Sources */,
+ B5CF75EF25CC7A4A003BFF3D /* StageParserStrategy.swift in Sources */,
B523C77625CA181100C44061 /* MapColor.swift in Sources */,
+ B5CF75DD25CC79D7003BFF3D /* EdgeParserStrategy.swift in Sources */,
B526257425C874F9003E73B7 /* ContentView.swift in Sources */,
B526257E25C874FA003E73B7 /* Map.xcdatamodeld in Sources */,
+ B5CF75C925CC19FC003BFF3D /* EvolutionPicker.swift in Sources */,
B523C74B25C9C1BA00C44061 /* DefaultMapView.swift in Sources */,
B539516C25CB0C9300959F72 /* Store.swift in Sources */,
B526257225C874F9003E73B7 /* MapApp.swift in Sources */,
B52625A625C876C3003E73B7 /* MapDetail.swift in Sources */,
B523C76225CA05A300C44061 /* MapStages.swift in Sources */,
+ B5CF75F725CC97CA003BFF3D /* Debouncer.swift in Sources */,
B523C76C25CA0DFA00C44061 /* MapEdges.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 11;
- MARKETING_VERSION = 1.0.0;
+ MARKETING_VERSION = 1.1.0;
PRODUCT_BUNDLE_IDENTIFIER = pizza.unlimited.Map;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 11;
- MARKETING_VERSION = 1.0.0;
+ MARKETING_VERSION = 1.1.0;
PRODUCT_BUNDLE_IDENTIFIER = pizza.unlimited.Map;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
--- /dev/null
+import Foundation
+
+class Debouncer {
+
+ // MARK: - Properties
+ private let queue = DispatchQueue.main
+ private var workItem = DispatchWorkItem(block: {})
+ private var interval: TimeInterval
+
+ // MARK: - Initializer
+ init(seconds: TimeInterval) {
+ self.interval = seconds
+ }
+
+ // MARK: - Debouncing function
+ func debounce(action: @escaping (() -> Void)) {
+ workItem.cancel()
+ workItem = DispatchWorkItem(block: { action() })
+ queue.asyncAfter(deadline: .now() + interval, execute: workItem)
+ }
+}
-import CoreGraphics
-import Foundation
-
-let vertexPattern =
- "([^\\(]+?)[\\s]*\\([\\s]*([0-9]+.?[0-9]*)[\\s]*,[\\s]*([0-9]+.?[0-9]*)[\\s]*\\)"
-let edgePattern = "(.+?)[\\s]*-([->])[\\s]*(.+)"
-let blockerPattern = "\\[Blocker\\][\\s]*(.+)"
-let opportunityPattern = "\\[Opportunity\\][\\s]*(.+)[\\s]+([-+])[\\s]*([0-9]+.?[0-9]*)"
-let stagePattern = "\\[(I{1,3})\\][\\s]*([0-9]+.?[0-9]*)"
-
-struct ParsedMap {
- let vertices: [Vertex]
- let edges: [MapEdge]
- let blockers: [Blocker]
- let opportunities: [Opportunity]
- let stages: [CGFloat]
-}
-
-struct Vertex {
- let id: Int
- let label: String
- let position: CGPoint
-}
-
-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
-}
-
-let defaultDimensions: [CGFloat] = [
- 25.0,
- 50.0,
- 75.0,
-]
-
-// Extracts the vertices from the text
-
-func parseVertices(_ text: String) -> [String: CGPoint] {
-
- var result: [String: CGPoint] = [:]
- let regex = try! NSRegularExpression(pattern: vertexPattern, options: .caseInsensitive)
-
- let lines = text.split(whereSeparator: \.isNewline)
-
- for line in lines {
- let range = NSRange(location: 0, length: line.utf16.count)
- let matches = regex.matches(in: String(line), options: [], range: range)
-
- if matches.count > 0 && matches[0].numberOfRanges == 4 {
-
- let match = matches[0]
- let key = String(line[Range(match.range(at: 1), in: line)!])
- let xString = String(line[Range(match.range(at: 2), in: line)!])
- let yString = String(line[Range(match.range(at: 3), in: line)!])
- let x = CGFloat(truncating: NumberFormatter().number(from: xString) ?? 0.0)
- let y = CGFloat(truncating: NumberFormatter().number(from: yString) ?? 0.0)
- let point = CGPoint(x: x, y: y)
-
- result[key] = point
- }
- }
-
- return result
-}
-
-func parseEdges(_ text: String, vertices: [String: CGPoint]) -> [MapEdge] {
-
- var result: [MapEdge] = []
- let regex = try! NSRegularExpression(pattern: edgePattern, options: .caseInsensitive)
-
- let lines = text.split(whereSeparator: \.isNewline)
-
- for (index, line) in lines.enumerated() {
- let range = NSRange(location: 0, length: line.utf16.count)
- let matches = regex.matches(in: String(line), options: [], range: range)
-
- if matches.count > 0 && matches[0].numberOfRanges == 4 {
-
- let match = matches[0]
- let arrowhead = String(line[Range(match.range(at: 2), in: line)!]) == ">"
- let vertexA = String(line[Range(match.range(at: 1), in: line)!])
- let vertexB = String(line[Range(match.range(at: 3), in: line)!])
-
- if let origin = vertices[vertexA] {
- if let destination = vertices[vertexB] {
- result.append(
- MapEdge(id: index, origin: origin, destination: destination, arrowhead: arrowhead))
+extension Map {
+ static func parse(content: String) -> ParsedMap {
+
+ let parsers = [
+ AnyMapParserStrategy(VertexParserStrategy()),
+ AnyMapParserStrategy(EdgeParserStrategy()),
+ AnyMapParserStrategy(BlockerParserStrategy()),
+ AnyMapParserStrategy(OpportunityParserStrategy()),
+ AnyMapParserStrategy(StageParserStrategy()),
+ ]
+ 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 result
-}
-
-func parseOpportunities(_ text: String, vertices: [String: CGPoint]) -> [Opportunity] {
-
- var result: [Opportunity] = []
- let regex = try! NSRegularExpression(pattern: opportunityPattern, options: .caseInsensitive)
-
- let lines = text.split(whereSeparator: \.isNewline)
-
- for (index, line) in lines.enumerated() {
- let range = NSRange(location: 0, length: line.utf16.count)
- let matches = regex.matches(in: String(line), options: [], range: range)
-
- if matches.count > 0 && matches[0].numberOfRanges == 4 {
-
- let match = matches[0]
- let multiplier = CGFloat(
- String(line[Range(match.range(at: 2), in: line)!]) == "-" ? -1.0 : 1.0)
- let vertex = String(line[Range(match.range(at: 1), in: line)!])
- let opportunityString = String(line[Range(match.range(at: 3), in: line)!])
- let opportunity = CGFloat(
- truncating: NumberFormatter().number(from: opportunityString) ?? 0.0)
-
- if let origin = vertices[vertex] {
- let destination = CGPoint(x: origin.x + opportunity * multiplier, y: origin.y)
- result.append(Opportunity(id: index, origin: origin, destination: destination))
- }
- }
- }
-
- return result
-}
-
-func parseBlockers(_ text: String, vertices: [String: CGPoint]) -> [Blocker] {
-
- var result: [Blocker] = []
- let regex = try! NSRegularExpression(pattern: blockerPattern, options: .caseInsensitive)
-
- let lines = text.split(whereSeparator: \.isNewline)
-
- for (index, line) in lines.enumerated() {
- let range = NSRange(location: 0, length: line.utf16.count)
- let matches = regex.matches(in: String(line), options: [], range: range)
-
- if matches.count > 0 && matches[0].numberOfRanges == 2 {
-
- let match = matches[0]
- let vertexA = String(line[Range(match.range(at: 1), in: line)!])
-
- if let position = vertices[vertexA] {
- result.append(Blocker(id: index, position: position))
- }
- }
- }
-
- return result
-}
-
-func parseStages(_ text: String) -> [CGFloat] {
-
- var result = defaultDimensions
- let regex = try! NSRegularExpression(pattern: stagePattern, options: .caseInsensitive)
-
- let lines = text.split(whereSeparator: \.isNewline)
-
- for line in lines {
- let range = NSRange(location: 0, length: line.utf16.count)
- let matches = regex.matches(in: String(line), options: [], range: range)
-
- if matches.count > 0 && matches[0].numberOfRanges == 3 {
-
- let match = matches[0]
- let stage = String(line[Range(match.range(at: 1), in: line)!])
- let dimensionsString = String(line[Range(match.range(at: 2), in: line)!])
- let dimensions = CGFloat(truncating: NumberFormatter().number(from: dimensionsString) ?? 0.0)
-
- result[stage.count - 1] = dimensions
- }
- }
-
- return result
-}
-
-// Converts vetex dictionary to array
-
-func mapVertices(_ vertices: [String: CGPoint]) -> [Vertex] {
- var i = 0
- return vertices.map { label, position in
- i += 1
- return Vertex(id: i, label: label, position: position)
- }
-}
-
-extension Map {
- func parse() -> ParsedMap {
-
- let text = self.content ?? ""
- let vertices = parseVertices(text)
- let mappedVertices = mapVertices(vertices)
- let edges = parseEdges(text, vertices: vertices)
- let blockers = parseBlockers(text, vertices: vertices)
- let opportunities = parseOpportunities(text, vertices: vertices)
- let stages = parseStages(text)
- return ParsedMap(
- vertices: mappedVertices, edges: edges, blockers: blockers, opportunities: opportunities,
- stages: stages)
+ return builder.build()
}
}
let blocker: Color
let opportunity: Color
let stages: StageColor
+ let syntax: SyntaxColor
static func colorForScheme(_ colorScheme: ColorScheme) -> MapColor {
if colorScheme == .dark {
i: Color(.sRGB, red: 0.37, green: 0.16, blue: 0.25),
ii: Color(.sRGB, red: 0.30, green: 0.29, blue: 0.26),
iii: Color(.sRGB, red: 0.15, green: 0.29, blue: 0.23),
- iv: Color(.sRGB, red: 0.14, green: 0.22, blue: 0.31)))
+ iv: Color(.sRGB, red: 0.14, green: 0.22, blue: 0.31)),
+ syntax: SyntaxColor(
+ vertex: NSColor(srgbRed: 0.41, green: 0.84, blue: 0.96, alpha: 1.0),
+ number: NSColor(srgbRed: 0.85, green: 0.78, blue: 0.49, alpha: 1.0),
+ option: NSColor(srgbRed: 1.0, green: 0.48, blue: 0.7, alpha: 1.0), // #FE7AB3
+ symbol: NSColor(srgbRed: 0.85, green: 0.73, blue: 1.0, alpha: 1.0) // #DABBFF
+ ))
} else {
return MapColor(
foreground: Color(.sRGB, red: 0.13, green: 0.13, blue: 0.13),
i: Color(.sRGB, red: 1.00, green: 0.93, blue: 0.97),
ii: Color(.sRGB, red: 1.00, green: 0.98, blue: 0.92),
iii: Color(.sRGB, red: 0.93, green: 1.00, blue: 0.97),
- iv: Color(.sRGB, red: 0.93, green: 0.96, blue: 1.00)))
+ iv: Color(.sRGB, red: 0.93, green: 0.96, blue: 1.00)),
+ syntax: SyntaxColor(
+ vertex: NSColor(srgbRed: 0.11, green: 0.42, blue: 0.57, alpha: 1.0),
+ number: NSColor(srgbRed: 0.27, green: 0.31, blue: 0.87, alpha: 1.0),
+ option: NSColor(srgbRed: 0.68, green: 0.24, blue: 0.64, alpha: 1.0),
+ symbol: NSColor(srgbRed: 0.29, green: 0.13, blue: 0.69, alpha: 1.0)
+ ))
}
}
}
let iii: Color
let iv: Color
}
+
+struct SyntaxColor {
+ let vertex: NSColor
+ let number: NSColor
+ let option: NSColor
+ let symbol: NSColor
+}
--- /dev/null
+import CoreGraphics
+import Foundation
+
+// MARK: - Types
+
+struct MapParsingPatterns {
+ static let vertex = try! NSRegularExpression(
+ pattern:
+ "([^\\(]+?)[\\s]*\\([\\s]*([0-9]+.?[0-9]*)[\\s]*,[\\s]*([0-9]+.?[0-9]*)[\\s]*\\)[\\s]*(?:\\[(.*?)\\])?",
+ options: .caseInsensitive)
+ static let edge = try! NSRegularExpression(
+ pattern: "(.+?)[\\s]*-([->])[\\s]*(.+)", options: .caseInsensitive)
+ static let blocker = try! NSRegularExpression(
+ pattern: "\\[(Blocker)\\][\\s]*(.+)", options: .caseInsensitive)
+ static let opportunity = try! NSRegularExpression(
+ pattern: "\\[(Opportunity)\\][\\s]*(.+)[\\s]+([-+])[\\s]*([0-9]+.?[0-9]*)",
+ options: .caseInsensitive)
+ static let stage = try! NSRegularExpression(
+ pattern: "\\[(I{1,3})\\][\\s]*([0-9]+.?[0-9]*)", options: .caseInsensitive)
+}
+
+struct ParsedMap {
+ let vertices: [Vertex]
+ let edges: [MapEdge]
+ let blockers: [Blocker]
+ let opportunities: [Opportunity]
+ let stages: [CGFloat]
+}
+
+struct Vertex {
+ let id: Int
+ let label: String
+ let position: CGPoint
+ var shape: VertexShape = .circle
+}
+
+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<T: MapParserStrategy>(_ 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 stages: [CGFloat] = defaultDimensions
+
+ 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 == StageDimensions.self {
+ let stageDimensions = object as! StageDimensions
+ stages[stageDimensions.index] = stageDimensions.dimensions
+
+ }
+ }
+
+ func build() -> ParsedMap {
+ let mappedVertices = vertices.map { label, vertex in return vertex }
+ return ParsedMap(
+ vertices: mappedVertices, edges: edges, blockers: blockers, opportunities: opportunities,
+ stages: stages)
+ }
+}
--- /dev/null
+import Foundation
+
+struct BlockerParserStrategy: MapParserStrategy {
+ private let regex = MapParsingPatterns.blocker
+
+ func canHandle(line: String) -> Bool {
+ let range = NSRange(location: 0, length: line.utf16.count)
+ let matches = regex.matches(in: String(line), options: [], range: range)
+ return matches.count > 0 && matches[0].numberOfRanges == 3
+ }
+
+ func handle(index: Int, line: String, vertices: [String: Vertex]) -> (Any.Type, Any) {
+ let range = NSRange(location: 0, length: line.utf16.count)
+ let matches = regex.matches(in: String(line), options: [], range: range)
+
+ let match = matches[0]
+ let vertexA = String(line[Range(match.range(at: 2), in: line)!])
+
+ if let vertex = vertices[vertexA] {
+ let blocker = Blocker(id: index, position: vertex.position)
+ return (Blocker.self, blocker)
+ }
+
+ return (NSObject.self, NSObject()) // No matching object
+ }
+}
--- /dev/null
+import Foundation
+
+struct EdgeParserStrategy: MapParserStrategy {
+ private let regex = MapParsingPatterns.edge
+
+ func canHandle(line: String) -> Bool {
+ let range = NSRange(location: 0, length: line.utf16.count)
+ let matches = regex.matches(in: String(line), options: [], range: range)
+ return matches.count > 0 && matches[0].numberOfRanges == 4
+ }
+
+ func handle(index: Int, line: String, vertices: [String: Vertex]) -> (Any.Type, Any) {
+ let range = NSRange(location: 0, length: line.utf16.count)
+ let matches = regex.matches(in: String(line), options: [], range: range)
+
+ let match = matches[0]
+ let arrowhead = String(line[Range(match.range(at: 2), in: line)!]) == ">"
+ let vertexA = String(line[Range(match.range(at: 1), in: line)!])
+ let vertexB = String(line[Range(match.range(at: 3), in: line)!])
+
+ if let origin = vertices[vertexA] {
+ if let destination = vertices[vertexB] {
+ let edge = MapEdge(
+ id: index, origin: origin.position, destination: destination.position,
+ arrowhead: arrowhead)
+ return (MapEdge.self, edge)
+ }
+ }
+
+ return (NSObject.self, NSObject()) // No matching object
+ }
+}
--- /dev/null
+import Foundation
+
+struct OpportunityParserStrategy: MapParserStrategy {
+ private let regex = MapParsingPatterns.opportunity
+
+ func canHandle(line: String) -> Bool {
+ let range = NSRange(location: 0, length: line.utf16.count)
+ let matches = regex.matches(in: String(line), options: [], range: range)
+ return matches.count > 0 && matches[0].numberOfRanges == 5
+ }
+
+ func handle(index: Int, line: String, vertices: [String: Vertex]) -> (Any.Type, Any) {
+ let range = NSRange(location: 0, length: line.utf16.count)
+ let matches = regex.matches(in: String(line), options: [], range: range)
+
+ let match = matches[0]
+ let multiplier = CGFloat(
+ String(line[Range(match.range(at: 3), in: line)!]) == "-" ? -1.0 : 1.0)
+ let vertex = String(line[Range(match.range(at: 2), in: line)!])
+ let opportunityString = String(line[Range(match.range(at: 4), in: line)!])
+ let opportunity = CGFloat(
+ truncating: NumberFormatter().number(from: opportunityString) ?? 0.0)
+
+ if let origin = vertices[vertex] {
+ let destination = CGPoint(
+ x: origin.position.x + opportunity * multiplier, y: origin.position.y)
+ let opportunity = Opportunity(id: index, origin: origin.position, destination: destination)
+ return (Opportunity.self, opportunity)
+ }
+
+ return (NSObject.self, NSObject()) // No matching object
+ }
+}
--- /dev/null
+import Foundation
+
+struct StageParserStrategy: MapParserStrategy {
+ private let regex = MapParsingPatterns.stage
+
+ func canHandle(line: String) -> Bool {
+ let range = NSRange(location: 0, length: line.utf16.count)
+ let matches = regex.matches(in: String(line), options: [], range: range)
+ return matches.count > 0 && matches[0].numberOfRanges == 3
+ }
+
+ func handle(index: Int, line: String, vertices: [String: Vertex]) -> (Any.Type, Any) {
+ let range = NSRange(location: 0, length: line.utf16.count)
+ let matches = regex.matches(in: String(line), options: [], range: range)
+
+ let match = matches[0]
+ let stage = String(line[Range(match.range(at: 1), in: line)!])
+ let dimensionsString = String(line[Range(match.range(at: 2), in: line)!])
+ let dimensions = CGFloat(truncating: NumberFormatter().number(from: dimensionsString) ?? 0.0)
+
+ let stageDimensions = StageDimensions(index: stage.count - 1, dimensions: dimensions)
+ return (StageDimensions.self, stageDimensions)
+ }
+}
--- /dev/null
+import Foundation
+
+struct VertexParserStrategy: MapParserStrategy {
+ private let regex = MapParsingPatterns.vertex
+
+ func canHandle(line: String) -> Bool {
+ let range = NSRange(location: 0, length: line.utf16.count)
+ let matches = regex.matches(in: String(line), options: [], range: range)
+ return matches.count > 0 && matches[0].numberOfRanges >= 4
+ }
+
+ func handle(index: Int, line: String, vertices: [String: Vertex]) -> (Any.Type, Any) {
+ let range = NSRange(location: 0, length: line.utf16.count)
+ let matches = regex.matches(in: String(line), options: [], range: range)
+
+ let match = matches[0]
+ let key = String(line[Range(match.range(at: 1), in: line)!])
+ let xString = String(line[Range(match.range(at: 2), in: line)!])
+ let yString = String(line[Range(match.range(at: 3), in: line)!])
+ let x = CGFloat(truncating: NumberFormatter().number(from: xString) ?? 0.0)
+ let y = CGFloat(truncating: NumberFormatter().number(from: yString) ?? 0.0)
+
+ var vertex = Vertex(
+ id: index,
+ label: key,
+ position: CGPoint(x: x, y: y)
+ )
+
+ if let range = Range(match.range(at: 4), in: line) {
+ let shapeString = String(line[range])
+ vertex.shape = VertexShape(rawValue: shapeString.lowercased()) ?? .circle
+ }
+
+ return (Vertex.self, vertex)
+ }
+}
let lineWidth: CGFloat
let evolution: Stage
let stages: [CGFloat]
- let stageHeight = CGFloat(50.0)
+ let stageHeight = CGFloat(100.0)
let padding = CGFloat(5.0)
var color: Color {
Text("Uncharted")
.font(.title3)
.foregroundColor(color)
- .frame(width: mapSize.width / 4, height: stageHeight, alignment: .topLeading)
- .offset(CGSize(width: 0.0, height: -stageHeight / 2.0))
+ .frame(width: mapSize.width / 4, height: stageHeight / 2.0, alignment: .topLeading)
+ .offset(CGSize(width: 0.0, height: -stageHeight / 4.0))
Text("Industrialised")
.font(.title3)
.foregroundColor(color)
- .frame(width: mapSize.width / 4, height: stageHeight, alignment: .topLeading)
- .offset(CGSize(width: mapSize.width - 100.0, height: -stageHeight / 2.0))
+ .frame(width: mapSize.width / 4, height: stageHeight / 2.0, alignment: .topLeading)
+ .offset(CGSize(width: mapSize.width - 100.0, height: -stageHeight / 4.0))
Text(evolution.i)
.font(.title3)
var body: some View {
ForEach(vertices, id: \.id) { vertex in
- Path { path in
- path.addEllipse(
- in: CGRect(
- origin: CGPoint(x: w(vertex.position.x), y: h(vertex.position.y)), size: vertexSize
- ))
- }.fill(color.foreground)
+ getVertexShape(vertex).fill(color.foreground)
Text(vertex.label).foregroundColor(color.secondary).offset(
CGSize(
width: w(vertex.position.x) + vertexSize.width + padding,
func w(_ dimension: CGFloat) -> CGFloat {
max(0.0, min(mapSize.width, dimension * mapSize.width / 100.0))
}
+
+ func getVertexShape(_ vertex: Vertex) -> Path {
+ switch vertex.shape {
+ case .circle:
+ return Path { path in
+ path.addEllipse(
+ in: CGRect(
+ origin: CGPoint(x: w(vertex.position.x), y: h(vertex.position.y)), size: vertexSize
+ ))
+ }
+ case .square:
+ return Path { path in
+ path.addRect(
+ CGRect(
+ x: w(vertex.position.x), y: h(vertex.position.y), width: vertexSize.width,
+ height: vertexSize.height
+ ))
+ }
+ case .triangle:
+ return Path { path in
+ path.move(to: CGPoint(x: w(vertex.position.x), y: h(vertex.position.y) + vertexSize.height))
+ path.addLine(
+ to: CGPoint(
+ x: w(vertex.position.x) + vertexSize.width, y: h(vertex.position.y) + vertexSize.height)
+ )
+ path.addLine(
+ to: CGPoint(x: w(vertex.position.x) + vertexSize.width / 2.0, y: h(vertex.position.y)))
+ path.addLine(
+ to: CGPoint(x: w(vertex.position.x), y: h(vertex.position.y) + vertexSize.height))
+ path.closeSubpath()
+ }
+ case .x:
+ return Path { path in
+ path.move(to: CGPoint(x: w(vertex.position.x), y: h(vertex.position.y)))
+ path.addLine(
+ to: CGPoint(
+ x: w(vertex.position.x) + vertexSize.width, y: h(vertex.position.y) + vertexSize.height)
+ )
+ path.closeSubpath()
+ path.move(to: CGPoint(x: w(vertex.position.x) + vertexSize.width, y: h(vertex.position.y)))
+ path.addLine(
+ to: CGPoint(x: w(vertex.position.x), y: h(vertex.position.y) + vertexSize.height))
+ path.closeSubpath()
+ }.strokedPath(StrokeStyle(lineWidth: 5.0, lineCap: .butt))
+ }
+ }
}
struct MapVertices_Previews: PreviewProvider {
import SwiftUI
struct AppState {
- var selectedMap: Map? = nil
+ var selectedEvolution: StageType = .general
var mapBeingDeleted: Map? = nil
}
enum AppAction {
- case selectMap(map: Map?)
- case deleteMap(map: Map)
- case exportMapAsImage(map: Map, evolution: StageType)
+ case selectEvolution(evolution: StageType)
+ case exportMapAsImage(map: Map)
case exportMapAsText(map: Map)
}
switch action {
- case .selectMap(let map):
- state.selectedMap = map
+ case .selectEvolution(let evolution):
+ state.selectedEvolution = evolution
- case .deleteMap(let map):
- let context = PersistenceController.shared.container.viewContext
-
- context.delete(map)
- try? context.save()
-
- case .exportMapAsImage(let map, let evolution):
+ case .exportMapAsImage(let map):
let window = NSWindow(
contentRect: .init(
origin: .zero,
window.isMovableByWindowBackground = true
window.makeKeyAndOrderFront(nil)
- let renderView = MapRenderView(map: map, evolution: Stage.stages(evolution))
+ let renderView = MapRenderView(
+ content: map.content ?? "", evolution: Stage.stages(state.selectedEvolution))
let view = NSHostingView(rootView: renderView)
window.contentView = view
switch type {
case .general:
return Stage(
- i: "Genesis", ii: "Custom Built", iii: "Product (+rental)", iv: "Commodity (+utility)")
+ i: "Genesis", ii: "Custom", iii: "Product (+rental)", iv: "Commodity (+utility)")
case .practice:
return Stage(
i: "Novel", ii: "Emerging", iii: "Good", iv: "Best")
i: "Concept", ii: "Hypothesis", iii: "Theory", iv: "Accepted")
case .ubiquity:
return Stage(
- i: "Rare", ii: "Slowly Increasing Consumption", iii: "Rapidly Increasing Consumption",
- iv: "Widespread and stabilising")
+ i: "Rare", ii: "Slowly Increasing", iii: "Rapidly Increasing",
+ iv: "Widespread in the applicable market / ecosystem")
case .certainty:
return Stage(
- i: "Poorly Understood", ii: "Rapid Increase In Learning",
- iii: "Rapid Increase in Use / fit for purpose", iv: "Commonly understood (in terms of use)")
+ i: "Poorly Understood / exploring the unknown",
+ ii: "Rapid Increase In Learning / discovery becomes refining",
+ iii: "Rapid increase in use / increasing fit for purpose",
+ iv: "Commonly understood (in terms of use)")
case .publicationTypes:
return Stage(
- i: "Normally describing the wonder of the thing",
- ii: "Build / construct / awareness and learning",
- iii: "Maintenance / operations / installation / feature", iv: "Focused on use")
+ i:
+ "Describe the wonder of the thing / the discovery of some marvel / a new land / an unknown frontier",
+ ii:
+ "Focused on build / construct / awareness and learning / many models of explanation / no accepted forms / a wild west",
+ iii:
+ "Maintenance / operations / installation / comparison between competing forms / feature analysis",
+ iv: "Focused on use / increasingly an accepted, almost invisible component")
case .market:
return Stage(
- i: "Undefined Market", ii: "Forming Market", iii: "Growing Market", iv: "Mature Market")
+ i: "Undefined Market",
+ ii: "Forming Market / an array of competing forms and models of understanding",
+ iii: "Growing Market / consolidation to a few competing but more accepted forms",
+ iv: "Mature Market / stabilised to an accepted form")
case .knowledgeManagement:
return Stage(
- i: "Uncertain", ii: "Learning on use", iii: "Learning on operation", iv: "Known / accepted")
+ i: "Uncertain", ii: "Learning on use / focused on testing prediction",
+ iii: "Learning on operation / using prediction / verification", iv: "Known / accepted")
case .marketPerception:
return Stage(
- i: "Chaotic (non-linear)", ii: "Domain of experts", iii: "Increasing expectation of use",
- iv: "Ordered (appearance of being trivial) / trivial")
+ i: "Chaotic (non-linear) / domain of the \"crazy\"", ii: "Domain of \"experts\"",
+ iii: "Increasing expectation of use / domain of \"professionals\"",
+ iv: "Ordered (appearance of being linear) / trivial / formula to be applied")
case .userPerception:
return Stage(
- i: "Different / confusing / exciting / surprising", ii: "Leading edge / emerging",
- iii: "Increasingly common / disappointed if not used", iv: "Standard / expected")
+ i: "Different / confusing / exciting / surprising / dangerous",
+ ii: "Leading edge / emerging / unceirtanty over results",
+ iii: "Increasingly common / disappointed if not used or available / feeling left behind",
+ iv: "Standard / expected / feeling of shock if not used")
case .perceptionInIndustry:
return Stage(
- i: "Competitive advantage / unpredictable / unknown",
- ii: "Competitive advantage / ROI / case examples",
- iii: "Advantage through implementation / features", iv: "Cost of doing business")
+ i: "Future source of competitive advantage / unpredictable / unknown",
+ ii: "Seen as a scompetitive advantage / a differential / ROI / case examples",
+ iii: "Advantage through implementation / features / this model is better than that",
+ iv: "Cost of doing business / accepted / specific defined models")
case .focusOfValue:
return Stage(
- i: "High future worth", ii: "Seeking profit / ROI", iii: "High profitability",
- iv: "High volume / reducing margin")
+ i: "High future worth but immediate investment",
+ ii: "Seeking ways to profit and a ROI / seeking confirmation of value",
+ iii:
+ "High profitability per unit / a valuable model / a feeling of understanding / focus on exploitation",
+ iv:
+ "High volume / reducing margin / important but invisible / an essential component of something more complex"
+ )
case .understanding:
return Stage(
i: "Poorly Understood / unpredictable",
return Stage(
i: "Constantly changing / a differential / unstable",
ii: "Learning from others / testing the water / some evidential support",
- iii: "Feature difference", iv: "Essential / operational advantage")
+ iii: "Competing models / feature difference / evidential support",
+ iv: "Essential / any advantage is operational / accepted norm")
case .failure:
return Stage(
- i: "High / tolerated / assumed", ii: "Moderate / unsurprising but disappointed",
- iii: "Not tolerated, focus on constant improvement",
- iv: "Operational efficiency and surprised by failure")
+ i: "High / tolerated / assumed to be wrong",
+ ii: "Moderate / unsurprising if wrong but disappointed",
+ iii:
+ "Not tolerated / focus on constant improvement / assumed to be in the right direction / resistance to changing the model",
+ iv: "Surprised by failure / focus on operational efficiency")
case .marketAction:
return Stage(
i: "Gambling / driven by gut", ii: "Exploring a \"found\" value",
case practice
case data
case knowledge
+
case ubiquity
case certainty
case publicationTypes
+
case market
case knowledgeManagement
case marketPerception
case marketAction
case efficiency
case decisionDrivers
+
case behavior
var id: String { self.rawValue }
+
+ static let types: [StageType] = [.general, .practice, .data, .knowledge]
+ static let characteristics: [StageType] = [.ubiquity, .certainty, .publicationTypes]
+ static let properties: [StageType] = [
+ .market, .knowledgeManagement, .marketPerception, .userPerception,
+ .perceptionInIndustry, .focusOfValue, .understanding, .comparison, .failure,
+ .marketAction, .efficiency, .decisionDrivers,
+ ]
+ static let custom: [StageType] = [.behavior]
}
struct ContentView: View {
@Environment(\.managedObjectContext) private var viewContext
+ @EnvironmentObject var store: AppStore
+
@FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \Map.createdAt, ascending: true)],
animation: .default)
struct DefaultMapView_Previews: PreviewProvider {
static var previews: some View {
- MapDetailView(map: Map()).environment(
+ DefaultMapView().environment(
\.managedObjectContext, PersistenceController.preview.container.viewContext)
}
}
--- /dev/null
+//
+// EvolutionPicker.swift
+// Map
+//
+// Created by Ruben Beltran del Rio on 2/4/21.
+//
+
+import SwiftUI
+
+struct EvolutionPicker: View {
+
+ @EnvironmentObject private var store: AppStore
+
+ private var selectedEvolution: Binding<StageType> {
+ Binding(
+ get: { store.state.selectedEvolution },
+ set: { evolution in
+ store.send(.selectEvolution(evolution: evolution))
+ }
+ )
+ }
+
+ var body: some View {
+ Picker("Evolution", selection: selectedEvolution) {
+ ForEach(StageType.types) { stage in
+ Text(Stage.title(stage)).tag(stage).padding(4.0)
+ }
+ Divider()
+ ForEach(StageType.characteristics) { stage in
+ Text(Stage.title(stage)).tag(stage).padding(4.0)
+ }
+ Divider()
+ ForEach(StageType.properties) { stage in
+ Text(Stage.title(stage)).tag(stage).padding(4.0)
+ }
+ Divider()
+ ForEach(StageType.custom) { stage in
+ Text(Stage.title(stage)).tag(stage).padding(4.0)
+ }
+ }.padding(.horizontal, 8.0).padding(.vertical, 4.0)
+ }
+}
+
+struct EvolutionPicker_Previews: PreviewProvider {
+ static var previews: some View {
+ EvolutionPicker()
+ }
+}
)
}
- @State private var selectedEvolution = StageType.general
-
var body: some View {
if map.uuid != nil {
VSplitView {
Image(systemName: "photo")
}.padding(.vertical, 4.0).padding(.leading, 4.0).padding(.trailing, 8.0)
}
- Picker("Evolution", selection: $selectedEvolution) {
- ForEach(StageType.allCases) { stage in
- Text(Stage.title(stage)).tag(stage).padding(4.0)
- }
- }.padding(.horizontal, 8.0).padding(.vertical, 4.0)
+ EvolutionPicker()
ZStack(alignment: .topLeading) {
- MapTextEditor(text: content).onChange(of: map.content) { _ in
+ MapTextEditor(text: content, colorScheme: colorScheme).onChange(of: map.content) { _ in
try? viewContext.save()
}
.background(mapColor.background)
5.0)
}.padding(.horizontal, 8.0)
ScrollView([.horizontal, .vertical]) {
- MapRenderView(map: map, evolution: Stage.stages(selectedEvolution))
+ MapRenderView(
+ content: content.wrappedValue, evolution: Stage.stages(store.state.selectedEvolution))
}.background(mapColor.background)
}
} else {
}
private func saveImage() {
- store.send(.exportMapAsImage(map: map, evolution: selectedEvolution))
+ store.send(.exportMapAsImage(map: map))
}
}
// Created by Ruben Beltran del Rio on 2/1/21.
//
+import Combine
import CoreData
import CoreGraphics
import SwiftUI
@Environment(\.colorScheme) var colorScheme
- @ObservedObject var map: Map
+ var content: String
let evolution: Stage
let mapSize = CGSize(width: 1300.0, height: 1000.0)
let padding = CGFloat(30.0)
var parsedMap: ParsedMap {
- return map.parse()
+ return Map.parse(content: content)
}
var body: some View {
path.addRect(
CGRect(
x: -padding, y: -padding, width: mapSize.width + padding * 2,
- height: mapSize.height + padding * 2))
+ height: mapSize.height + padding * 4))
}.fill(MapColor.colorForScheme(colorScheme).background)
MapStages(mapSize: mapSize, lineWidth: lineWidth, stages: parsedMap.stages)
mapSize: mapSize, lineWidth: lineWidth, vertexSize: vertexSize, edges: parsedMap.edges)
}.frame(
width: mapSize.width,
- height: mapSize.height, alignment: .topLeading
+ height: mapSize.height + 2 * padding, alignment: .topLeading
).padding(padding)
}
}
struct MapRenderView_Previews: PreviewProvider {
static var previews: some View {
- MapDetailView(map: Map()).environment(
+ MapRenderView(content: "", evolution: Stage.stages(.general)).environment(
\.managedObjectContext, PersistenceController.preview.container.viewContext)
}
}
class MapTextEditorController: NSViewController {
@Binding var text: String
+ var colorScheme: ColorScheme
- init(text: Binding<String>) {
+ private let vertexRegex = MapParsingPatterns.vertex
+ private let edgeRegex = MapParsingPatterns.edge
+ private let blockerRegex = MapParsingPatterns.blocker
+ private let opportunityRegex = MapParsingPatterns.opportunity
+ private let stageRegex = MapParsingPatterns.stage
+
+ private let debouncer: Debouncer = Debouncer(seconds: 0.2)
+
+ init(text: Binding<String>, colorScheme: ColorScheme) {
self._text = text
+ self.colorScheme = colorScheme
super.init(nibName: nil, bundle: nil)
}
scrollView.translatesAutoresizingMaskIntoConstraints = false
textView.delegate = self
+ textView.textStorage?.delegate = self
textView.string = self.text
textView.isEditable = true
textView.font = .monospacedSystemFont(ofSize: 16.0, weight: .regular)
override func viewDidAppear() {
self.view.window?.makeFirstResponder(self.view)
}
+
+ func updateColorScheme(_ colorScheme: ColorScheme) {
+ self.colorScheme = colorScheme
+ }
}
extension MapTextEditorController: NSTextViewDelegate {
}
}
+extension MapTextEditorController: NSTextStorageDelegate {
+ override func textStorageDidProcessEditing(_ obj: Notification) {
+ if let textStorage = obj.object as? NSTextStorage {
+ debouncer.debounce {
+ DispatchQueue.main.async {
+ self.colorizeText(textStorage: textStorage)
+ }
+ }
+ }
+ }
+
+ private func colorizeText(textStorage: NSTextStorage) {
+ let range = NSMakeRange(0, textStorage.length)
+ var matches = vertexRegex.matches(in: textStorage.string, options: [], range: range)
+ let colors = MapColor.colorForScheme(colorScheme)
+
+ for match in matches {
+ textStorage.addAttributes([.foregroundColor: colors.syntax.vertex], range: match.range(at: 1))
+ textStorage.addAttributes([.foregroundColor: colors.syntax.number], range: match.range(at: 2))
+ textStorage.addAttributes([.foregroundColor: colors.syntax.number], range: match.range(at: 3))
+ textStorage.addAttributes([.foregroundColor: colors.syntax.option], range: match.range(at: 4))
+ }
+
+ matches = edgeRegex.matches(in: textStorage.string, options: [], range: range)
+
+ for match in matches {
+ textStorage.addAttributes([.foregroundColor: colors.syntax.vertex], range: match.range(at: 1))
+ let arrowRange = match.range(at: 2)
+ textStorage.addAttributes(
+ [.foregroundColor: colors.syntax.symbol],
+ range: NSMakeRange(arrowRange.lowerBound - 1, arrowRange.length + 1))
+ textStorage.addAttributes([.foregroundColor: colors.syntax.vertex], range: match.range(at: 3))
+ }
+
+ matches = opportunityRegex.matches(in: textStorage.string, options: [], range: range)
+
+ for match in matches {
+ textStorage.addAttributes([.foregroundColor: colors.syntax.option], range: match.range(at: 1))
+ textStorage.addAttributes([.foregroundColor: colors.syntax.vertex], range: match.range(at: 2))
+ textStorage.addAttributes([.foregroundColor: colors.syntax.symbol], range: match.range(at: 3))
+ textStorage.addAttributes([.foregroundColor: colors.syntax.number], range: match.range(at: 4))
+ }
+
+ matches = blockerRegex.matches(in: textStorage.string, options: [], range: range)
+
+ for match in matches {
+ textStorage.addAttributes([.foregroundColor: colors.syntax.option], range: match.range(at: 1))
+ textStorage.addAttributes([.foregroundColor: colors.syntax.vertex], range: match.range(at: 2))
+ }
+
+ matches = stageRegex.matches(in: textStorage.string, options: [], range: range)
+
+ for match in matches {
+ textStorage.addAttributes([.foregroundColor: colors.syntax.option], range: match.range(at: 1))
+ textStorage.addAttributes([.foregroundColor: colors.syntax.number], range: match.range(at: 2))
+ }
+ }
+}
+
struct MapTextEditor: NSViewControllerRepresentable {
@Binding var text: String
+ let colorScheme: ColorScheme
func makeNSViewController(
context: NSViewControllerRepresentableContext<MapTextEditor>
) -> MapTextEditorController {
- return MapTextEditorController(text: $text)
+ return MapTextEditorController(text: $text, colorScheme: colorScheme)
}
func updateNSViewController(
_ nsViewController: MapTextEditorController,
context: NSViewControllerRepresentableContext<MapTextEditor>
) {
+ nsViewController.updateColorScheme(context.environment.colorScheme)
}
}