Browse Source

Add rudimentary JavaScript highlighting to query text view

master
Shadowfacts 3 months ago
parent
commit
6e4cbfdb6d
Signed by: Shadowfacts <me@shadowfacts.net> GPG Key ID: 94A5AB95422746E5

+ 104
- 1
MongoView.xcodeproj/project.pbxproj View File

@@ -42,6 +42,9 @@
42 42
 		D63CDF4523C970C50012D658 /* DatabaseViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = D63CDF4323C970C50012D658 /* DatabaseViewController.xib */; };
43 43
 		D6A7D096243541A400B46857 /* WindowStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A7D095243541A400B46857 /* WindowStatusView.swift */; };
44 44
 		D6A7D09A243546B500B46857 /* WindowStatusView.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6A7D099243546B500B46857 /* WindowStatusView.xib */; };
45
+		D6A7D0A42435885B00B46857 /* JavaScriptHighlighter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A7D0A32435885B00B46857 /* JavaScriptHighlighter.swift */; };
46
+		D6D13B042436C33D00493D97 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D13B032436C33D00493D97 /* main.swift */; };
47
+		D6D13B082436C34200493D97 /* JavaScriptHighlighter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A7D0A32435885B00B46857 /* JavaScriptHighlighter.swift */; };
45 48
 		D6D4665323CB730C00F13B1B /* MongoEvaluator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D4665223CB730C00F13B1B /* MongoEvaluator.swift */; };
46 49
 /* End PBXBuildFile section */
47 50
 
@@ -65,6 +68,15 @@
65 68
 			name = "Embed Frameworks";
66 69
 			runOnlyForDeploymentPostprocessing = 0;
67 70
 		};
71
+		D6D13AFF2436C33D00493D97 /* CopyFiles */ = {
72
+			isa = PBXCopyFilesBuildPhase;
73
+			buildActionMask = 2147483647;
74
+			dstPath = /usr/share/man/man1/;
75
+			dstSubfolderSpec = 0;
76
+			files = (
77
+			);
78
+			runOnlyForDeploymentPostprocessing = 1;
79
+		};
68 80
 /* End PBXCopyFilesBuildPhase section */
69 81
 
70 82
 /* Begin PBXFileReference section */
@@ -97,6 +109,9 @@
97 109
 		D63CDF4323C970C50012D658 /* DatabaseViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = DatabaseViewController.xib; sourceTree = "<group>"; };
98 110
 		D6A7D095243541A400B46857 /* WindowStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowStatusView.swift; sourceTree = "<group>"; };
99 111
 		D6A7D099243546B500B46857 /* WindowStatusView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = WindowStatusView.xib; sourceTree = "<group>"; };
112
+		D6A7D0A32435885B00B46857 /* JavaScriptHighlighter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JavaScriptHighlighter.swift; sourceTree = "<group>"; };
113
+		D6D13B012436C33D00493D97 /* jstest */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = jstest; sourceTree = BUILT_PRODUCTS_DIR; };
114
+		D6D13B032436C33D00493D97 /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = "<group>"; };
100 115
 		D6D4665223CB730C00F13B1B /* MongoEvaluator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MongoEvaluator.swift; sourceTree = "<group>"; };
101 116
 /* End PBXFileReference section */
102 117
 
@@ -117,6 +132,13 @@
117 132
 			);
118 133
 			runOnlyForDeploymentPostprocessing = 0;
119 134
 		};
135
+		D6D13AFE2436C33D00493D97 /* Frameworks */ = {
136
+			isa = PBXFrameworksBuildPhase;
137
+			buildActionMask = 2147483647;
138
+			files = (
139
+			);
140
+			runOnlyForDeploymentPostprocessing = 0;
141
+		};
120 142
 /* End PBXFrameworksBuildPhase section */
121 143
 
122 144
 /* Begin PBXGroup section */
@@ -150,6 +172,7 @@
150 172
 			isa = PBXGroup;
151 173
 			children = (
152 174
 				D63CDEBC23C837DC0012D658 /* MongoView */,
175
+				D6D13B022436C33D00493D97 /* jstest */,
153 176
 				D63CDEBB23C837DC0012D658 /* Products */,
154 177
 				D63CDF1523C837F10012D658 /* Frameworks */,
155 178
 			);
