aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.periphery.yml3
-rw-r--r--Map.xcodeproj/project.pbxproj115
-rw-r--r--Map.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved38
-rw-r--r--Map/Business/RenderableMap.swift198
-rw-r--r--Map/Business/WmapParser/Entity.swift440
-rw-r--r--Map/Business/WmapParser/Lexer.swift463
-rw-r--r--Map/Business/WmapParser/Parser.swift504
-rw-r--r--Map/Business/WmapParser/SourcePosition.swift35
-rw-r--r--Map/Business/WmapParser/Syntax.swift32
-rw-r--r--Map/Business/WmapParser/Token.swift22
-rw-r--r--Map/Business/WmapParser/TokenType.swift36
-rw-r--r--Map/Business/WmapSyntaxHighlighter/SyntaxHighlighter.swift102
-rw-r--r--Map/Data/MapDocument.swift9
-rw-r--r--Map/MapApp.swift45
-rw-r--r--Map/Presentation/Base Components/EvolutionPicker/EvolutionPicker.swift1
-rw-r--r--Map/Presentation/Base Components/EvolutionPicker/EvolutionPickerMenuHeader.swift1
-rw-r--r--Map/Presentation/Base Components/EvolutionPicker/GlassEvolutionPicker.swift1
-rw-r--r--Map/Presentation/Base Components/MapRender/MapAxes.swift2
-rw-r--r--Map/Presentation/Base Components/MapRender/MapBackground.swift11
-rw-r--r--Map/Presentation/Base Components/MapRender/MapEdges.swift1
-rw-r--r--Map/Presentation/Base Components/MapRender/MapGroup.swift3
-rw-r--r--Map/Presentation/Base Components/MapRender/MapGroups.swift2
-rw-r--r--Map/Presentation/Base Components/MapRender/MapMask.swift2
-rw-r--r--Map/Presentation/Base Components/MapRender/MapStages.swift3
-rw-r--r--Map/Presentation/Base Components/MapRender/MapVertices.swift4
-rw-r--r--Map/Presentation/Base Components/MapTextEditor/MapTextEditor.swift89
-rw-r--r--Map/Presentation/Base Components/SearchBar.swift1
-rw-r--r--Map/Presentation/Commands/MapCommands.swift4
-rw-r--r--Map/Presentation/Complex Components/MapRender/MapRenderView.swift45
-rw-r--r--Map/Presentation/MapEditor.swift11
-rw-r--r--Map/Presentation/PreferencesWindow.swift12
-rw-r--r--Map/Presentation/Theme/Color+theme.swift3
-rw-r--r--Map/Presentation/Theme/Dimensions.swift11
-rw-r--r--Map/Presentation/Theme/Font+theme.swift6
-rw-r--r--Map/Presentation/Theme/NSColor+theme.swift34
-rw-r--r--Map/Presentation/Theme/NSFont+theme.swift18
-rw-r--r--QuickLook/PreviewViewController.swift101
37 files changed, 420 insertions, 1988 deletions
diff --git a/.periphery.yml b/.periphery.yml
new file mode 100644
index 0000000..2db2f33
--- /dev/null
+++ b/.periphery.yml
@@ -0,0 +1,3 @@
+project: Map.xcodeproj
+schemes:
+- Map
diff --git a/Map.xcodeproj/project.pbxproj b/Map.xcodeproj/project.pbxproj
index 020ce52..f19865e 100644
--- a/Map.xcodeproj/project.pbxproj
+++ b/Map.xcodeproj/project.pbxproj
@@ -14,6 +14,14 @@
B5282B122E210D4500C62006 /* MapQuickLook.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = B5282B022E210D4400C62006 /* MapQuickLook.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
B5282B3D2E21109C00C62006 /* Patterns in Frameworks */ = {isa = PBXBuildFile; productRef = B5282B3C2E21109C00C62006 /* Patterns */; };
B5282B3F2E21109C00C62006 /* ConcaveHull in Frameworks */ = {isa = PBXBuildFile; productRef = B5282B3E2E21109C00C62006 /* ConcaveHull */; };
+ B5C5921A2EEF0B0F000A31F5 /* WmapParser in Frameworks */ = {isa = PBXBuildFile; productRef = B5C592192EEF0B0F000A31F5 /* WmapParser */; };
+ B5C5921C2EEF1E67000A31F5 /* WmapParser in Frameworks */ = {isa = PBXBuildFile; productRef = B5C5921B2EEF1E67000A31F5 /* WmapParser */; };
+ B5C5921F2EEF4212000A31F5 /* TreeSitterWmap in Frameworks */ = {isa = PBXBuildFile; productRef = B5C5921E2EEF4212000A31F5 /* TreeSitterWmap */; };
+ B5C592212EEF4235000A31F5 /* TreeSitterWmap in Frameworks */ = {isa = PBXBuildFile; productRef = B5C592202EEF4235000A31F5 /* TreeSitterWmap */; };
+ B5C5922C2EEF4C22000A31F5 /* SwiftTreeSitter in Frameworks */ = {isa = PBXBuildFile; productRef = B5C5922B2EEF4C22000A31F5 /* SwiftTreeSitter */; };
+ B5C5922E2EEF4C22000A31F5 /* SwiftTreeSitterLayer in Frameworks */ = {isa = PBXBuildFile; productRef = B5C5922D2EEF4C22000A31F5 /* SwiftTreeSitterLayer */; };
+ B5C592302EEF4C2F000A31F5 /* SwiftTreeSitter in Frameworks */ = {isa = PBXBuildFile; productRef = B5C5922F2EEF4C2F000A31F5 /* SwiftTreeSitter */; };
+ B5C592322EEF4C2F000A31F5 /* SwiftTreeSitterLayer in Frameworks */ = {isa = PBXBuildFile; productRef = B5C592312EEF4C2F000A31F5 /* SwiftTreeSitterLayer */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@@ -101,14 +109,6 @@
Business/SmartLabelPositioning/SmartLabelPositioning.swift,
Business/Stage.swift,
Business/Template.swift,
- Business/WmapParser/Entity.swift,
- Business/WmapParser/Lexer.swift,
- Business/WmapParser/Parser.swift,
- Business/WmapParser/SourcePosition.swift,
- Business/WmapParser/Syntax.swift,
- Business/WmapParser/Token.swift,
- Business/WmapParser/TokenType.swift,
- Business/WmapSyntaxHighlighter/SyntaxHighlighter.swift,
Data/UserPreferences.swift,
"Data/UTType+wmap.swift",
Localizable.xcstrings,
@@ -178,7 +178,11 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
+ B5C5921C2EEF1E67000A31F5 /* WmapParser in Frameworks */,
B5282B052E210D4500C62006 /* Quartz.framework in Frameworks */,
+ B5C592212EEF4235000A31F5 /* TreeSitterWmap in Frameworks */,
+ B5C592302EEF4C2F000A31F5 /* SwiftTreeSitter in Frameworks */,
+ B5C592322EEF4C2F000A31F5 /* SwiftTreeSitterLayer in Frameworks */,
B5282B3D2E21109C00C62006 /* Patterns in Frameworks */,
B5282B3F2E21109C00C62006 /* ConcaveHull in Frameworks */,
);
@@ -188,7 +192,11 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
+ B5C5921A2EEF0B0F000A31F5 /* WmapParser in Frameworks */,
B51A45F52C982FAE00870ED5 /* Sparkle in Frameworks */,
+ B5C5921F2EEF4212000A31F5 /* TreeSitterWmap in Frameworks */,
+ B5C5922C2EEF4C22000A31F5 /* SwiftTreeSitter in Frameworks */,
+ B5C5922E2EEF4C22000A31F5 /* SwiftTreeSitterLayer in Frameworks */,
B5012E742C9625E200AC4D68 /* Patterns in Frameworks */,
B5012E7F2C97315800AC4D68 /* ConcaveHull in Frameworks */,
);
@@ -268,6 +276,10 @@
packageProductDependencies = (
B5282B3C2E21109C00C62006 /* Patterns */,
B5282B3E2E21109C00C62006 /* ConcaveHull */,
+ B5C5921B2EEF1E67000A31F5 /* WmapParser */,
+ B5C592202EEF4235000A31F5 /* TreeSitterWmap */,
+ B5C5922F2EEF4C2F000A31F5 /* SwiftTreeSitter */,
+ B5C592312EEF4C2F000A31F5 /* SwiftTreeSitterLayer */,
);
productName = QuickLook;
productReference = B5282B022E210D4400C62006 /* MapQuickLook.appex */;
@@ -297,6 +309,10 @@
B5012E732C9625E200AC4D68 /* Patterns */,
B5012E7E2C97315800AC4D68 /* ConcaveHull */,
B51A45F42C982FAE00870ED5 /* Sparkle */,
+ B5C592192EEF0B0F000A31F5 /* WmapParser */,
+ B5C5921E2EEF4212000A31F5 /* TreeSitterWmap */,
+ B5C5922B2EEF4C22000A31F5 /* SwiftTreeSitter */,
+ B5C5922D2EEF4C22000A31F5 /* SwiftTreeSitterLayer */,
);
productName = Map;
productReference = B545870C2C961E9C0067B788 /* Map.app */;
@@ -382,6 +398,9 @@
B5012E722C9625E200AC4D68 /* XCRemoteSwiftPackageReference "patterns" */,
B5012E7D2C97315800AC4D68 /* XCRemoteSwiftPackageReference "ConcaveHull" */,
B51A45F32C982FAE00870ED5 /* XCRemoteSwiftPackageReference "Sparkle" */,
+ B5C592182EEF0B0F000A31F5 /* XCRemoteSwiftPackageReference "wmap-parser-swift" */,
+ B5C5921D2EEF4212000A31F5 /* XCRemoteSwiftPackageReference "tree-sitter-wmap" */,
+ B5C5922A2EEF4C22000A31F5 /* XCRemoteSwiftPackageReference "swift-tree-sitter" */,
);
preferredProjectObjectVersion = 60;
productRefGroup = B545870D2C961E9C0067B788 /* Products */;
@@ -483,7 +502,7 @@
buildSettings = {
CODE_SIGN_ENTITLEMENTS = QuickLook/QuickLook.entitlements;
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 61;
+ CURRENT_PROJECT_VERSION = 70;
ENABLE_HARDENED_RUNTIME = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = QuickLook/Info.plist;
@@ -495,7 +514,7 @@
"@executable_path/../../../../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 15.5;
- MARKETING_VERSION = 4.1.1;
+ MARKETING_VERSION = 4.3.0;
PRODUCT_BUNDLE_IDENTIFIER = systems.tranquil.Map.QuickLook;
PRODUCT_NAME = "$(TARGET_NAME)";
REGISTER_APP_GROUPS = YES;
@@ -511,7 +530,7 @@
buildSettings = {
CODE_SIGN_ENTITLEMENTS = QuickLook/QuickLook.entitlements;
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 61;
+ CURRENT_PROJECT_VERSION = 70;
ENABLE_HARDENED_RUNTIME = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = QuickLook/Info.plist;
@@ -523,7 +542,7 @@
"@executable_path/../../../../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 15.5;
- MARKETING_VERSION = 4.1.1;
+ MARKETING_VERSION = 4.3.0;
PRODUCT_BUNDLE_IDENTIFIER = systems.tranquil.Map.QuickLook;
PRODUCT_NAME = "$(TARGET_NAME)";
REGISTER_APP_GROUPS = YES;
@@ -666,7 +685,7 @@
CODE_SIGN_ENTITLEMENTS = Map/Map.entitlements;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
- CURRENT_PROJECT_VERSION = 69;
+ CURRENT_PROJECT_VERSION = 70;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_ASSET_PATHS = "\"Map/Preview Content\"";
ENABLE_HARDENED_RUNTIME = YES;
@@ -680,7 +699,7 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 14;
- MARKETING_VERSION = 4.2.0;
+ MARKETING_VERSION = 4.3.0;
PRODUCT_BUNDLE_IDENTIFIER = systems.tranquil.Map;
PRODUCT_NAME = "$(TARGET_NAME)";
REGISTER_APP_GROUPS = YES;
@@ -699,7 +718,7 @@
CODE_SIGN_ENTITLEMENTS = Map/Map.entitlements;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
- CURRENT_PROJECT_VERSION = 69;
+ CURRENT_PROJECT_VERSION = 70;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_ASSET_PATHS = "\"Map/Preview Content\"";
ENABLE_HARDENED_RUNTIME = YES;
@@ -713,7 +732,7 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 14;
- MARKETING_VERSION = 4.2.0;
+ MARKETING_VERSION = 4.3.0;
PRODUCT_BUNDLE_IDENTIFIER = systems.tranquil.Map;
PRODUCT_NAME = "$(TARGET_NAME)";
REGISTER_APP_GROUPS = YES;
@@ -866,6 +885,30 @@
minimumVersion = 2.6.4;
};
};
+ B5C592182EEF0B0F000A31F5 /* XCRemoteSwiftPackageReference "wmap-parser-swift" */ = {
+ isa = XCRemoteSwiftPackageReference;
+ repositoryURL = "https://git.sr.ht/~rbdr/wmap-parser-swift";
+ requirement = {
+ branch = main;
+ kind = branch;
+ };
+ };
+ B5C5921D2EEF4212000A31F5 /* XCRemoteSwiftPackageReference "tree-sitter-wmap" */ = {
+ isa = XCRemoteSwiftPackageReference;
+ repositoryURL = "https://git.sr.ht/~rbdr/tree-sitter-wmap";
+ requirement = {
+ branch = main;
+ kind = branch;
+ };
+ };
+ B5C5922A2EEF4C22000A31F5 /* XCRemoteSwiftPackageReference "swift-tree-sitter" */ = {
+ isa = XCRemoteSwiftPackageReference;
+ repositoryURL = "https://github.com/tree-sitter/swift-tree-sitter";
+ requirement = {
+ kind = upToNextMajorVersion;
+ minimumVersion = 0.25.0;
+ };
+ };
/* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
@@ -894,6 +937,46 @@
package = B5012E7D2C97315800AC4D68 /* XCRemoteSwiftPackageReference "ConcaveHull" */;
productName = ConcaveHull;
};
+ B5C592192EEF0B0F000A31F5 /* WmapParser */ = {
+ isa = XCSwiftPackageProductDependency;
+ package = B5C592182EEF0B0F000A31F5 /* XCRemoteSwiftPackageReference "wmap-parser-swift" */;
+ productName = WmapParser;
+ };
+ B5C5921B2EEF1E67000A31F5 /* WmapParser */ = {
+ isa = XCSwiftPackageProductDependency;
+ package = B5C592182EEF0B0F000A31F5 /* XCRemoteSwiftPackageReference "wmap-parser-swift" */;
+ productName = WmapParser;
+ };
+ B5C5921E2EEF4212000A31F5 /* TreeSitterWmap */ = {
+ isa = XCSwiftPackageProductDependency;
+ package = B5C5921D2EEF4212000A31F5 /* XCRemoteSwiftPackageReference "tree-sitter-wmap" */;
+ productName = TreeSitterWmap;
+ };
+ B5C592202EEF4235000A31F5 /* TreeSitterWmap */ = {
+ isa = XCSwiftPackageProductDependency;
+ package = B5C5921D2EEF4212000A31F5 /* XCRemoteSwiftPackageReference "tree-sitter-wmap" */;
+ productName = TreeSitterWmap;
+ };
+ B5C5922B2EEF4C22000A31F5 /* SwiftTreeSitter */ = {
+ isa = XCSwiftPackageProductDependency;
+ package = B5C5922A2EEF4C22000A31F5 /* XCRemoteSwiftPackageReference "swift-tree-sitter" */;
+ productName = SwiftTreeSitter;
+ };
+ B5C5922D2EEF4C22000A31F5 /* SwiftTreeSitterLayer */ = {
+ isa = XCSwiftPackageProductDependency;
+ package = B5C5922A2EEF4C22000A31F5 /* XCRemoteSwiftPackageReference "swift-tree-sitter" */;
+ productName = SwiftTreeSitterLayer;
+ };
+ B5C5922F2EEF4C2F000A31F5 /* SwiftTreeSitter */ = {
+ isa = XCSwiftPackageProductDependency;
+ package = B5C5922A2EEF4C22000A31F5 /* XCRemoteSwiftPackageReference "swift-tree-sitter" */;
+ productName = SwiftTreeSitter;
+ };
+ B5C592312EEF4C2F000A31F5 /* SwiftTreeSitterLayer */ = {
+ isa = XCSwiftPackageProductDependency;
+ package = B5C5922A2EEF4C22000A31F5 /* XCRemoteSwiftPackageReference "swift-tree-sitter" */;
+ productName = SwiftTreeSitterLayer;
+ };
/* End XCSwiftPackageProductDependency section */
};
rootObject = B54587042C961E9C0067B788 /* Project object */;
diff --git a/Map.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Map.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
index 8e294b8..519bc42 100644
--- a/Map.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
+++ b/Map.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
@@ -1,5 +1,5 @@
{
- "originHash" : "761de39b705b9793661607e3e8ffa75397e9772bccc8dc4c303ed42c76e874de",
+ "originHash" : "4db2597b88285f3511f8b59a0a29d034c4bf7642aed1bab0bdf0f4ec3479b739",
"pins" : [
{
"identity" : "concavehull",
@@ -27,6 +27,42 @@
"revision" : "0ef1ee0220239b3776f433314515fd849025673f",
"version" : "2.6.4"
}
+ },
+ {
+ "identity" : "swift-tree-sitter",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/tree-sitter/swift-tree-sitter",
+ "state" : {
+ "revision" : "08ef81eb8620617b55b08868126707ad72bf754f",
+ "version" : "0.25.0"
+ }
+ },
+ {
+ "identity" : "tree-sitter",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/tree-sitter/tree-sitter",
+ "state" : {
+ "revision" : "da6fe9beb4f7f67beb75914ca8e0d48ae48d6406",
+ "version" : "0.25.10"
+ }
+ },
+ {
+ "identity" : "tree-sitter-wmap",
+ "kind" : "remoteSourceControl",
+ "location" : "https://git.sr.ht/~rbdr/tree-sitter-wmap",
+ "state" : {
+ "branch" : "main",
+ "revision" : "c01881ae8910f11653e69d7bbded07e4413991bc"
+ }
+ },
+ {
+ "identity" : "wmap-parser-swift",
+ "kind" : "remoteSourceControl",
+ "location" : "https://git.sr.ht/~rbdr/wmap-parser-swift",
+ "state" : {
+ "branch" : "main",
+ "revision" : "969b6a6653699b9f8662feb7f42511740f96f84a"
+ }
}
],
"version" : 3
diff --git a/Map/Business/RenderableMap.swift b/Map/Business/RenderableMap.swift
index d43b584..82acaff 100644
--- a/Map/Business/RenderableMap.swift
+++ b/Map/Business/RenderableMap.swift
@@ -13,6 +13,7 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see https://map.tranquil.systems.
import Foundation
+import WmapParser
struct RenderableMap {
let vertices: [Vertex]
@@ -20,111 +21,101 @@ struct RenderableMap {
let inertias: [Inertia]
let opportunities: [Opportunity]
let notes: [Note]
- let stages: [CGFloat]
+ let stages: [Double]
let groups: [[Vertex]]
static let empty: RenderableMap = RenderableMap(
vertices: [], edges: [], inertias: [], opportunities: [], notes: [], stages: defaultDimensions,
groups: [])
- static func from(_ entities: [Wmap.Entity]) -> RenderableMap {
+ static func from(_ map: WmapParser.Map) -> RenderableMap {
var vertices: [Vertex] = []
var edges: [MapEdge] = []
var inertias: [Inertia] = []
var opportunities: [Opportunity] = []
var notes: [Note] = []
- var stages: [CGFloat] = [25.0, 50.0, 75.0] // default dimensions
+ var stages: [Double] = [25.0, 50.0, 75.0] // default dimensions
var groups: [[Vertex]] = []
// Build a vertex lookup for edges, groups, etc.
var vertexMap: [String: Vertex] = [:]
- var vertexIdCounter = 0
-
- for entity in entities {
- switch entity {
- case .vertex(let vertex):
- let newVertex = Vertex(
- id: vertexIdCounter,
- label: vertex.label,
- position: CGPoint(x: vertex.coordinates.0, y: vertex.coordinates.1),
- shape: convertShape(vertex.shape)
- )
- vertices.append(newVertex)
- vertexMap[vertex.label.lowercased()] = newVertex
- vertexIdCounter += 1
-
- case .note(let note):
- notes.append(
- Note(
- id: notes.count,
- position: CGPoint(x: note.coordinates.0, y: note.coordinates.1),
- text: note.text
- ))
-
- case .stage(let stage):
- let stageIndex = convertStageNumber(stage.number) - 1
- if stageIndex >= 0 && stageIndex < 3 {
- stages[stageIndex] = stage.value
- }
+
+ for (index, component) in map.components.enumerated() {
+ let newVertex = Vertex(
+ id: index,
+ label: component.label,
+ position: CGPoint(x: component.coordinates.xCoordinate, y: component.coordinates.yCoordinate),
+ shape: component.shape
+ )
+ vertices.append(newVertex)
+ vertexMap[component.label.lowercased()] = newVertex
+ }
+
+ for (index, note) in map.notes.enumerated() {
+ notes.append(
+ Note(
+ id: index,
+ position: CGPoint(x: note.coordinates.xCoordinate, y: note.coordinates.yCoordinate),
+ text: note.text
+ ))
+ }
- default:
- continue
+ for stage in map.stages {
+ let stageIndex = stageToIndex(stage.stage)
+ if stageIndex >= 0 && stageIndex < 3 {
+ stages[stageIndex] = stage.value
}
}
// Second pass for entities that depend on vertices
- for entity in entities {
- switch entity {
- case .edge(let edge):
- if let fromVertex = vertexMap[edge.from.lowercased()],
- let toVertex = vertexMap[edge.to.lowercased()]
- {
- edges.append(
- MapEdge(
- id: edges.count,
- origin: fromVertex.position,
- destination: toVertex.position,
- arrowhead: edge.isDirected
- ))
- }
-
- case .inertia(let inertia):
- if let vertex = vertexMap[inertia.vertex.lowercased()] {
- inertias.append(
- Inertia(
- id: inertias.count,
- position: vertex.position
- ))
- }
-
- case .evolution(let evolution):
- if let vertex = vertexMap[evolution.vertex.lowercased()] {
- let direction: CGFloat = evolution.isPositive ? 1.0 : -1.0
- let destination = CGPoint(
- x: vertex.position.x + (direction * evolution.value),
- y: vertex.position.y
- )
- opportunities.append(
- Opportunity(
- id: opportunities.count,
- origin: vertex.position,
- destination: destination
- ))
- }
-
- case .group(let group):
- var groupVertices: [Vertex] = []
- for vertexName in group.vertices {
- if let vertex = vertexMap[vertexName.lowercased()] {
- groupVertices.append(vertex)
- }
- }
- if !groupVertices.isEmpty {
- groups.append(groupVertices)
+ for (index, dependency) in map.dependencies.enumerated() {
+ if let fromVertex = vertexMap[dependency.fromComponent.lowercased()],
+ let toVertex = vertexMap[dependency.toComponent.lowercased()]
+ {
+ edges.append(
+ MapEdge(
+ id: index,
+ origin: fromVertex.position,
+ destination: toVertex.position,
+ arrowhead: dependency.isDirected
+ ))
+ }
+ }
+
+ for (index, inertia) in map.inertias.enumerated() {
+ if let vertex = vertexMap[inertia.component.lowercased()] {
+ inertias.append(
+ Inertia(
+ id: index,
+ position: vertex.position
+ ))
+ }
+ }
+
+ for (index, evolution) in map.evolutions.enumerated() {
+ if let vertex = vertexMap[evolution.component.lowercased()] {
+ let destination = CGPoint(
+ x: vertex.position.x + evolution.value,
+ y: vertex.position.y
+ )
+ opportunities.append(
+ Opportunity(
+ id: index,
+ origin: vertex.position,
+ destination: destination
+ ))
+ }
+ }
+
+ for group in map.groups {
+ var groupVertices: [Vertex] = []
+ for componentName in group.components {
+ if let vertex = vertexMap[componentName.lowercased()] {
+ groupVertices.append(vertex)
}
-
- default:
- continue
+ }
+ if !groupVertices.isEmpty {
+ groups.append(groupVertices)
}
}
@@ -139,23 +130,12 @@ struct RenderableMap {
)
}
- private static func convertShape(_ shape: String?) -> VertexShape {
- guard let shape = shape?.lowercased() else { return .circle }
- switch shape {
- case "square": return .square
- case "triangle": return .triangle
- case "x": return .x
- default: return .circle
- }
- }
-
- private static func convertStageNumber(_ stageNumber: String) -> Int {
- switch stageNumber.lowercased() {
- case "i": return 1
- case "ii": return 2
- case "iii": return 3
- case "iv": return 4
- default: return 1
+ private static func stageToIndex(_ stageNumber: WmapParser.StageNumber) -> Int {
+ switch stageNumber {
+ case .stageI: return 0
+ case .stageII: return 1
+ case .stageIII: return 2
+ case .stageIV: return 3
}
}
}
@@ -164,7 +144,7 @@ struct Vertex: Identifiable, Hashable {
let id: Int
let label: String
let position: CGPoint
- var shape: VertexShape = .circle
+ var shape: WmapParser.Shape = .circle
}
struct Note {
@@ -173,13 +153,6 @@ struct Note {
let text: String
}
-enum VertexShape: String {
- case circle
- case square
- case triangle
- case x
-}
-
struct MapEdge {
let id: Int
let origin: CGPoint
@@ -198,12 +171,7 @@ struct Opportunity {
let destination: CGPoint
}
-struct StageDimensions {
- let index: Int
- let dimensions: CGFloat
-}
-
-private let defaultDimensions: [CGFloat] = [
+private let defaultDimensions: [Double] = [
25.0,
50.0,
75.0,
diff --git a/Map/Business/WmapParser/Entity.swift b/Map/Business/WmapParser/Entity.swift
deleted file mode 100644
index 52f6e93..0000000
--- a/Map/Business/WmapParser/Entity.swift
+++ /dev/null
@@ -1,440 +0,0 @@
-struct Wmap {
- enum Entity: Equatable {
- case vertex(Vertex)
- case edge(Edge)
- case note(Note)
- case stage(Stage)
- case group(Group)
- case inertia(Inertia)
- case evolution(Evolution)
-
- var position: SourcePosition {
- switch self {
- case .vertex(let vertex): return vertex.position
- case .edge(let edge): return edge.position
- case .note(let note): return note.position
- case .stage(let stage): return stage.position
- case .group(let group): return group.position
- case .inertia(let inertia): return inertia.position
- case .evolution(let evolution): return evolution.position
- }
- }
-
- var range: SourceRange {
- switch self {
- case .vertex(let vertex): return vertex.range
- case .edge(let edge): return edge.range
- case .note(let note): return note.range
- case .stage(let stage): return stage.range
- case .group(let group): return group.range
- case .inertia(let inertia): return inertia.range
- case .evolution(let evolution): return evolution.range
- }
- }
-
- var syntaxElements: [SyntaxElement] {
- switch self {
- case .vertex(let vertex): return vertex.syntaxElements
- case .edge(let edge): return edge.syntaxElements
- case .note(let note): return note.syntaxElements
- case .stage(let stage): return stage.syntaxElements
- case .group(let group): return group.syntaxElements
- case .inertia(let inertia): return inertia.syntaxElements
- case .evolution(let evolution): return evolution.syntaxElements
- }
- }
- }
-
- struct Vertex: Equatable {
- let label: String
- let position: SourcePosition
- let coordinates: (Double, Double)
- let shape: String?
- let range: SourceRange
-
- // Syntax element ranges (populated during parsing)
- let labelRange: SourceRange
- let leftParenRange: SourceRange
- let xCoordRange: SourceRange
- let commaRange: SourceRange
- let yCoordRange: SourceRange
- let rightParenRange: SourceRange
- let shapeRange: SourceRange?
-
- static func == (lhs: Vertex, rhs: Vertex) -> Bool {
- return lhs.label == rhs.label && lhs.position == rhs.position
- && lhs.coordinates.0 == rhs.coordinates.0 && lhs.coordinates.1 == rhs.coordinates.1
- && lhs.shape == rhs.shape && lhs.range == rhs.range
- }
-
- var syntaxElements: [SyntaxElement] {
- var elements: [SyntaxElement] = []
-
- // Vertex label
- elements.append(
- SyntaxElement(
- type: .vertexLabel,
- range: labelRange,
- value: label
- ))
-
- // Left parenthesis
- elements.append(
- SyntaxElement(
- type: .punctuation,
- range: leftParenRange,
- value: "("
- ))
-
- // X coordinate
- elements.append(
- SyntaxElement(
- type: .number,
- range: xCoordRange,
- value: String(coordinates.0)
- ))
-
- // Comma
- elements.append(
- SyntaxElement(
- type: .punctuation,
- range: commaRange,
- value: ","
- ))
-
- // Y coordinate
- elements.append(
- SyntaxElement(
- type: .number,
- range: yCoordRange,
- value: String(coordinates.1)
- ))
-
- // Right parenthesis
- elements.append(
- SyntaxElement(
- type: .punctuation,
- range: rightParenRange,
- value: ")"
- ))
-
- // Shape if present
- if let shape = shape, let shapeRange = shapeRange {
- elements.append(
- SyntaxElement(
- type: .shape,
- range: shapeRange,
- value: "[\(shape)]"
- ))
- }
-
- return elements
- }
- }
-
- struct Edge: Equatable {
- let from: String
- let to: String
- let isDirected: Bool
- let position: SourcePosition
- let range: SourceRange
-
- // Syntax element ranges
- let fromLabelRange: SourceRange
- let edgeTypeRange: SourceRange
- let toLabelRange: SourceRange
-
- var syntaxElements: [SyntaxElement] {
- var elements: [SyntaxElement] = []
-
- // From vertex label
- elements.append(
- SyntaxElement(
- type: .vertexLabel,
- range: fromLabelRange,
- value: from
- ))
-
- // Edge symbol
- elements.append(
- SyntaxElement(
- type: .symbol,
- range: edgeTypeRange,
- value: isDirected ? "->" : "--"
- ))
-
- // To vertex label
- elements.append(
- SyntaxElement(
- type: .vertexLabel,
- range: toLabelRange,
- value: to
- ))
-
- return elements
- }
- }
-
- struct Note: Equatable {
- let coordinates: (Double, Double)
- let text: String
- let position: SourcePosition
- let range: SourceRange
-
- // Syntax element ranges
- let keywordRange: SourceRange
- let leftParenRange: SourceRange
- let xCoordRange: SourceRange
- let commaRange: SourceRange
- let yCoordRange: SourceRange
- let rightParenRange: SourceRange
- let textRange: SourceRange
-
- static func == (lhs: Note, rhs: Note) -> Bool {
- return lhs.coordinates.0 == rhs.coordinates.0 && lhs.coordinates.1 == rhs.coordinates.1
- && lhs.text == rhs.text && lhs.position == rhs.position && lhs.range == rhs.range
- }
-
- var syntaxElements: [SyntaxElement] {
- var elements: [SyntaxElement] = []
-
- // [Note] keyword
- elements.append(
- SyntaxElement(
- type: .keyword,
- range: keywordRange,
- value: "[Note]"
- ))
-
- // Left parenthesis
- elements.append(
- SyntaxElement(
- type: .punctuation,
- range: leftParenRange,
- value: "("
- ))
-
- // X coordinate
- elements.append(
- SyntaxElement(
- type: .number,
- range: xCoordRange,
- value: String(coordinates.0)
- ))
-
- // Comma
- elements.append(
- SyntaxElement(
- type: .punctuation,
- range: commaRange,
- value: ","
- ))
-
- // Y coordinate
- elements.append(
- SyntaxElement(
- type: .number,
- range: yCoordRange,
- value: String(coordinates.1)
- ))
-
- // Right parenthesis
- elements.append(
- SyntaxElement(
- type: .punctuation,
- range: rightParenRange,
- value: ")"
- ))
-
- // Text content
- elements.append(
- SyntaxElement(
- type: .text,
- range: textRange,
- value: text
- ))
-
- return elements
- }
- }
-
- struct Stage: Equatable {
- let number: String // i, ii, iii, iv
- let value: Double
- let position: SourcePosition
- let range: SourceRange
-
- // Syntax element ranges
- let leftBracketRange: SourceRange
- let stageNumberRange: SourceRange
- let rightBracketRange: SourceRange
- let valueRange: SourceRange
-
- var syntaxElements: [SyntaxElement] {
- var elements: [SyntaxElement] = []
-
- // Left bracket
- elements.append(
- SyntaxElement(
- type: .punctuation,
- range: leftBracketRange,
- value: "["
- ))
-
- // Stage number
- elements.append(
- SyntaxElement(
- type: .stageNumber,
- range: stageNumberRange,
- value: number
- ))
-
- // Right bracket
- elements.append(
- SyntaxElement(
- type: .punctuation,
- range: rightBracketRange,
- value: "]"
- ))
-
- // Value
- elements.append(
- SyntaxElement(
- type: .number,
- range: valueRange,
- value: String(value)
- ))
-
- return elements
- }
- }
-
- struct Group: Equatable {
- let vertices: [String]
- let position: SourcePosition
- let range: SourceRange
-
- // Syntax element ranges
- let keywordRange: SourceRange
- let vertexRanges: [SourceRange]
- let commaRanges: [SourceRange]
-
- var syntaxElements: [SyntaxElement] {
- var elements: [SyntaxElement] = []
-
- // [Group] keyword
- elements.append(
- SyntaxElement(
- type: .keyword,
- range: keywordRange,
- value: "[Group]"
- ))
-
- // Vertex labels and commas
- for (index, vertex) in vertices.enumerated() {
- elements.append(
- SyntaxElement(
- type: .vertexLabel,
- range: vertexRanges[index],
- value: vertex
- ))
-
- // Add comma if not the last vertex
- if index < vertices.count - 1 {
- elements.append(
- SyntaxElement(
- type: .punctuation,
- range: commaRanges[index],
- value: ","
- ))
- }
- }
-
- return elements
- }
- }
-
- struct Inertia: Equatable {
- let vertex: String
- let position: SourcePosition
- let range: SourceRange
-
- // Syntax element ranges
- let keywordRange: SourceRange
- let vertexRange: SourceRange
-
- var syntaxElements: [SyntaxElement] {
- var elements: [SyntaxElement] = []
-
- // [Inertia] keyword
- elements.append(
- SyntaxElement(
- type: .keyword,
- range: keywordRange,
- value: "[Inertia]"
- ))
-
- // Vertex label
- elements.append(
- SyntaxElement(
- type: .vertexLabel,
- range: vertexRange,
- value: vertex
- ))
-
- return elements
- }
- }
-
- struct Evolution: Equatable {
- let vertex: String
- let isPositive: Bool
- let value: Double
- let position: SourcePosition
- let range: SourceRange
-
- // Syntax element ranges
- let keywordRange: SourceRange
- let vertexRange: SourceRange
- let signRange: SourceRange
- let valueRange: SourceRange
-
- var syntaxElements: [SyntaxElement] {
- var elements: [SyntaxElement] = []
-
- // [Evolution] keyword
- elements.append(
- SyntaxElement(
- type: .keyword,
- range: keywordRange,
- value: "[Evolution]"
- ))
-
- // Vertex label
- elements.append(
- SyntaxElement(
- type: .vertexLabel,
- range: vertexRange,
- value: vertex
- ))
-
- // Sign
- elements.append(
- SyntaxElement(
- type: .symbol,
- range: signRange,
- value: isPositive ? "+" : "-"
- ))
-
- // Value
- elements.append(
- SyntaxElement(
- type: .number,
- range: valueRange,
- value: String(value)
- ))
-
- return elements
- }
- }
-}
diff --git a/Map/Business/WmapParser/Lexer.swift b/Map/Business/WmapParser/Lexer.swift
deleted file mode 100644
index 93ba712..0000000
--- a/Map/Business/WmapParser/Lexer.swift
+++ /dev/null
@@ -1,463 +0,0 @@
-// 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.
-extension Wmap {
- class Lexer {
- private enum LexerState {
- case normal
- case afterNoteKeyword
- case inNotePosition(tokensNeeded: Int) // tracks: leftParen(1), number(2), comma(3), number(4), rightParen(5)
- case expectingText
- }
-
- let input: String
- private var current: String.Index
- private var line = 1
- private var column = 1
- private var state: LexerState = .normal
-
- init(_ input: String) {
- self.input = input
- self.current = input.startIndex
- }
-
- func nextToken() -> Token {
- skipWhitespace()
-
- guard current < input.endIndex else {
- return Token(
- type: .eof,
- value: "",
- position: currentPosition(),
- stringRange: current..<current
- )
- }
-
- // Handle text scanning before character-specific parsing
- if case .expectingText = state {
- state = .normal // Reset after scanning text
- return scanText()
- }
-
- let char = input[current].lowercased().first!
- let pos = currentPosition()
- let tokenStart = current
-
- switch char {
- case "\n", "\r":
- state = .normal // Reset state on newline
- return scanLineEnding()
-
- case "(":
- updateStateForToken(.leftParen)
- advance()
- return Token(
- type: .leftParen,
- value: "(",
- position: pos,
- stringRange: tokenStart..<current
- )
-
- case ")":
- updateStateForToken(.rightParen)
- advance()
- return Token(
- type: .rightParen,
- value: ")",
- position: pos,
- stringRange: tokenStart..<current
- )
-
- case "[":
- return scanBracketedToken()
-
- case ",":
- updateStateForToken(.comma)
- advance()
- return Token(
- type: .comma,
- value: ",",
- position: pos,
- stringRange: tokenStart..<current
- )
-
- case "+":
- advance()
- return Token(
- type: .plus,
- value: "+",
- position: pos,
- stringRange: tokenStart..<current
- )
-
- case "-":
- if peek() == "-" {
- advance()
- advance()
- return Token(
- type: .edgeUndirected,
- value: "--",
- position: pos,
- stringRange: tokenStart..<current
- )
- } else if peek() == ">" {
- advance()
- advance()
- return Token(
- type: .edgeDirected,
- value: "->",
- position: pos,
- stringRange: tokenStart..<current
- )
- } else {
- advance()
- return Token(
- type: .minus,
- value: "-",
- position: pos,
- stringRange: tokenStart..<current
- )
- }
-
- case "0"..."9", ".":
- let numberToken = scanNumber()
- updateStateForToken(.realNumber)
- return numberToken
-
- default:
- // If we're expecting text, scan as text instead of vertex label
- if case .expectingText = state {
- state = .normal // Reset after scanning text
- return scanText()
- }
-
- // If we encounter an unexpected character, try to scan it as a vertex label
- // This provides more graceful degradation than failing immediately
- return scanVertexLabel()
- }
- }
-
- private func updateStateForToken(_ tokenType: TokenType) {
- switch state {
- case .normal:
- // No state changes needed for normal parsing
- break
-
- case .afterNoteKeyword:
- if tokenType == .leftParen {
- state = .inNotePosition(tokensNeeded: 1) // Expecting: number, comma, number, rightParen
- } else {
- state = .normal // Invalid sequence, reset
- }
-
- case .inNotePosition(let tokensNeeded):
- switch tokensNeeded {
- case 1: // Expecting first number
- if tokenType == .realNumber {
- state = .inNotePosition(tokensNeeded: 2)
- } else {
- state = .normal
- }
- case 2: // Expecting comma
- if tokenType == .comma {
- state = .inNotePosition(tokensNeeded: 3)
- } else {
- state = .normal
- }
- case 3: // Expecting second number
- if tokenType == .realNumber {
- state = .inNotePosition(tokensNeeded: 4)
- } else {
- state = .normal
- }
- case 4: // Expecting right paren
- if tokenType == .rightParen {
- state = .expectingText // Complete position sequence, now expect text
- } else {
- state = .normal
- }
- default:
- state = .normal
- }
-
- case .expectingText:
- // Text has been consumed, reset to normal
- state = .normal
- }
- }
-
- func scanText() -> Token {
- let pos = currentPosition()
-
- // Skip any leading whitespace
- skipWhitespace()
-
- let tokenStart = current
-
- while current < input.endIndex && !isLineEnding(input[current]) {
- advance()
- }
-
- let originalValue = String(input[tokenStart..<current]).trimmingCharacters(in: .whitespaces)
- return Token(
- type: .text,
- value: originalValue,
- position: pos,
- stringRange: tokenStart..<current
- )
- }
-
- private func scanLineEnding() -> Token {
- let pos = currentPosition()
- let tokenStart = current
-
- if input[current] == "\r" {
- advance()
- // Check for CRLF
- if current < input.endIndex && input[current] == "\n" {
- advance()
- let token = Token(
- type: .newline,
- value: "\r\n",
- position: pos,
- stringRange: tokenStart..<current
- )
- line += 1
- column = 1
- return token
- } else {
- // Just CR
- let token = Token(
- type: .newline,
- value: "\r",
- position: pos,
- stringRange: tokenStart..<current
- )
- line += 1
- column = 1
- return token
- }
- } else if input[current] == "\n" {
- advance()
- let token = Token(
- type: .newline,
- value: "\n",
- position: pos,
- stringRange: tokenStart..<current
- )
- line += 1
- column = 1
- return token
- } else {
- fatalError("scanLineEnding called on non-line-ending character")
- }
- }
-
- private func isLineEnding(_ char: Character) -> Bool {
- return char == "\n" || char == "\r"
- }
-
- private func scanBracketedToken() -> Token {
- let pos = currentPosition()
- let tokenStart = current
- advance() // consume '['
-
- let contentStart = current
- while current < input.endIndex && input[current] != "]" && !isLineEnding(input[current]) {
- advance()
- }
-
- // If we hit a line ending or EOF before finding the closing bracket, it's an error
- guard current < input.endIndex && input[current] == "]" else {
- // Return error token for unclosed bracket (don't consume past line ending)
- return Token(
- type: .error,
- value: String(input[tokenStart..<current]),
- position: pos,
- stringRange: tokenStart..<current
- )
- }
-
- let originalContent = String(input[contentStart..<current])
- let lowercaseContent = originalContent.lowercased()
- advance() // consume ']'
-
- let tokenEnd = current
-
- switch lowercaseContent {
- case "note":
- state = .afterNoteKeyword // Set state to track note parsing sequence
- return Token(
- type: .noteKeyword,
- value: "[Note]",
- position: pos,
- stringRange: tokenStart..<tokenEnd
- )
- case "group":
- return Token(
- type: .groupKeyword,
- value: "[Group]",
- position: pos,
- stringRange: tokenStart..<tokenEnd
- )
- case "inertia":
- return Token(
- type: .inertiaKeyword,
- value: "[Inertia]",
- position: pos,
- stringRange: tokenStart..<tokenEnd
- )
- case "evolution":
- return Token(
- type: .evolutionKeyword,
- value: "[Evolution]",
- position: pos,
- stringRange: tokenStart..<tokenEnd
- )
- case "x", "square", "triangle", "circle":
- return Token(
- type: .shapeLabel,
- value: originalContent, // Use original case, not lowercase
- position: pos,
- stringRange: tokenStart..<tokenEnd
- )
- default:
- if lowercaseContent.allSatisfy({ $0 == "i" }) && lowercaseContent.count <= 4 {
- return Token(
- type: .stageNumber,
- value: lowercaseContent,
- position: pos,
- stringRange: tokenStart..<tokenEnd
- )
- }
- // Return error token for invalid bracketed content
- return Token(
- type: .error,
- value: String(input[tokenStart..<tokenEnd]),
- position: pos,
- stringRange: tokenStart..<tokenEnd
- )
- }
- }
-
- private func scanNumber() -> Token {
- let pos = currentPosition()
- let tokenStart = current
-
- // Handle leading decimal point
- if input[current] == "." {
- advance()
- }
-
- while current < input.endIndex && input[current].isNumber {
- advance()
- }
-
- // Handle decimal point if we haven't seen one
- if current < input.endIndex && input[current] == "." && input[tokenStart] != "." {
- advance()
- while current < input.endIndex && input[current].isNumber {
- advance()
- }
- }
-
- let value = String(input[tokenStart..<current])
- return Token(
- type: .realNumber,
- value: value,
- position: pos,
- stringRange: tokenStart..<current
- )
- }
-
- private func scanStageOrLabel() -> Token {
- let pos = currentPosition()
- let tokenStart = current
-
- // Check if it's a stage number (i, ii, iii, iv)
- var tempIndex = current
- var iCount = 0
-
- while tempIndex < input.endIndex && input[tempIndex].lowercased() == "i" && iCount < 4 {
- tempIndex = input.index(after: tempIndex)
- iCount += 1
- }
-
- // If we have 1-4 i's and next char is whitespace/delimiter, it's a stage
- if iCount > 0 && iCount <= 4 && (tempIndex >= input.endIndex || isDelimiter(input[tempIndex]))
- {
- current = tempIndex
- return Token(
- type: .stageNumber,
- value: String(repeating: "i", count: iCount),
- position: pos,
- stringRange: tokenStart..<current
- )
- }
-
- // Otherwise, scan as vertex label
- return scanVertexLabel()
- }
-
- private func scanVertexLabel() -> Token {
- let pos = currentPosition()
- let tokenStart = current
-
- while current < input.endIndex && !isVertexLabelDelimiter(input[current]) {
- advance()
- }
-
- let originalValue = String(input[tokenStart..<current]).trimmingCharacters(in: .whitespaces)
- return Token(
- type: .vertexLabel,
- value: originalValue,
- position: pos,
- stringRange: tokenStart..<current
- )
- }
-
- private func isVertexLabelDelimiter(_ char: Character) -> Bool {
- return char == "-" || char == "+" || char == "," || char == "[" || char == "]" || char == "("
- || char == ")" || isLineEnding(char)
- }
-
- private func isDelimiter(_ char: Character) -> Bool {
- return char.isWhitespace || isLineEnding(char) || char == "(" || char == ")" || char == "["
- || char == "]" || char == ","
- }
-
- private func skipWhitespace() {
- while current < input.endIndex && input[current].isWhitespace && !isLineEnding(input[current])
- {
- advance()
- }
- }
-
- private func advance() {
- if current < input.endIndex {
- current = input.index(after: current)
- column += 1
- }
- }
-
- private func peek() -> Character? {
- let next = input.index(after: current)
- return next < input.endIndex ? input[next] : nil
- }
-
- private func currentPosition() -> SourcePosition {
- return SourcePosition(line: line, column: column, stringIndex: current)
- }
- }
-}
diff --git a/Map/Business/WmapParser/Parser.swift b/Map/Business/WmapParser/Parser.swift
deleted file mode 100644
index 7c87150..0000000
--- a/Map/Business/WmapParser/Parser.swift
+++ /dev/null
@@ -1,504 +0,0 @@
-// 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 Foundation
-
-extension Wmap {
-
- struct ParsedMap {
- let entities: [Entity]
- let vertexLabels: Set<String>
- }
-
- class Parser {
- private let lexer: Lexer
- private var currentToken: Token
- private var vertexLabelMap: [String: String] = [:]
-
- init(lexer: Lexer) {
- self.lexer = lexer
- self.currentToken = lexer.nextToken()
- }
-
- func parse() -> ParsedMap {
- var entities: [Entity] = []
- var vertexLabels: Set<String> = Set()
-
- while currentToken.type != .eof {
- if currentToken.type == .newline {
- advance()
- continue
- }
-
- do {
- let entity = try parseEntity()
- entities.append(entity)
- switch entity {
- case .vertex(let vertex):
- vertexLabels.insert(vertex.label.lowercased())
- default:
- break
- }
-
- if currentToken.type == .newline {
- advance()
- } else if currentToken.type != .eof {
- throw ParseError.expectedNewline(actual: currentToken)
- }
- } catch {
- skipToNextLine()
- }
- }
-
- return ParsedMap(entities: entities, vertexLabels: vertexLabels)
- }
-
- private func parseEntity() throws -> Entity {
- switch currentToken.type {
- case .vertexLabel:
- return try parseVertexOrEdge()
- case .noteKeyword:
- return .note(try parseNote())
- case .stageNumber:
- return .stage(try parseStage())
- case .groupKeyword:
- return .group(try parseGroup())
- case .inertiaKeyword:
- return .inertia(try parseInertia())
- case .evolutionKeyword:
- return .evolution(try parseEvolution())
- case .error:
- // Error tokens should cause the line to be ignored
- throw ParseError.invalidLine
- default:
- throw ParseError.invalidLine
- }
- }
-
- private func parseVertexOrEdge() throws -> Entity {
- let startPos = currentToken.position
- let firstLabel = currentToken.value
- let firstLabelRange = makeSourceRange(from: currentToken)
-
- vertexLabelMap[firstLabel.lowercased()] = firstLabel
- advance()
-
- if currentToken.type == .edgeUndirected || currentToken.type == .edgeDirected {
- let isDirected = currentToken.type == .edgeDirected
- let edgeTypeRange = makeSourceRange(from: currentToken)
- advance()
-
- guard currentToken.type == .vertexLabel else {
- throw ParseError.expectedVertexLabel(actual: currentToken)
- }
-
- let secondLabel = currentToken.value
- let secondLabelRange = makeSourceRange(from: currentToken)
- let endPos = currentToken.position
-
- vertexLabelMap[secondLabel.lowercased()] = secondLabel
- advance()
-
- return .edge(
- Edge(
- from: firstLabel,
- to: secondLabel,
- isDirected: isDirected,
- position: startPos,
- range: SourceRange(
- start: startPos, end: endPos,
- stringRange: firstLabelRange.stringRange
- .lowerBound..<secondLabelRange.stringRange.upperBound),
- fromLabelRange: firstLabelRange,
- edgeTypeRange: edgeTypeRange,
- toLabelRange: secondLabelRange
- ))
- } else {
- // Parse vertex
- let (coordinates, coordRanges) = try parsePositionWithRanges()
- let (shape, shapeRange) = try parseOptionalShapeWithRange()
-
- let endPos = shapeRange?.end ?? coordRanges.rightParen.end
-
- return .vertex(
- Vertex(
- label: firstLabel,
- position: startPos,
- coordinates: coordinates,
- shape: shape,
- range: SourceRange(
- start: startPos, end: endPos,
- stringRange: firstLabelRange.stringRange
- .lowerBound..<(shapeRange?.stringRange.upperBound
- ?? coordRanges.rightParen.stringRange.upperBound)),
- labelRange: firstLabelRange,
- leftParenRange: coordRanges.leftParen,
- xCoordRange: coordRanges.xCoord,
- commaRange: coordRanges.comma,
- yCoordRange: coordRanges.yCoord,
- rightParenRange: coordRanges.rightParen,
- shapeRange: shapeRange
- ))
- }
- }
-
- private func parseNote() throws -> Note {
- let startPos = currentToken.position
- let keywordRange = makeSourceRange(from: currentToken)
- advance()
-
- let (coordinates, coordRanges) = try parsePositionWithRanges()
-
- guard currentToken.type == .text else {
- throw ParseError.expectedText(actual: currentToken)
- }
- let text = currentToken.value
- let textRange = makeSourceRange(from: currentToken)
- let endPos = currentToken.position
- advance()
-
- return Note(
- coordinates: coordinates,
- text: text,
- position: startPos,
- range: SourceRange(
- start: startPos, end: endPos,
- stringRange: keywordRange.stringRange.lowerBound..<textRange.stringRange.upperBound),
- keywordRange: keywordRange,
- leftParenRange: coordRanges.leftParen,
- xCoordRange: coordRanges.xCoord,
- commaRange: coordRanges.comma,
- yCoordRange: coordRanges.yCoord,
- rightParenRange: coordRanges.rightParen,
- textRange: textRange
- )
- }
-
- private func parseStage() throws -> Stage {
- let startPos = currentToken.position
- let stageToken = currentToken
- let stageValue = stageToken.value
-
- // For bracketed stage numbers like [ii], we need to parse the components
- // The lexer gives us the full token, but we need to break it down
- let tokenRange = stageToken.stringRange
-
- // Find the bracket positions within the token
- let leftBracketRange = SourceRange(
- start: stageToken.position,
- end: SourcePosition(
- line: stageToken.position.line,
- column: stageToken.position.column + 1,
- stringIndex: lexer.input.index(after: tokenRange.lowerBound)
- ),
- stringRange: tokenRange.lowerBound..<lexer.input.index(after: tokenRange.lowerBound)
- )
-
- let stageNumberStart = lexer.input.index(after: tokenRange.lowerBound)
- let stageNumberEnd = lexer.input.index(tokenRange.upperBound, offsetBy: -1)
- let stageNumberRange = SourceRange(
- start: SourcePosition(
- line: stageToken.position.line,
- column: stageToken.position.column + 1,
- stringIndex: stageNumberStart
- ),
- end: SourcePosition(
- line: stageToken.position.line,
- column: stageToken.position.column + 1 + stageValue.count,
- stringIndex: stageNumberEnd
- ),
- stringRange: stageNumberStart..<stageNumberEnd
- )
-
- let rightBracketRange = SourceRange(
- start: SourcePosition(
- line: stageToken.position.line,
- column: stageToken.position.column + 1 + stageValue.count,
- stringIndex: stageNumberEnd
- ),
- end: SourcePosition(
- line: stageToken.position.line,
- column: stageToken.position.column + 2 + stageValue.count,
- stringIndex: tokenRange.upperBound
- ),
- stringRange: stageNumberEnd..<tokenRange.upperBound
- )
-
- advance()
-
- guard currentToken.type == .realNumber else {
- throw ParseError.expectedNumber(actual: currentToken)
- }
-
- guard let value = Double(currentToken.value) else {
- throw ParseError.invalidNumber(currentToken.value)
- }
-
- let valueRange = makeSourceRange(from: currentToken)
- let endPos = currentToken.position
- advance()
-
- return Stage(
- number: stageValue,
- value: value,
- position: startPos,
- range: SourceRange(
- start: startPos,
- end: endPos,
- stringRange: leftBracketRange.stringRange.lowerBound..<valueRange.stringRange.upperBound
- ),
- leftBracketRange: leftBracketRange,
- stageNumberRange: stageNumberRange,
- rightBracketRange: rightBracketRange,
- valueRange: valueRange
- )
- }
-
- private func parseGroup() throws -> Group {
- let startPos = currentToken.position
- let keywordRange = makeSourceRange(from: currentToken)
- advance()
-
- var vertices: [String] = []
- var vertexRanges: [SourceRange] = []
- var commaRanges: [SourceRange] = []
-
- guard currentToken.type == .vertexLabel else {
- throw ParseError.expectedVertexLabel(actual: currentToken)
- }
-
- vertices.append(currentToken.value)
- vertexRanges.append(makeSourceRange(from: currentToken))
- advance()
-
- while currentToken.type == .comma {
- commaRanges.append(makeSourceRange(from: currentToken))
- advance()
-
- guard currentToken.type == .vertexLabel else {
- throw ParseError.expectedVertexLabel(actual: currentToken)
- }
-
- vertices.append(currentToken.value)
- vertexRanges.append(makeSourceRange(from: currentToken))
- advance()
- }
-
- let endPos = vertexRanges.last?.end ?? startPos
-
- return Group(
- vertices: vertices,
- position: startPos,
- range: SourceRange(
- start: startPos, end: endPos,
- stringRange: keywordRange.stringRange
- .lowerBound..<vertexRanges.last!.stringRange.upperBound),
- keywordRange: keywordRange,
- vertexRanges: vertexRanges,
- commaRanges: commaRanges
- )
- }
-
- private func parseInertia() throws -> Inertia {
- let startPos = currentToken.position
- let keywordRange = makeSourceRange(from: currentToken)
- advance()
-
- guard currentToken.type == .vertexLabel else {
- throw ParseError.expectedVertexLabel(actual: currentToken)
- }
-
- let vertex = currentToken.value
- let vertexRange = makeSourceRange(from: currentToken)
- let endPos = currentToken.position
- advance()
-
- return Inertia(
- vertex: vertex,
- position: startPos,
- range: SourceRange(
- start: startPos, end: endPos,
- stringRange: keywordRange.stringRange.lowerBound..<vertexRange.stringRange.upperBound),
- keywordRange: keywordRange,
- vertexRange: vertexRange
- )
- }
-
- private func parseEvolution() throws -> Evolution {
- let startPos = currentToken.position
- let keywordRange = makeSourceRange(from: currentToken)
- advance()
-
- guard currentToken.type == .vertexLabel else {
- throw ParseError.expectedVertexLabel(actual: currentToken)
- }
-
- let vertex = currentToken.value
- let vertexRange = makeSourceRange(from: currentToken)
- advance()
-
- let isPositive: Bool
- let signRange: SourceRange
- if currentToken.type == .plus {
- isPositive = true
- signRange = makeSourceRange(from: currentToken)
- advance()
- } else if currentToken.type == .minus {
- isPositive = false
- signRange = makeSourceRange(from: currentToken)
- advance()
- } else {
- throw ParseError.expectedSign(actual: currentToken)
- }
-
- guard currentToken.type == .realNumber else {
- throw ParseError.expectedNumber(actual: currentToken)
- }
-
- guard let value = Double(currentToken.value) else {
- throw ParseError.invalidNumber(currentToken.value)
- }
-
- let valueRange = makeSourceRange(from: currentToken)
- let endPos = currentToken.position
- advance()
-
- return Evolution(
- vertex: vertex,
- isPositive: isPositive,
- value: value,
- position: startPos,
- range: SourceRange(
- start: startPos, end: endPos,
- stringRange: keywordRange.stringRange.lowerBound..<valueRange.stringRange.upperBound),
- keywordRange: keywordRange,
- vertexRange: vertexRange,
- signRange: signRange,
- valueRange: valueRange
- )
- }
-
- // Helper methods
- private func parsePositionWithRanges() throws -> (
- (Double, Double),
- (
- leftParen: SourceRange, xCoord: SourceRange, comma: SourceRange, yCoord: SourceRange,
- rightParen: SourceRange
- )
- ) {
- guard currentToken.type == .leftParen else {
- throw ParseError.expectedLeftParen(actual: currentToken)
- }
- let leftParenRange = makeSourceRange(from: currentToken)
- advance()
-
- guard currentToken.type == .realNumber else {
- throw ParseError.expectedNumber(actual: currentToken)
- }
- guard let x = Double(currentToken.value) else {
- throw ParseError.invalidNumber(currentToken.value)
- }
- let xCoordRange = makeSourceRange(from: currentToken)
- advance()
-
- guard currentToken.type == .comma else {
- throw ParseError.expectedComma(actual: currentToken)
- }
- let commaRange = makeSourceRange(from: currentToken)
- advance()
-
- guard currentToken.type == .realNumber else {
- throw ParseError.expectedNumber(actual: currentToken)
- }
- guard let y = Double(currentToken.value) else {
- throw ParseError.invalidNumber(currentToken.value)
- }
- let yCoordRange = makeSourceRange(from: currentToken)
- advance()
-
- guard currentToken.type == .rightParen else {
- throw ParseError.expectedRightParen(actual: currentToken)
- }
- let rightParenRange = makeSourceRange(from: currentToken)
- advance()
-
- return ((x, y), (leftParenRange, xCoordRange, commaRange, yCoordRange, rightParenRange))
- }
-
- private func parseOptionalShapeWithRange() throws -> (String?, SourceRange?) {
- guard currentToken.type == .shapeLabel else {
- return (nil, nil)
- }
-
- let shape = currentToken.value
- let shapeRange = makeSourceRange(from: currentToken)
- advance()
- return (shape, shapeRange)
- }
-
- private func makeSourceRange(from token: Token) -> SourceRange {
- return SourceRange(
- start: token.position,
- end: SourcePosition(
- line: token.position.line,
- column: token.position.column + token.value.count,
- stringIndex: token.stringRange.upperBound
- ),
- stringRange: token.stringRange
- )
- }
-
- private func advance() {
- currentToken = lexer.nextToken()
- }
-
- private func skipToNextLine() {
- while currentToken.type != .newline && currentToken.type != .eof {
- advance()
- }
- if currentToken.type == .newline {
- advance()
- }
- }
- }
-
- enum ParseError: Error, LocalizedError {
- case unexpectedToken(expected: String, actual: Token)
- case expectedVertexLabel(actual: Token)
- case expectedNumber(actual: Token)
- case expectedText(actual: Token)
- case expectedSign(actual: Token)
- case expectedComma(actual: Token)
- case expectedLeftParen(actual: Token)
- case expectedRightParen(actual: Token)
- case expectedNewline(actual: Token)
- case invalidNumber(String)
- case invalidLine
-
- var errorDescription: String? {
- switch self {
- case .unexpectedToken(let expected, let actual):
- return
- "Expected \(expected), got \(actual.type) '\(actual.value)' at line \(actual.position.line)"
- case .expectedVertexLabel(let actual):
- return "Expected vertex label, got \(actual.type) at line \(actual.position.line)"
- case .invalidNumber(let value):
- return "Invalid number format: \(value)"
- // ... other cases
- default:
- return "Parse error"
- }
- }
- }
-}
diff --git a/Map/Business/WmapParser/SourcePosition.swift b/Map/Business/WmapParser/SourcePosition.swift
deleted file mode 100644
index e83c07a..0000000
--- a/Map/Business/WmapParser/SourcePosition.swift
+++ /dev/null
@@ -1,35 +0,0 @@
-// 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.
-extension Wmap {
- struct SourcePosition: Equatable {
- let line: Int
- let column: Int
- let stringIndex: String.Index
-
- static func == (lhs: SourcePosition, rhs: SourcePosition) -> Bool {
- return lhs.line == rhs.line && lhs.column == rhs.column && lhs.stringIndex == rhs.stringIndex
- }
- }
-
- struct SourceRange: Equatable {
- let start: SourcePosition
- let end: SourcePosition
- let stringRange: Range<String.Index>
-
- static func == (lhs: SourceRange, rhs: SourceRange) -> Bool {
- return lhs.start == rhs.start && lhs.end == rhs.end && lhs.stringRange == rhs.stringRange
- }
- }
-}
diff --git a/Map/Business/WmapParser/Syntax.swift b/Map/Business/WmapParser/Syntax.swift
deleted file mode 100644
index 551ebcd..0000000
--- a/Map/Business/WmapParser/Syntax.swift
+++ /dev/null
@@ -1,32 +0,0 @@
-// 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.
-extension Wmap {
- struct SyntaxElement {
- let type: SyntaxType
- let range: SourceRange
- let value: String
- }
-
- enum SyntaxType {
- case keyword // [Note], [Group], etc.
- case vertexLabel // vertex names
- case number // coordinates, values
- case symbol // ->, --, +, -
- case shape // [square], [circle]
- case text // note text
- case punctuation // (, ), [, ], ,
- case stageNumber // i, ii, iii, iv
- }
-}
diff --git a/Map/Business/WmapParser/Token.swift b/Map/Business/WmapParser/Token.swift
deleted file mode 100644
index 86bab60..0000000
--- a/Map/Business/WmapParser/Token.swift
+++ /dev/null
@@ -1,22 +0,0 @@
-// 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.
-extension Wmap {
- struct Token {
- let type: TokenType
- let value: String
- let position: SourcePosition
- let stringRange: Range<String.Index>
- }
-}
diff --git a/Map/Business/WmapParser/TokenType.swift b/Map/Business/WmapParser/TokenType.swift
deleted file mode 100644
index 2e316bd..0000000
--- a/Map/Business/WmapParser/TokenType.swift
+++ /dev/null
@@ -1,36 +0,0 @@
-// 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.
-extension Wmap {
- enum TokenType: CaseIterable {
- case vertexLabel
- case edgeUndirected // --
- case edgeDirected // ->
- case leftParen, rightParen
- case leftBracket, rightBracket
- case comma
- case plus, minus
- case realNumber
- case stageNumber // i, ii, iii, iv
- case shapeLabel // x, square, triangle, circle
- case noteKeyword // [Note]
- case groupKeyword // [Group]
- case inertiaKeyword // [Inertia]
- case evolutionKeyword // [Evolution]
- case text // free text until newline
- case newline
- case eof
- case error // invalid token that should cause line to be ignored
- }
-}
diff --git a/Map/Business/WmapSyntaxHighlighter/SyntaxHighlighter.swift b/Map/Business/WmapSyntaxHighlighter/SyntaxHighlighter.swift
deleted file mode 100644
index 8b31547..0000000
--- a/Map/Business/WmapSyntaxHighlighter/SyntaxHighlighter.swift
+++ /dev/null
@@ -1,102 +0,0 @@
-// 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 AppKit
-
-extension Wmap {
- class SyntaxHighlighter {
- var cachedSyntaxElements: [SyntaxElement] = []
- private var lineToElementsMap: [Int: [SyntaxElement]] = [:]
- private var cachedVertexLabels: Set<String> = Set()
-
- func updateSyntaxElements(from parsedMap: Wmap.ParsedMap) {
- cachedSyntaxElements = parsedMap.entities.flatMap { $0.syntaxElements }
- cachedVertexLabels = parsedMap.vertexLabels
-
- // Build line-to-elements mapping for fast lookup
- lineToElementsMap.removeAll()
- for element in cachedSyntaxElements {
- let line = element.range.start.line
- lineToElementsMap[line, default: []].append(element)
- }
- }
-
- func applySyntaxHighlighting(textStorage: NSTextStorage, range: NSRange) {
- // Get the line range that encompasses the edited range
- let string = textStorage.string as NSString
- let lineRange = string.lineRange(for: range)
-
- // Apply highlighting to elements that intersect with the line range
- for element in cachedSyntaxElements {
- // Safely convert string range to NSRange with bounds checking
- guard element.range.stringRange.lowerBound <= element.range.stringRange.upperBound,
- element.range.stringRange.upperBound <= textStorage.string.endIndex
- else {
- continue
- }
-
- let nsRange = NSRange(element.range.stringRange, in: textStorage.string)
-
- // Only process elements that intersect with our update area
- guard nsRange.location != NSNotFound,
- nsRange.location + nsRange.length <= textStorage.length,
- NSIntersectionRange(nsRange, lineRange).length > 0
- else {
- continue
- }
-
- let color = colorForSyntaxType(element.type)
- var attributes: [NSAttributedString.Key: Any] = [
- .foregroundColor: color
- ]
-
- if UserDefaults.standard.bool(forKey: "useSmartEditor") {
- if element.type == .vertexLabel
- && !cachedVertexLabels.contains(element.value.lowercased())
- {
- if UserDefaults.standard.bool(forKey: "highlightMissingComponents") {
- attributes[.underlineStyle] =
- NSUnderlineStyle.thick.rawValue | NSUnderlineStyle.patternDot.rawValue
- attributes[.underlineColor] = NSColor.Theme.Syntax.error
- attributes[.init("MissingComponent")] = element.value
- }
- }
- }
-
- textStorage.addAttributes(attributes, range: nsRange)
- }
- }
-
- private func colorForSyntaxType(_ type: SyntaxType) -> NSColor {
- switch type {
- case .keyword:
- return NSColor.Theme.Syntax.option
- case .vertexLabel:
- return NSColor.Theme.Syntax.vertex
- case .number:
- return NSColor.Theme.Syntax.number
- case .symbol:
- return NSColor.Theme.Syntax.symbol
- case .shape:
- return NSColor.Theme.Syntax.option
- case .text:
- return NSColor.Theme.Syntax.text
- case .punctuation:
- return NSColor.Theme.Syntax.punctuation
- case .stageNumber:
- return NSColor.Theme.Syntax.option
- }
- }
- }
-}
diff --git a/Map/Data/MapDocument.swift b/Map/Data/MapDocument.swift
index 3c19ca1..813b81d 100644
--- a/Map/Data/MapDocument.swift
+++ b/Map/Data/MapDocument.swift
@@ -14,6 +14,7 @@
// along with this program. If not, see https://map.tranquil.systems.
import SwiftUI
import UniformTypeIdentifiers
+import WmapParser
struct MapDocument: FileDocument {
var text: String
@@ -40,10 +41,8 @@ struct MapDocument: FileDocument {
static var readableContentTypes: [UTType] { [.wmap] }
- var parsed: Wmap.ParsedMap {
- let lexer = Wmap.Lexer(self.text)
- let parser = Wmap.Parser(lexer: lexer)
- return parser.parse()
+ var parsed: WmapParser.Map {
+ return WmapParser.parse(self.text)
}
init(configuration: ReadConfiguration) throws {
@@ -63,7 +62,7 @@ struct MapDocument: FileDocument {
@MainActor
func exportAsImage(withEvolution selectedEvolution: StageType) -> NSImage? {
let renderView = MapRenderView(
- entities: self.parsed.entities,
+ parsedMap: self.parsed,
evolution: .constant(selectedEvolution))
let renderer = ImageRenderer(content: renderView)
diff --git a/Map/MapApp.swift b/Map/MapApp.swift
index b66d715..5e74049 100644
--- a/Map/MapApp.swift
+++ b/Map/MapApp.swift
@@ -32,7 +32,7 @@ struct MapApp: App {
var body: some Scene {
DocumentGroup(newDocument: MapDocument(text: nil)) { file in
- MapEditor(document: file.$document, url: file.fileURL)
+ MapEditor(document: file.$document)
.focusedSceneValue(\.document, file.$document)
.focusedSceneValue(\.fileURL, file.fileURL)
}
@@ -65,46 +65,3 @@ struct MapApp: App {
}
}
}
-
-struct LazyMapEditor: View {
- @Binding var document: MapDocument
- var url: URL?
-
- @FocusedBinding(\.document) var focusedDocument: MapDocument?
- @FocusedValue(\.fileURL) var focusedFileURL: URL?
- @State private var shouldRender = false
- @State private var renderTask: Task<Void, Never>?
-
- var isActiveDocument: Bool {
- guard let focusedDocument else { return false }
- // Use URL for identification if available, otherwise fall back to text comparison
- if let documentURL = url,
- let focusedURL = focusedFileURL
- {
- return documentURL == focusedURL
- }
- // For new documents without URLs, use text comparison as fallback
- return focusedDocument.text == document.text
- }
-
- var body: some View {
- if isActiveDocument {
- MapEditor(document: $document, url: url)
- } else {
- Rectangle()
- .fill(Color(NSColor.windowBackgroundColor))
- .overlay(
- VStack(spacing: 16) {
- Text("document.inactive")
- .font(.title2)
- .foregroundColor(.secondary)
- if let url = url {
- Text(url.lastPathComponent)
- .font(.caption)
- .foregroundColor(.secondary)
- }
- }
- )
- }
- }
-}
diff --git a/Map/Presentation/Base Components/EvolutionPicker/EvolutionPicker.swift b/Map/Presentation/Base Components/EvolutionPicker/EvolutionPicker.swift
index da67054..83d5009 100644
--- a/Map/Presentation/Base Components/EvolutionPicker/EvolutionPicker.swift
+++ b/Map/Presentation/Base Components/EvolutionPicker/EvolutionPicker.swift
@@ -18,7 +18,6 @@ struct EvolutionPicker: View {
@State var isShowingMenu = false
@State var isHovered = false
- @FocusState var isFocused: Bool
@Binding var selectedEvolution: StageType
diff --git a/Map/Presentation/Base Components/EvolutionPicker/EvolutionPickerMenuHeader.swift b/Map/Presentation/Base Components/EvolutionPicker/EvolutionPickerMenuHeader.swift
index b3eeb97..095c74c 100644
--- a/Map/Presentation/Base Components/EvolutionPicker/EvolutionPickerMenuHeader.swift
+++ b/Map/Presentation/Base Components/EvolutionPicker/EvolutionPickerMenuHeader.swift
@@ -8,6 +8,7 @@ struct EvolutionPickerMenuHeader: View {
HStack(alignment: .center, spacing: Dimensions.Spacing.regular) {
Text(key)
.font(.Theme.Caption.emphasized)
+ .lineSpacing(Dimensions.LineHeight.caption - Dimensions.FontSize.caption)
.padding(.vertical, Dimensions.Spacing.cozy)
.textCase(.uppercase)
.padding(.leading, Dimensions.Spacing.regular)
diff --git a/Map/Presentation/Base Components/EvolutionPicker/GlassEvolutionPicker.swift b/Map/Presentation/Base Components/EvolutionPicker/GlassEvolutionPicker.swift
index 3b6f199..72a572a 100644
--- a/Map/Presentation/Base Components/EvolutionPicker/GlassEvolutionPicker.swift
+++ b/Map/Presentation/Base Components/EvolutionPicker/GlassEvolutionPicker.swift
@@ -18,7 +18,6 @@ import SwiftUI
struct GlassEvolutionPicker: View {
@State var isShowingMenu = false
- @FocusState var isFocused: Bool
@Binding var selectedEvolution: StageType
diff --git a/Map/Presentation/Base Components/MapRender/MapAxes.swift b/Map/Presentation/Base Components/MapRender/MapAxes.swift
index 9d0f43f..7c2b74b 100644
--- a/Map/Presentation/Base Components/MapRender/MapAxes.swift
+++ b/Map/Presentation/Base Components/MapRender/MapAxes.swift
@@ -19,7 +19,7 @@ struct MapAxes: View {
let mapSize: CGSize
let lineWidth: CGFloat
let evolution: Stage
- let stages: [CGFloat]
+ let stages: [Double]
let stageHeight = CGFloat(100.0)
let padding = CGFloat(5.0)
diff --git a/Map/Presentation/Base Components/MapRender/MapBackground.swift b/Map/Presentation/Base Components/MapRender/MapBackground.swift
index b33e791..710d549 100644
--- a/Map/Presentation/Base Components/MapRender/MapBackground.swift
+++ b/Map/Presentation/Base Components/MapRender/MapBackground.swift
@@ -14,14 +14,13 @@
// along with this program. If not, see https://map.tranquil.systems.
import Patterns
import SwiftUI
+import WmapParser
struct MapBackground: View {
let mapSize: CGSize
- let lineWidth: CGFloat
- let stages: [CGFloat]
- let opacity = 0.1
-
+ let stages: [Double]
+
var body: some View {
ZStack(alignment: .topLeading) {
PatternView(
@@ -57,6 +56,6 @@ struct MapBackground: View {
#Preview {
MapBackground(
- mapSize: CGSize(width: 200.0, height: 200.0), lineWidth: CGFloat(0.5),
- stages: [25.0, 50.0, 75.0])
+ mapSize: CGSize(width: 200.0, height: 200.0),
+ stages: [])
}
diff --git a/Map/Presentation/Base Components/MapRender/MapEdges.swift b/Map/Presentation/Base Components/MapRender/MapEdges.swift
index 3c6ef82..5819069 100644
--- a/Map/Presentation/Base Components/MapRender/MapEdges.swift
+++ b/Map/Presentation/Base Components/MapRender/MapEdges.swift
@@ -13,6 +13,7 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see https://map.tranquil.systems.
import SwiftUI
+import WmapParser
struct MapEdges: View {
diff --git a/Map/Presentation/Base Components/MapRender/MapGroup.swift b/Map/Presentation/Base Components/MapRender/MapGroup.swift
index a5877a8..ca5e954 100644
--- a/Map/Presentation/Base Components/MapRender/MapGroup.swift
+++ b/Map/Presentation/Base Components/MapRender/MapGroup.swift
@@ -22,7 +22,6 @@ struct MapGroup: View {
let group: [Vertex]
let color: Color
- let cornerSize = CGSize(width: 2.0, height: 2.0)
var strokeSize: CGFloat { 1.75 * vertexSize.width }
var hull: [CGPoint] {
@@ -92,6 +91,6 @@ struct MapGroup: View {
Vertex(id: 0, label: "A Circle", position: CGPoint(x: 50.0, y: 50.0)),
Vertex(id: 1, label: "A Square", position: CGPoint(x: 10.0, y: 20.0), shape: .square),
Vertex(id: 2, label: "A triangle", position: CGPoint(x: 25, y: 32.0), shape: .triangle),
- Vertex(id: 3, label: "An X", position: CGPoint(x: 70.0, y: 70.0), shape: .x),
+ Vertex(id: 3, label: "An X", position: CGPoint(x: 70.0, y: 70.0), shape: .xMark),
], color: .red)
}
diff --git a/Map/Presentation/Base Components/MapRender/MapGroups.swift b/Map/Presentation/Base Components/MapRender/MapGroups.swift
index 1983dcd..149ea65 100644
--- a/Map/Presentation/Base Components/MapRender/MapGroups.swift
+++ b/Map/Presentation/Base Components/MapRender/MapGroups.swift
@@ -40,7 +40,7 @@ struct MapGroups: View {
Vertex(id: 0, label: "A Circle", position: CGPoint(x: 50.0, y: 50.0)),
Vertex(id: 1, label: "A Square", position: CGPoint(x: 10.0, y: 20.0), shape: .square),
Vertex(id: 2, label: "A triangle", position: CGPoint(x: 25, y: 32.0), shape: .triangle),
- Vertex(id: 3, label: "An X", position: CGPoint(x: 70.0, y: 70.0), shape: .x),
+ Vertex(id: 3, label: "An X", position: CGPoint(x: 70.0, y: 70.0), shape: .xMark),
]
])
}
diff --git a/Map/Presentation/Base Components/MapRender/MapMask.swift b/Map/Presentation/Base Components/MapRender/MapMask.swift
index b477c0a..c4ba055 100644
--- a/Map/Presentation/Base Components/MapRender/MapMask.swift
+++ b/Map/Presentation/Base Components/MapRender/MapMask.swift
@@ -65,7 +65,7 @@ struct MapMask: View {
Vertex(id: 0, label: "A Circle", position: CGPoint(x: 50.0, y: 50.0)),
Vertex(id: 1, label: "A Square", position: CGPoint(x: 10.0, y: 20.0), shape: .square),
Vertex(id: 2, label: "A triangle", position: CGPoint(x: 25, y: 32.0), shape: .triangle),
- Vertex(id: 3, label: "An X", position: CGPoint(x: 70.0, y: 70.0), shape: .x),
+ Vertex(id: 3, label: "An X", position: CGPoint(x: 70.0, y: 70.0), shape: .xMark),
],
labelPositions: [:])
}
diff --git a/Map/Presentation/Base Components/MapRender/MapStages.swift b/Map/Presentation/Base Components/MapRender/MapStages.swift
index 7d7e51e..bcd95a2 100644
--- a/Map/Presentation/Base Components/MapRender/MapStages.swift
+++ b/Map/Presentation/Base Components/MapRender/MapStages.swift
@@ -19,8 +19,7 @@ struct MapStages: View {
let mapSize: CGSize
let lineWidth: CGFloat
- let stages: [CGFloat]
- let opacity = 0.1
+ let stages: [Double]
var body: some View {
ZStack(alignment: .topLeading) {
diff --git a/Map/Presentation/Base Components/MapRender/MapVertices.swift b/Map/Presentation/Base Components/MapRender/MapVertices.swift
index 621c2f9..a3558fa 100644
--- a/Map/Presentation/Base Components/MapRender/MapVertices.swift
+++ b/Map/Presentation/Base Components/MapRender/MapVertices.swift
@@ -132,7 +132,7 @@ struct MapVertices: View {
to: CGPoint(x: w(vertex.position.x), y: h(vertex.position.y) + vertexSize.height))
path.closeSubpath()
}
- case .x:
+ case .xMark:
return Path { path in
path.move(to: CGPoint(x: w(vertex.position.x), y: h(vertex.position.y)))
path.addLine(
@@ -156,7 +156,7 @@ struct MapVertices: View {
Vertex(id: 0, label: "A Circle", position: CGPoint(x: 50.0, y: 50.0)),
Vertex(id: 1, label: "A Square", position: CGPoint(x: 10.0, y: 20.0), shape: .square),
Vertex(id: 2, label: "A triangle", position: CGPoint(x: 25, y: 32.0), shape: .triangle),
- Vertex(id: 3, label: "An X", position: CGPoint(x: 70.0, y: 70.0), shape: .x),
+ Vertex(id: 3, label: "An X", position: CGPoint(x: 70.0, y: 70.0), shape: .xMark),
],
labelPositions: [:])
}
diff --git a/Map/Presentation/Base Components/MapTextEditor/MapTextEditor.swift b/Map/Presentation/Base Components/MapTextEditor/MapTextEditor.swift
index a1b357d..99fa684 100644
--- a/Map/Presentation/Base Components/MapTextEditor/MapTextEditor.swift
+++ b/Map/Presentation/Base Components/MapTextEditor/MapTextEditor.swift
@@ -14,17 +14,17 @@
// along with this program. If not, see https://map.tranquil.systems.
import Cocoa
import SwiftUI
+import WmapParser
+import SwiftTreeSitter
+import SwiftTreeSitterLayer
+import TreeSitterWmap
+
class MapTextEditorController: NSViewController {
@Binding var text: String
- var parsedMap: Wmap.ParsedMap {
- didSet {
- syntaxHighlighter.updateSyntaxElements(from: parsedMap)
- // Don't re-highlight entire document here - let the text change handler do it
- }
- }
+ var parsedMap: WmapParser.Map?
var highlightRanges: [Range<String.Index>] {
didSet {
@@ -41,17 +41,18 @@ class MapTextEditorController: NSViewController {
let onChange: () -> Void
- private let syntaxHighlighter = Wmap.SyntaxHighlighter()
private let changeDebouncer: Debouncer = Debouncer(seconds: 1)
private var trackingArea: NSTrackingArea?
private var hoverView: NSView?
+ private var rootLayer: LanguageLayer?
+
var hoverTimer: Timer?
var isHoveringValidationText = false
var isHoveringHoverView = false
init(
text: Binding<String>,
- parsedMap: Wmap.ParsedMap = Wmap.ParsedMap(entities: [], vertexLabels: Set()),
+ parsedMap: WmapParser.Map? = nil,
highlightRanges: [Range<String.Index>],
selectedRange: Int,
onChange: @escaping () -> Void
@@ -62,9 +63,16 @@ class MapTextEditorController: NSViewController {
self.highlightRanges = highlightRanges
self.selectedRange = selectedRange
super.init(nibName: nil, bundle: nil)
+
+ if let wmapConfiguration = try? LanguageConfiguration(tree_sitter_wmap(), name: "wmap") {
+ let languageConfiguration = LanguageLayer.Configuration(languageProvider: { name in
+ switch name {
+ case "wmap": return wmapConfiguration
+ default: return nil
+ }
+ })
+ self.rootLayer = try? LanguageLayer(languageConfig: wmapConfiguration, configuration: languageConfiguration)
- Task {
- syntaxHighlighter.updateSyntaxElements(from: parsedMap)
}
}
@@ -75,7 +83,7 @@ class MapTextEditorController: NSViewController {
override func loadView() {
let scrollView = NSTextView.scrollableTextView()
let textView = scrollView.documentView as! NSTextView
-
+
scrollView.translatesAutoresizingMaskIntoConstraints = false
textView.backgroundColor = .Theme.UI.background
@@ -87,7 +95,7 @@ class MapTextEditorController: NSViewController {
textView.font = NSFont.Theme.Editor.regular
textView.textColor = NSColor.Theme.Syntax.text
textView.textContainerInset = NSSize(
- width: Dimensions.Spacing.coziest, height: Dimensions.Spacing.coziest)
+ width: Dimensions.Spacing.regular, height: Dimensions.Spacing.regular)
// Configure text wrapping based on user preference
let softWrapLines = UserDefaults.standard.bool(forKey: "softWrapLines")
@@ -114,6 +122,7 @@ class MapTextEditorController: NSViewController {
textView.isVerticallyResizable = true
setupTrackingArea(for: textView)
+ rootLayer?.replaceContent(with: text)
self.view = scrollView
}
@@ -241,10 +250,10 @@ class MapTextEditorController: NSViewController {
var effectiveRange = NSRange()
let attributes = textStorage.attributes(at: characterIndex, effectiveRange: &effectiveRange)
- if let missinVertex = attributes[.init(rawValue: "MissingComponent")] as? String {
+ if let missingVertex = attributes[.init(rawValue: "MissingComponent")] as? String {
isHoveringValidationText = true
hoverTimer?.invalidate()
- showHoverView(for: effectiveRange, with: missinVertex, characterIndex: characterIndex)
+ showHoverView(for: effectiveRange, with: missingVertex, characterIndex: characterIndex)
} else {
isHoveringValidationText = false
scheduleHideHoverView()
@@ -346,17 +355,39 @@ class MapTextEditorController: NSViewController {
}
}
- private func getSyntaxElementAt(characterIndex: Int) -> Wmap.SyntaxElement? {
- let textStorage = currentTextView?.textStorage
- guard let textStorage = textStorage else { return nil }
+ private func applyTreeSitterHighlighting(textStorage: NSTextStorage, range: NSRange) {
+ guard let highlights = try? rootLayer?.highlights(in: range, provider: text.predicateTextProvider)
+ else { return }
+
+ for highlight in highlights {
+ let color = NSColor.Theme.Syntax.colorForSyntax(highlight.name)
+ var attributes: [NSAttributedString.Key: Any] = [
+ .foregroundColor: color
+ ]
- for element in syntaxHighlighter.cachedSyntaxElements {
- let nsRange = NSRange(element.range.stringRange, in: textStorage.string)
- if nsRange.contains(characterIndex) {
- return element
+ // Check for missing components
+ if UserDefaults.standard.bool(forKey: "useSmartEditor"),
+ let parsedMap,
+ highlight.name == "variable.component_label" {
+
+ // Extract the component label text from the range
+ let componentText = (textStorage.string as NSString).substring(with: highlight.range).trimmingCharacters(in: .whitespacesAndNewlines)
+
+ // Check if this component exists in the parsed map
+ let componentExists = parsedMap.components.contains(where: {
+ $0.label.lowercased() == componentText.lowercased()
+ })
+
+ if !componentExists && UserDefaults.standard.bool(forKey: "highlightMissingComponents") {
+ attributes[.underlineStyle] =
+ NSUnderlineStyle.thick.rawValue | NSUnderlineStyle.patternDot.rawValue
+ attributes[.underlineColor] = NSColor.Theme.Syntax.error
+ attributes[.init("MissingComponent")] = componentText
+ }
}
+
+ textStorage.addAttributes(attributes, range: highlight.range)
}
- return nil
}
}
@@ -400,6 +431,13 @@ extension MapTextEditorController: NSTextStorageDelegate {
}
let editedRange = textStorage.editedRange
+ let content = textStorage.string
+
+ // Update tree-sitter layer with new content
+ Task { @MainActor in
+ rootLayer?.replaceContent(with: content)
+ }
+
if editedRange.location != NSNotFound {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { [weak self] in
guard let self = self,
@@ -432,21 +470,22 @@ extension MapTextEditorController: NSTextStorageDelegate {
// Set default text color for the range
textStorage.addAttribute(.foregroundColor, value: NSColor.Theme.Syntax.text, range: range)
- syntaxHighlighter.applySyntaxHighlighting(textStorage: textStorage, range: range)
+ // Apply tree-sitter syntax highlighting
+ applyTreeSitterHighlighting(textStorage: textStorage, range: range)
}
}
struct MapTextEditor: NSViewControllerRepresentable {
@Binding var text: String
- var parsedMap: Wmap.ParsedMap
+ var parsedMap: WmapParser.Map?
var highlightRanges: [Range<String.Index>]
var selectedRange: Int
var onChange: () -> Void = {}
init(
text: Binding<String>,
- parsedMap: Wmap.ParsedMap = Wmap.ParsedMap(entities: [], vertexLabels: Set()),
+ parsedMap: WmapParser.Map? = nil,
highlightRanges: [Range<String.Index>] = [], selectedRange: Int = 0,
onChange: @escaping () -> Void = {}
) {
diff --git a/Map/Presentation/Base Components/SearchBar.swift b/Map/Presentation/Base Components/SearchBar.swift
index cf19ddd..706b66f 100644
--- a/Map/Presentation/Base Components/SearchBar.swift
+++ b/Map/Presentation/Base Components/SearchBar.swift
@@ -20,7 +20,6 @@ struct SearchBar: View {
@FocusState var isSearchFocused: Bool
var onNext: () -> Void = {}
var onPrevious: () -> Void = {}
- var onSubmit: () -> Void = {}
var onDismiss: () -> Void = {}
var body: some View {
diff --git a/Map/Presentation/Commands/MapCommands.swift b/Map/Presentation/Commands/MapCommands.swift
index fe21f57..b992a42 100644
--- a/Map/Presentation/Commands/MapCommands.swift
+++ b/Map/Presentation/Commands/MapCommands.swift
@@ -34,10 +34,6 @@ struct MapCommands: Commands {
return []
}
- private var defaultTemplate: Template? {
- templates.first { $0.isDefault }
- }
-
var body: some Commands {
// File
diff --git a/Map/Presentation/Complex Components/MapRender/MapRenderView.swift b/Map/Presentation/Complex Components/MapRender/MapRenderView.swift
index ce95813..ba525ad 100644
--- a/Map/Presentation/Complex Components/MapRender/MapRenderView.swift
+++ b/Map/Presentation/Complex Components/MapRender/MapRenderView.swift
@@ -16,13 +16,13 @@ import Combine
import CoreData
import CoreGraphics
import SwiftUI
+import WmapParser
struct MapRenderView: View {
- var entities: [Wmap.Entity]
+ var parsedMap: WmapParser.Map
@Binding var evolution: StageType
- @State private var lastEntityHash: Int = 0
@AppStorage("showMapBackground") var showMapBackground: Bool = UserPreferences.Defaults
.showMapBackground
@AppStorage("useCustomFont") var useCustomFont: Bool = UserPreferences.Defaults.useCustomFont
@@ -34,7 +34,7 @@ struct MapRenderView: View {
Stage.stages(evolution)
}
- @State private var parsedMap: RenderableMap = RenderableMap.empty
+ @State private var renderableMap: RenderableMap = .empty
@State private var smartLabelPositions: [Int: CGPoint] = [:]
@State private var parseTask: Task<Void, Never>?
@@ -49,11 +49,10 @@ struct MapRenderView: View {
private func convertEntitiesInBackground() {
parseTask?.cancel()
- let currentEntities = entities
parseTask = Task {
let parsed = await Task.detached(priority: .userInitiated) {
- RenderableMap.from(currentEntities)
+ RenderableMap.from(parsedMap)
}.value
let positions = await Task.detached(priority: .userInitiated) {
@@ -87,7 +86,7 @@ struct MapRenderView: View {
if !Task.isCancelled {
await MainActor.run {
- parsedMap = parsed
+ renderableMap = parsed
smartLabelPositions = positions
}
}
@@ -107,37 +106,37 @@ struct MapRenderView: View {
// The order Matters. Think of this as layers.
if showMapBackground {
- MapBackground(mapSize: mapSize, lineWidth: lineWidth, stages: parsedMap.stages)
+ MapBackground(mapSize: mapSize, stages: renderableMap.stages)
}
MapAxes(
- mapSize: mapSize, lineWidth: lineWidth, evolution: stage, stages: parsedMap.stages)
+ mapSize: mapSize, lineWidth: lineWidth, evolution: stage, stages: renderableMap.stages)
Group {
- MapStages(mapSize: mapSize, lineWidth: lineWidth, stages: parsedMap.stages)
+ MapStages(mapSize: mapSize, lineWidth: lineWidth, stages: renderableMap.stages)
MapEdges(
- mapSize: mapSize, lineWidth: lineWidth, vertexSize: vertexSize, edges: parsedMap.edges)
+ mapSize: mapSize, lineWidth: lineWidth, vertexSize: vertexSize, edges: renderableMap.edges)
MapOpportunities(
mapSize: mapSize, lineWidth: lineWidth, vertexSize: vertexSize,
- opportunities: parsedMap.opportunities)
- MapIntertias(mapSize: mapSize, vertexSize: vertexSize, inertias: parsedMap.inertias)
+ opportunities: renderableMap.opportunities)
+ MapIntertias(mapSize: mapSize, vertexSize: vertexSize, inertias: renderableMap.inertias)
}.mask {
MapMask(
- mapSize: mapSize, vertexSize: vertexSize, vertices: parsedMap.vertices,
+ mapSize: mapSize, vertexSize: vertexSize, vertices: renderableMap.vertices,
labelPositions: smartLabelPositions)
}
MapVertices(
- mapSize: mapSize, vertexSize: vertexSize, vertices: parsedMap.vertices,
+ mapSize: mapSize, vertexSize: vertexSize, vertices: renderableMap.vertices,
labelPositions: smartLabelPositions, onDragVertex: onDragVertex)
if #available(macOS 26.0, *) {
- MapGroups(mapSize: mapSize, vertexSize: vertexSize, groups: parsedMap.groups)
+ MapGroups(mapSize: mapSize, vertexSize: vertexSize, groups: renderableMap.groups)
.drawingGroup()
.opacity(0.1)
} else {
- MapGroups(mapSize: mapSize, vertexSize: vertexSize, groups: parsedMap.groups).drawingGroup(
+ MapGroups(mapSize: mapSize, vertexSize: vertexSize, groups: renderableMap.groups).drawingGroup(
opaque: true
).opacity(0.1)
}
MapNotes(
- mapSize: mapSize, lineWidth: lineWidth, notes: parsedMap.notes)
+ mapSize: mapSize, lineWidth: lineWidth, notes: renderableMap.notes)
}
.id("\(useCustomFont)-\(customFontName)")
.offset(x: padding, y: padding).frame(
@@ -147,7 +146,7 @@ struct MapRenderView: View {
.onAppear {
convertEntitiesInBackground()
}
- .onChange(of: entities) { _, _ in
+ .onChange(of: parsedMap) { _, _ in
convertEntitiesInBackground()
}
.onChange(of: useSmartLabelPositioning) {
@@ -157,19 +156,11 @@ struct MapRenderView: View {
parseTask?.cancel()
}
}
-
- func h(_ dimension: CGFloat) -> CGFloat {
- max(0.0, min(mapSize.height, dimension * mapSize.height / 100.0))
- }
-
- func w(_ dimension: CGFloat) -> CGFloat {
- max(0.0, min(mapSize.width, dimension * mapSize.width / 100.0))
- }
}
#Preview {
MapRenderView(
- entities: [],
+ parsedMap: WmapParser.parse(""),
evolution: Binding.constant(StageType.general)
)
}
diff --git a/Map/Presentation/MapEditor.swift b/Map/Presentation/MapEditor.swift
index f5589e5..892c803 100644
--- a/Map/Presentation/MapEditor.swift
+++ b/Map/Presentation/MapEditor.swift
@@ -13,13 +13,13 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see https://map.tranquil.systems.
import SwiftUI
+import WmapParser
struct MapEditor: View {
@Binding var document: MapDocument
- var url: URL?
@State var selectedEvolution: StageType = .behavior
@State var isSearching: Bool = false
- var parsedMap: Wmap.ParsedMap {
+ var parsedMap: WmapParser.Map {
document.parsed
}
@@ -89,9 +89,6 @@ struct MapEditor: View {
}
}
},
- onSubmit: {
-
- },
onDismiss: {
isSearching = false
}
@@ -129,7 +126,7 @@ struct MapEditor: View {
GeometryReader { geometry in
ScrollView([.horizontal, .vertical], showsIndicators: false) {
MapRenderView(
- entities: parsedMap.entities, evolution: $selectedEvolution,
+ parsedMap: parsedMap, evolution: $selectedEvolution,
onDragVertex: onDragVertex
)
.scaleEffect(zoom, anchor: .center).frame(
@@ -285,5 +282,5 @@ struct MapEditor: View {
}
#Preview {
- MapEditor(document: .constant(MapDocument(text: nil)), url: URL(filePath: "test.wmap")!)
+ MapEditor(document: .constant(MapDocument(text: nil)))
}
diff --git a/Map/Presentation/PreferencesWindow.swift b/Map/Presentation/PreferencesWindow.swift
index fa5bd4c..3a50119 100644
--- a/Map/Presentation/PreferencesWindow.swift
+++ b/Map/Presentation/PreferencesWindow.swift
@@ -93,7 +93,8 @@ struct PreferencesWindow: View {
selectedTab == tab ? Color.Theme.UI.accent : Color.background.opacity(0.01)
)
.foregroundColor(selectedTab == tab ? Color.white : Color.Theme.UI.foreground)
- .cornerRadius(Dimensions.Preferences.Toolbar.radius)
+ .clipShape(clipShape())
+ .padding(.horizontal, 2.0)
}
.buttonStyle(.plain)
}
@@ -101,8 +102,17 @@ struct PreferencesWindow: View {
}
}
}
+
+ private func clipShape() -> AnyShape {
+ if #available(macOS 26.0, *) {
+ AnyShape(RoundedRectangle(cornerRadius: Dimensions.Preferences.Toolbar.glassRadius))
+ } else {
+ AnyShape(RoundedRectangle(cornerRadius: Dimensions.Preferences.Toolbar.radius))
+ }
+ }
}
+
#Preview {
PreferencesWindow()
}
diff --git a/Map/Presentation/Theme/Color+theme.swift b/Map/Presentation/Theme/Color+theme.swift
index e0addca..dcc84cb 100644
--- a/Map/Presentation/Theme/Color+theme.swift
+++ b/Map/Presentation/Theme/Color+theme.swift
@@ -25,10 +25,7 @@ extension Color {
static let lightPorcelainGreen = Color("Light Porcelain Green")
static let naplesYellow = Color("Naples Yellow")
static let hermosaPink = Color("Hermosa Pink")
- static let neutralGray = Color("Neutral Gray")
static let lightNeutralGray = Color("Light Neutral Gray")
- static let darkNeutralGray = Color("Dark Neutral Gray")
- static let darkerNeutralGray = Color("Darker Neutral Gray")
// MARK: - General
diff --git a/Map/Presentation/Theme/Dimensions.swift b/Map/Presentation/Theme/Dimensions.swift
index 57bd8b8..b7c1faf 100644
--- a/Map/Presentation/Theme/Dimensions.swift
+++ b/Map/Presentation/Theme/Dimensions.swift
@@ -20,7 +20,6 @@ struct Dimensions {
struct FontSize {
static let body: CGFloat = 13
- static let title: CGFloat = 32
static let caption: CGFloat = 11
static let smallControl: CGFloat = 11
@@ -38,19 +37,10 @@ struct Dimensions {
struct LineHeight {
static let body: CGFloat = 18
- static let title: CGFloat = 38
static let caption: CGFloat = 18
- static let smallControl: CGFloat = 18
-
- static var editor: CGFloat {
- let fontSize = UserDefaults.standard.double(forKey: "editorFontSize")
- let actualSize = fontSize > 0 ? fontSize : 14
- return actualSize >= 16 ? 38 : 18
- }
struct Map {
static let note: CGFloat = 18
- static let axisLabel: CGFloat = 18
static let vertexLabel: CGFloat = 18
}
}
@@ -109,6 +99,7 @@ struct Dimensions {
struct Toolbar {
static let radius: CGFloat = 8.0
+ static let glassRadius: CGFloat = 24.0
static let size: CGFloat = 16.0
static let width: CGFloat = 60.0
static let height: CGFloat = 40.0
diff --git a/Map/Presentation/Theme/Font+theme.swift b/Map/Presentation/Theme/Font+theme.swift
index 4660e48..bb1d8f6 100644
--- a/Map/Presentation/Theme/Font+theme.swift
+++ b/Map/Presentation/Theme/Font+theme.swift
@@ -31,18 +31,12 @@ extension Font {
static let emphasized = Font.ronzino(size: Dimensions.FontSize.body).weight(.medium)
}
- struct Title {
- static let emphasized = Font.libertinus(size: Dimensions.FontSize.title).weight(.medium)
- }
-
struct Caption {
- static let regular = Font.ronzino(size: Dimensions.FontSize.caption).weight(.regular)
static let emphasized = Font.ronzino(size: Dimensions.FontSize.caption).weight(.medium)
}
struct SmallControl {
static let regular = Font.ronzino(size: Dimensions.FontSize.smallControl).weight(.regular)
- static let emphasized = Font.ronzino(size: Dimensions.FontSize.smallControl).weight(.medium)
}
struct Map {
diff --git a/Map/Presentation/Theme/NSColor+theme.swift b/Map/Presentation/Theme/NSColor+theme.swift
index 7e53717..21db412 100644
--- a/Map/Presentation/Theme/NSColor+theme.swift
+++ b/Map/Presentation/Theme/NSColor+theme.swift
@@ -29,11 +29,43 @@ extension NSColor {
.withAlphaComponent(
0.3)
static let error = (NSColor(named: "Jasper Red") ?? .textColor)
+
+ static func colorForSyntax(_ captureName: String) -> NSColor {
+ switch captureName {
+ case ("variable.component_label"):
+ return .Theme.Syntax.vertex
+ case ("number.real_number"):
+ return .Theme.Syntax.number
+ case ("punctuation.position"):
+ return .Theme.Syntax.punctuation
+ case ("punctuation.shape"):
+ return .Theme.Syntax.punctuation
+ case ("punctuation.dependency_type"):
+ return .Theme.Syntax.punctuation
+ case ("parameter.optional"):
+ return .Theme.Syntax.option
+ case ("operator.sign"):
+ return .Theme.Syntax.option
+ case ("keyword.evolution"):
+ return .Theme.Syntax.symbol
+ case ("keyword.inertia"):
+ return .Theme.Syntax.symbol
+ case ("keyword.stage"):
+ return .Theme.Syntax.symbol
+ case ("keyword.stage_number"):
+ return .Theme.Syntax.symbol
+ case ("keyword.note"):
+ return .Theme.Syntax.symbol
+ case ("keyword.group"):
+ return .Theme.Syntax.symbol
+ default:
+ return .Theme.Syntax.text
+ }
+ }
}
struct UI {
static let background = NSColor(named: "Background") ?? .windowBackgroundColor
- static let border = NSColor(named: "Border") ?? .separatorColor
}
}
}
diff --git a/Map/Presentation/Theme/NSFont+theme.swift b/Map/Presentation/Theme/NSFont+theme.swift
index cd119f2..712cc1d 100644
--- a/Map/Presentation/Theme/NSFont+theme.swift
+++ b/Map/Presentation/Theme/NSFont+theme.swift
@@ -16,30 +16,12 @@ import AppKit
extension NSFont {
- static func libertinus(size: CGFloat) -> NSFont {
- return NSFont(name: "Libertinus Serif", size: size) ?? .systemFont(ofSize: size)
- }
-
static func ronzino(size: CGFloat) -> NSFont {
return NSFont(name: "Ronzino", size: size) ?? .systemFont(ofSize: size)
}
public struct Theme {
- struct Body {
- @MainActor static let regular = NSFont.ronzino(size: Dimensions.FontSize.body)
- @MainActor static let emphasized = NSFont.ronzino(size: Dimensions.FontSize.body)
- }
-
- struct Title {
- @MainActor static let emphasized = NSFont.libertinus(size: Dimensions.FontSize.title)
- }
-
- struct Caption {
- @MainActor static let regular = NSFont.ronzino(size: Dimensions.FontSize.caption)
- @MainActor static let emphasized = NSFont.ronzino(size: Dimensions.FontSize.caption)
- }
-
struct SmallControl {
@MainActor static let regular = NSFont.ronzino(size: Dimensions.FontSize.smallControl)
@MainActor static let emphasized = NSFont.ronzino(size: Dimensions.FontSize.smallControl)
diff --git a/QuickLook/PreviewViewController.swift b/QuickLook/PreviewViewController.swift
index c93b2cb..ecacee0 100644
--- a/QuickLook/PreviewViewController.swift
+++ b/QuickLook/PreviewViewController.swift
@@ -1,13 +1,10 @@
-//
-// PreviewViewController.swift
-// QuickLook
-//
-// Created by Ruben Beltran del Rio on 2025-07-11.
-//
-
import Cocoa
import Quartz
import SwiftUI
+import WmapParser
+import SwiftTreeSitter
+import SwiftTreeSitterLayer
+import TreeSitterWmap
class PreviewViewController: NSViewController, QLPreviewingController {
@@ -48,43 +45,12 @@ class PreviewViewController: NSViewController, QLPreviewingController {
previewContent = AnyView(createPlainTextView(content: content))
case .highlightedText:
- // Parse for syntax highlighting
- let lexer = Wmap.Lexer(content)
- let parser = Wmap.Parser(lexer: lexer)
- let parsedMap = parser.parse()
previewContent = AnyView(
- createHighlightedTextView(content: content, parsedMap: parsedMap))
+ createHighlightedTextView(content: content))
case .map:
- // Parse the content to create a ParsedMap
- let lexer = Wmap.Lexer(content)
- let parser = Wmap.Parser(lexer: lexer)
- let parsedMap = parser.parse()
-
- if parsedMap.entities.isEmpty {
- // Fallback view for empty or invalid maps
- previewContent = AnyView(
- VStack {
- Image(systemName: "map")
- .font(.system(size: 48))
- .foregroundColor(.gray)
- Text("quick_look.empty_map.title")
- .font(.Theme.Title.emphasized)
- .foregroundColor(.Theme.UI.foreground)
- Text("quick_look.empty_map.description")
- .font(.Theme.Body.regular)
- .foregroundColor(.Theme.UI.foreground)
- }
- .frame(maxWidth: .infinity, maxHeight: .infinity)
- .foregroundColor(.Theme.UI.background)
- )
- } else {
- // Limit entities for memory efficiency in QuickLook
- let limitedEntities = Array(parsedMap.entities.prefix(100)) // Limit to first 100 entities
- let limitedMap = Wmap.ParsedMap(
- entities: limitedEntities, vertexLabels: parsedMap.vertexLabels)
- previewContent = AnyView(MapPreviewView(parsedMap: limitedMap))
- }
+ let parsedMap = WmapParser.parse(content)
+ previewContent = AnyView(MapPreviewView(parsedMap: parsedMap))
}
let hostingView = NSHostingView(rootView: previewContent)
@@ -117,8 +83,8 @@ class PreviewViewController: NSViewController, QLPreviewingController {
.background(Color.white)
}
- private func createHighlightedTextView(content: String, parsedMap: Wmap.ParsedMap) -> some View {
- QuickLookTextEditor(content: content, parsedMap: parsedMap)
+ private func createHighlightedTextView(content: String) -> some View {
+ QuickLookTextEditor(content: content)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.white)
}
@@ -136,7 +102,7 @@ class PreviewViewController: NSViewController, QLPreviewingController {
// A SwiftUI view specifically for QuickLook preview
struct MapPreviewView: View {
- let parsedMap: Wmap.ParsedMap
+ let parsedMap: WmapParser.Map
@State private var selectedEvolution: StageType = .general
var body: some View {
@@ -153,7 +119,7 @@ struct MapPreviewView: View {
ScrollView([.horizontal, .vertical]) {
MapRenderView(
- entities: parsedMap.entities,
+ parsedMap: parsedMap,
evolution: $selectedEvolution
)
.drawingGroup(opaque: false)
@@ -172,7 +138,6 @@ struct MapPreviewView: View {
// A read-only text editor for QuickLook that shows syntax highlighting
struct QuickLookTextEditor: NSViewRepresentable {
let content: String
- let parsedMap: Wmap.ParsedMap
func makeNSView(context: Context) -> NSScrollView {
let scrollView = NSTextView.scrollableTextView()
@@ -195,14 +160,9 @@ struct QuickLookTextEditor: NSViewRepresentable {
textView.textContainer?.containerSize = NSSize(
width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude)
- // Apply syntax highlighting
+ // Apply syntax highlighting using tree-sitter
if let textStorage = textView.textStorage {
- let syntaxHighlighter = Wmap.SyntaxHighlighter()
- syntaxHighlighter.updateSyntaxElements(from: parsedMap)
-
- // Apply highlighting to the entire text
- let fullRange = NSRange(location: 0, length: textStorage.length)
- syntaxHighlighter.applySyntaxHighlighting(textStorage: textStorage, range: fullRange)
+ applyTreeSitterHighlighting(textStorage: textStorage, content: content)
}
return scrollView
@@ -211,4 +171,39 @@ struct QuickLookTextEditor: NSViewRepresentable {
func updateNSView(_ nsView: NSScrollView, context: Context) {
// No updates needed for read-only view
}
+
+ private func applyTreeSitterHighlighting(textStorage: NSTextStorage, content: String) {
+ // Set up tree-sitter language layer
+ guard let wmapConfiguration = try? LanguageConfiguration(tree_sitter_wmap(), name: "wmap")
+ else { return }
+
+ let languageConfiguration = LanguageLayer.Configuration(languageProvider: { name in
+ switch name {
+ case "wmap": return wmapConfiguration
+ default: return nil
+ }
+ })
+
+ guard
+ let rootLayer = try? LanguageLayer(
+ languageConfig: wmapConfiguration, configuration: languageConfiguration)
+ else { return }
+
+ // Update layer with content
+ rootLayer.replaceContent(with: content)
+
+ // Set default text color
+ let fullRange = NSRange(location: 0, length: textStorage.length)
+ textStorage.addAttribute(.foregroundColor, value: NSColor.textColor, range: fullRange)
+
+ // Apply tree-sitter highlighting
+ guard let highlights = try? rootLayer.highlights(in: fullRange, provider: content.predicateTextProvider)
+ else { return }
+
+ for highlight in highlights {
+ let color = NSColor.Theme.Syntax.colorForSyntax(highlight.name)
+ textStorage.addAttribute(.foregroundColor, value: color, range: highlight.range)
+ }
+ }
+
}