@@ -159,6 +182,7 @@
159 182
 			isa = PBXGroup;
160 183
 			children = (
161 184
 				D63CDEBA23C837DC0012D658 /* MongoView.app */,
185
+				D6D13B012436C33D00493D97 /* jstest */,
162 186
 			);
163 187
 			name = Products;
164 188
 			sourceTree = "<group>";
@@ -170,6 +194,7 @@
170 194
 				D63CDF3423C838190012D658 /* MongoController.swift */,
171 195
 				D63CDF3323C838190012D658 /* Node.swift */,
172 196
 				D6D4665223CB730C00F13B1B /* MongoEvaluator.swift */,
197
+				D6A7D0A22435880700B46857 /* Synax Highlighting */,
173 198
 				D60C863B23CA2DD600C9DB8E /* Windows */,
174 199
 				D60C863C23CA2DDD00C9DB8E /* View Controllers */,
175 200
 				D63CDEBF23C837DD0012D658 /* Assets.xcassets */,
@@ -196,6 +221,22 @@
196 221
 			name = Frameworks;
197 222
 			sourceTree = "<group>";
198 223
 		};
224
+		D6A7D0A22435880700B46857 /* Synax Highlighting */ = {
225
+			isa = PBXGroup;
226
+			children = (
227
+				D6A7D0A32435885B00B46857 /* JavaScriptHighlighter.swift */,
228
+			);
229
+			path = "Synax Highlighting";
230
+			sourceTree = "<group>";
231
+		};
232
+		D6D13B022436C33D00493D97 /* jstest */ = {
233
+			isa = PBXGroup;
234
+			children = (
235
+				D6D13B032436C33D00493D97 /* main.swift */,
236
+			);
237
+			path = jstest;
238
+			sourceTree = "<group>";
239
+		};
199 240
 /* End PBXGroup section */
200 241
 
201 242
 /* Begin PBXNativeTarget section */
@@ -217,19 +258,39 @@
217 258
 			productReference = D63CDEBA23C837DC0012D658 /* MongoView.app */;
218 259
 			productType = "com.apple.product-type.application";
219 260
 		};
261
+		D6D13B002436C33D00493D97 /* jstest */ = {
262
+			isa = PBXNativeTarget;
263
+			buildConfigurationList = D6D13B072436C33D00493D97 /* Build configuration list for PBXNativeTarget "jstest" */;
264
+			buildPhases = (
265
+				D6D13AFD2436C33D00493D97 /* Sources */,
266
+				D6D13AFE2436C33D00493D97 /* Frameworks */,
267
+				D6D13AFF2436C33D00493D97 /* CopyFiles */,
268
+			);
269
+			buildRules = (
270
+			);
271
+			dependencies = (
272
+			);
273
+			name = jstest;
274
+			productName = jstest;
275
+			productReference = D6D13B012436C33D00493D97 /* jstest */;
276
+			productType = "com.apple.product-type.tool";
277
+		};
220 278
 /* End PBXNativeTarget section */
221 279
 
222 280
 /* Begin PBXProject section */
223 281
 		D63CDEB223C837DC0012D658 /* Project object */ = {
224 282
 			isa = PBXProject;
225 283
 			attributes = {
226
-				LastSwiftUpdateCheck = 1130;
284
+				LastSwiftUpdateCheck = 1140;
227 285
 				LastUpgradeCheck = 1140;
228 286
 				ORGANIZATIONNAME = Shadowfacts;
229 287
 				TargetAttributes = {
230 288
 					D63CDEB923C837DC0012D658 = {
231 289
 						CreatedOnToolsVersion = 11.3;
232 290
 					};
291
+					D6D13B002436C33D00493D97 = {
292
+						CreatedOnToolsVersion = 11.4;
293
+					};
233 294
 				};
234 295
 			};
235 296
 			buildConfigurationList = D63CDEB523C837DC0012D658 /* Build configuration list for PBXProject "MongoView" */;
@@ -246,6 +307,7 @@
246 307
 			projectRoot = "";
247 308
 			targets = (
248 309
 				D63CDEB923C837DC0012D658 /* MongoView */,
310
+				D6D13B002436C33D00493D97 /* jstest */,
249 311
 			);
250 312
 		};
251 313
 /* End PBXProject section */
@@ -282,10 +344,20 @@
282 344
 				D63CDF4423C970C50012D658 /* DatabaseViewController.swift in Sources */,
283 345
 				D63CDF3C23C838470012D658 /* DatabaseWindowController.swift in Sources */,
284 346
 				D6A7D096243541A400B46857 /* WindowStatusView.swift in Sources */,
347
+				D6A7D0A42435885B00B46857 /* JavaScriptHighlighter.swift in Sources */,
285 348
 				D63CDF4023C839010012D658 /* QueryViewController.swift in Sources */,
286 349
 			);
287 350
 			runOnlyForDeploymentPostprocessing = 0;
288 351
 		};
352
+		D6D13AFD2436C33D00493D97 /* Sources */ = {
353
+			isa = PBXSourcesBuildPhase;
354
+			buildActionMask = 2147483647;
355
+			files = (
356
+				D6D13B082436C34200493D97 /* JavaScriptHighlighter.swift in Sources */,
357
+				D6D13B042436C33D00493D97 /* main.swift in Sources */,
358
+			);
359
+			runOnlyForDeploymentPostprocessing = 0;
360
+		};
289 361
 /* End PBXSourcesBuildPhase section */
290 362
 
291 363
 /* Begin PBXVariantGroup section */
@@ -457,6 +529,28 @@
457 529
 			};
458 530
 			name = Release;
459 531
 		};
532
+		D6D13B052436C33D00493D97 /* Debug */ = {
533
+			isa = XCBuildConfiguration;
534
+			buildSettings = {
535
+				CODE_SIGN_STYLE = Automatic;
536
+				DEVELOPMENT_TEAM = HGYVAQA9FW;
537
+				ENABLE_HARDENED_RUNTIME = YES;
538
+				PRODUCT_NAME = "$(TARGET_NAME)";
539
+				SWIFT_VERSION = 5.0;
540
+			};
541
+			name = Debug;
542
+		};
543
+		D6D13B062436C33D00493D97 /* Release */ = {
544
+			isa = XCBuildConfiguration;
545
+			buildSettings = {
546
+				CODE_SIGN_STYLE = Automatic;
547
+				DEVELOPMENT_TEAM = HGYVAQA9FW;
548
+				ENABLE_HARDENED_RUNTIME = YES;
549
+				PRODUCT_NAME = "$(TARGET_NAME)";
550
+				SWIFT_VERSION = 5.0;
551
+			};
552
+			name = Release;
553
+		};
460 554
 /* End XCBuildConfiguration section */
461 555
 
462 556
 /* Begin XCConfigurationList section */
@@ -478,6 +572,15 @@
478 572
 			defaultConfigurationIsVisible = 0;
479 573
 			defaultConfigurationName = Release;
480 574
 		};
575
+		D6D13B072436C33D00493D97 /* Build configuration list for PBXNativeTarget "jstest" */ = {
576
+			isa = XCConfigurationList;
577
+			buildConfigurations = (
578
+				D6D13B052436C33D00493D97 /* Debug */,
579
+				D6D13B062436C33D00493D97 /* Release */,
580
+			);
581
+			defaultConfigurationIsVisible = 0;
582
+			defaultConfigurationName = Release;
583
+		};
481 584
 /* End XCConfigurationList section */
482 585
 	};
483 586
 	rootObject = D63CDEB223C837DC0012D658 /* Project object */;

+ 279
- 0
MongoView/Synax Highlighting/JavaScriptHighlighter.swift View File

@@ -0,0 +1,279 @@
1
+//
2
+//  JavaScriptHighlighter.swift
3
+//  MongoView
4
+//
5
+//  Created by Shadowfacts on 4/1/20.
6
+//  Copyright © 2020 Shadowfacts. All rights reserved.
7
+//
8
+
9
+import AppKit
10
+
11
+fileprivate let identifiers: CharacterSet = {
12
+    var set = CharacterSet.alphanumerics
13
+    set.insert(charactersIn: "$_")
14
+    return set
15
+}()
16
+fileprivate let identifierStarts: CharacterSet = {
17
+    var set = identifiers
18
+    set.subtract(.decimalDigits)
19
+    return set
20
+}()
21
+fileprivate let operators = CharacterSet(charactersIn: "+-*/<>=")
22
+
23
+class JavaScriptHighlighter {
24
+    private let text: String
25
+    private var attributed: NSMutableAttributedString!
26
+    private var currentIndex: String.Index!
27
+    private var indent = ""
28
+    
29
+    init(text: String) {
30
+        self.text = text
31
+    }
32
+    
33
+    private func print(_ str: String) {
34
+        Swift.print("\(indent)\(str)")
35
+    }
36
+    
37
+    private func range(from: String.Index, to: String.Index) -> NSRange {
38
+        return NSRange(from..<to, in: text)
39
+    }
40
+    
41
+    private func prevCharRange() -> NSRange {
42
+        return range(from: text.index(before: currentIndex), to: currentIndex)
43
+    }
44
+    
45
+    private func peek() -> Unicode.Scalar? {
46
+        guard currentIndex < text.endIndex else {
47
+            return nil
48
+        }
49
+        return text.unicodeScalars[currentIndex]
50
+    }
51
+    
52
+    private func peek(length: Int) -> Substring {
53
+        let realLength = min(length, text.distance(from: currentIndex, to: text.endIndex))
54
+        return text[currentIndex..<text.index(currentIndex, offsetBy: realLength)]
55
+    }
56
+    
57
+    @discardableResult
58
+    private func consume() -> Unicode.Scalar? {
59
+        let c = peek()
60
+        currentIndex = text.index(after: currentIndex)
61
+        return c
62
+    }
63
+    
64
+    func highlight(mutableAttributed: NSMutableAttributedString? = nil) -> NSAttributedString {
65
+        attributed = mutableAttributed ?? NSMutableAttributedString(attributedString: NSAttributedString(string: text))
66
+        
67
+        let fullRange = NSRange(location: 0, length: attributed.length)
68
+        attributed.setAttributes([
69
+            .foregroundColor: NSColor.textColor,
70
+            .font: NSFont.monospacedSystemFont(ofSize: 13, weight: .regular)
71
+        ], range: fullRange)
72
+
73
+        currentIndex = text.startIndex
74
+        while currentIndex < text.endIndex {
75
+            consumeExpression()
76
+        }
77
+
78
+        return attributed
79
+    }
80
+
81
+    private func consumeExpression() {
82
+        guard let char = peek() else { return }
83
+        
84
+        if identifierStarts.contains(char) {
85
+            consumeIdentifier()
86
+        } else if char == "'" || char == "\"" {
87
+            consumeString()
88
+        } else if char == "`" {
89
+            consumeTemplateString()
90
+        } else if CharacterSet.decimalDigits.contains(char) {
91
+            consumeNumber()
92
+        } else if operators.contains(char) {
93
+            consumeOperator()
94
+        } else if char == "(" {
95
+            consumeFunctionCallOrGrouping()
96
+        } else if char == "{" {
97
+            consumeObject()
98
+        } else if char == "[" {
99
+            consumeArray()
100
+        } else if char == "." {
101
+            consumeDotLookup()
102
+        } else {
103
+            consume()
104
+        }
105
+    }
106
+
107
+    private func consumeIdentifier() {
108
+        let identifierStart = currentIndex!
109
+        while let char = peek(), identifiers.contains(char) {
110
+            consume()
111
+        }
112
+        print("Identifier: '\(text[identifierStart..<currentIndex])'")
113
+    }
114
+
115
+    private func consumeNumber() {
116
+        let numberStart = currentIndex!
117
+        while let char = peek(), CharacterSet.decimalDigits.contains(char) {
118
+            consume()
119
+        }
120
+        if currentIndex < text.endIndex && peek() == "." {
121
+            consume()
122
+            while let char = peek(), CharacterSet.decimalDigits.contains(char) {
123
+                consume()
124
+            }
125
+        }
126
+        print("Number: \(text[numberStart..<currentIndex])")
127
+        attributed.addAttribute(.foregroundColor, value: NSColor.systemBlue, range: range(from: numberStart, to: currentIndex))
128
+    }
129
+    
130
+    private func consumeString() {
131
+        let stringStart = currentIndex!
132
+        let startChar = consume()
133
+        while currentIndex < text.endIndex && peek() != startChar && (currentIndex == text.startIndex || text[text.index(before: currentIndex)] != "\\") {
134
+            consume()
135
+        }
136
+        if currentIndex < text.endIndex {
137
+            consume() // string closing quote
138
+        }
139
+        print("String: \(text[stringStart..<currentIndex])")
140
+        attributed.addAttribute(.foregroundColor, value: NSColor.systemRed, range: range(from: stringStart, to: currentIndex))
141
+    }
142
+    
143
+    private func consumeTemplateString() {
144
+        var stringFragmentStart: String.Index? = currentIndex
145
+        consume() // opening `
146
+        while currentIndex < text.endIndex {
147
+            if peek(length: 2) == "${" {
148
+                consume() // $
149
+                consume() // {
150
+                print("Template string fragment: '\(text[stringFragmentStart!..<currentIndex])'")
151
+                attributed.addAttribute(.foregroundColor, value: NSColor.systemRed, range: range(from: stringFragmentStart!, to: currentIndex))
152
+                consumeTemplateStringExpression()
153
+                stringFragmentStart = currentIndex
154
+                if currentIndex < text.endIndex && peek() == "}" {
155
+                    consume()
156
+                }
157
+            } else if peek() == "`" {
158
+                stringFragmentStart = stringFragmentStart ?? currentIndex
159
+                consume() // `
160
+                print("Template string fragment: '\(text[stringFragmentStart!..<currentIndex])'")
161
+                attributed.addAttribute(.foregroundColor, value: NSColor.systemRed, range: range(from: stringFragmentStart!, to: currentIndex))
162
+                stringFragmentStart = nil
163
+                break
164
+            } else {
165
+                consume()
166
+            }
167
+        }
168
+        if let start = stringFragmentStart {
169
+            print("Template string fragment: '\(text[start..<currentIndex])'")
170
+            attributed.addAttribute(.foregroundColor, value: NSColor.systemRed, range: range(from: start, to: currentIndex))
171
+        }
172
+    }
173
+    
174
+    private func consumeTemplateStringExpression() {
175
+        indent += "  "
176
+        while currentIndex < text.endIndex && peek() != "}" {
177
+            consumeExpression()
178
+        }
179
+        indent = String(indent.dropLast(2))
180
+    }
181
+
182
+    private func consumeOperator() {
183
+        print("Operator: \(peek()!)")
184
+        consume()
185
+    }
186
+    
187
+    private func consumeFunctionCallOrGrouping() {
188
+        consume() // (
189
+        print("Opening (")
190
+        attributed.addAttribute(.foregroundColor, value: NSColor.systemTeal, range: prevCharRange())
191
+        indent += "  "
192
+        while currentIndex < text.endIndex && peek() != ")" {
193
+            consumeExpression()
194
+        }
195
+        indent = String(indent.dropLast(2))
196
+        if currentIndex < text.endIndex {
197
+            consume() // )
198
+            print("Closing )")
199
+            attributed.addAttribute(.foregroundColor, value: NSColor.systemTeal, range: prevCharRange())
200
+        }
201
+    }
202
+    
203
+    private func consumeObject() {
204
+        consume() // {
205
+        print("Opening {")
206
+        attributed.addAttribute(.foregroundColor, value: NSColor.systemTeal, range: prevCharRange())
207
+        indent += "  "
208
+        object:
209
+        while currentIndex < text.endIndex && peek() != "}" {
210
+            consumeObjectKey()
211
+            if peek() == ":" {
212
+                consume() // :
213
+                while currentIndex < text.endIndex && peek() != "," && peek() != "}" {
214
+                    consumeExpression()
215
+                }
216
+                if currentIndex < text.endIndex {
217
+                    break object
218
+                }
219
+            } else if peek() == "," {
220
+                continue
221
+            } else {
222
+                break
223
+            }
224
+        }
225
+        indent = String(indent.dropLast(2))
226
+        if currentIndex < text.endIndex {
227
+            consume() // }
228
+            print("Closing }")
229
+            attributed.addAttribute(.foregroundColor, value: NSColor.systemTeal, range: prevCharRange())
230
+        }
231
+    }
232
+    
233
+    private func consumeObjectKey() {
234
+        guard let char = peek() else { return }
235
+        let keyStart = currentIndex!
236
+        if identifierStarts.contains(char) {
237
+            consumeIdentifier()
238
+        } else if char == "'" || char == "\"" {
239
+            consumeString()
240
+        }
241
+        print("Object key: '\(text[keyStart..<currentIndex])'")
242
+    }
243
+    
244
+    private func consumeDotLookup() {
245
+        consume() // .
246
+        print("Dot lookup")
247
+        attributed.addAttribute(.foregroundColor, value: NSColor.systemTeal, range: prevCharRange())
248
+    }
249
+    
250
+    private func consumeArray() {
251
+        consume() // [
252
+        print("Opening [")
253
+        attributed.addAttribute(.foregroundColor, value: NSColor.systemTeal, range: prevCharRange())
254
+        indent += "  "
255
+        array:
256
+        while currentIndex < text.endIndex && peek() != "]" {
257
+            print("Array element")
258
+            while currentIndex < text.endIndex {
259
+                if peek() == "," {
260
+                    consume() // ,
261
+                    break
262
+                } else if peek() == "]" {
263
+                    break array
264
+                } else {
265
+                    indent += "  "
266
+                    consumeExpression()
267
+                    indent = String(indent.dropLast(2))
268
+                }
269
+            }
270
+        }
271
+        indent = String(indent.dropLast(2))
272
+        if currentIndex < text.endIndex {
273
+            consume() // ]
274
+            print("Closing ]")
275
+            attributed.addAttribute(.foregroundColor, value: NSColor.systemTeal, range: prevCharRange())
276
+        }
277
+    }
278
+    
279
+}

+ 12
- 0
MongoView/View Controllers/QueryViewController.swift View File

@@ -53,6 +53,8 @@ class QueryViewController: NSViewController {
53 53
         queryTextView.font = .monospacedSystemFont(ofSize: 13, weight: .regular)
54 54
         queryTextView.isAutomaticQuoteSubstitutionEnabled = false
55 55
         queryTextView.string = defaultQuery
56
+        queryTextView.delegate = self
57
+        highlightQuery()
56 58
         
57 59
         outlineView.dataSource = self
58 60
         outlineView.delegate = self
@@ -76,6 +78,10 @@ class QueryViewController: NSViewController {
76 78
         view.window!.makeFirstResponder(outlineView)
77 79
     }
78 80
     
81
+    func highlightQuery() {
82
+        _ = JavaScriptHighlighter(text: queryTextView.string).highlight(mutableAttributed: queryTextView.textStorage!)
83
+    }
84
+    
79 85
     func refresh(reload: Bool = true) {
80 86
         if let query = mostRecentQuery {
81 87
             let connStr = "\(mongoController.connectionString)/\(collection.database)"
@@ -170,6 +176,12 @@ class QueryViewController: NSViewController {
170 176
     
171 177
 }
172 178
 
179
+extension QueryViewController: NSTextViewDelegate {
180
+    func textDidChange(_ notification: Notification) {
181
+        highlightQuery()
182
+    }
183
+}
184
+
173 185
 extension QueryViewController: NSMenuItemValidation {
174 186
     func validateMenuItem(_ menuItem: NSMenuItem) -> Bool {
175 187
         if menuItem.action == #selector(deleteNode(_:)) {

+ 13
- 0
jstest/main.swift View File

@@ -0,0 +1,13 @@
1
+//
2
+//  main.swift
3
+//  jstest
4
+//
5
+//  Created by Shadowfacts on 4/2/20.
6
+//  Copyright © 2020 Shadowfacts. All rights reserved.
7
+//
8
+
9
+import Foundation
10
+
11
+let source = "`+`${{a:3}}`"
12
+_ = JavaScriptHighlighter(text: source).highlight()
13
+

Loading…
Cancel
Save