diff --git a/Package.resolved b/Package.resolved
index 75cd3b8..746ac6a 100644
--- a/Package.resolved
+++ b/Package.resolved
@@ -32,8 +32,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/vapor/leaf.git",
"state" : {
- "revision" : "bf48d2423c00292b5937c60166c7db99705cae47",
- "version" : "4.4.1"
+ "revision" : "d469584b9186851c5a4012d11325fb9db3207ebb",
+ "version" : "4.5.0"
}
},
{
diff --git a/Public/css/highlight.css b/Public/css/highlight.css
index 6ac380a..49b5518 100644
--- a/Public/css/highlight.css
+++ b/Public/css/highlight.css
@@ -85,6 +85,10 @@
color: #c0c;
}
+.exp-error {
+ background: #d22;
+}
+
span.match-char {
background: rgba(112, 176, 224, 0.5);
color: #101112;
diff --git a/Public/js/app.js b/Public/js/app.js
index 9c81c66..5552f27 100644
--- a/Public/js/app.js
+++ b/Public/js/app.js
@@ -227,6 +227,10 @@ export class App {
onExpressionFieldChange() {
if (!this.expressionField.value) {
+ this.expressionField.tokens = [];
+ this.expressionField.error = null;
+ this.dslView.value = "";
+ this.dslView.error = null;
this.updateMatchCount(0, "match-count");
return;
}
@@ -349,13 +353,35 @@ export class App {
} else {
this.expressionField.tokens = [];
}
- this.expressionField.error = response.error;
+ if (response.error) {
+ try {
+ const error = JSON.parse(response.error);
+ if (error) {
+ this.expressionField.error = error;
+ }
+ } catch (e) {
+ this.expressionField.error = response.error;
+ }
+ } else {
+ this.expressionField.error = null;
+ }
break;
case "convertToDSL":
if (response.result) {
this.dslView.value = JSON.parse(response.result);
}
- this.dslView.error = response.error;
+ if (response.error) {
+ try {
+ const error = JSON.parse(response.error);
+ if (error) {
+ this.dslView.error = error;
+ }
+ } catch (e) {
+ this.dslView.error = response.error;
+ }
+ } else {
+ this.dslView.error = null;
+ }
break;
case "match":
if (response.result) {
diff --git a/Public/js/views/dsl_view.js b/Public/js/views/dsl_view.js
index f21928b..d3ff030 100644
--- a/Public/js/views/dsl_view.js
+++ b/Public/js/views/dsl_view.js
@@ -16,7 +16,7 @@ export class DSLView extends EventDispatcher {
}
set value(val) {
- this.editor.setValue(val || this.defaultText);
+ this.editor.setValue(val);
}
set error(error) {
@@ -32,14 +32,26 @@ export class DSLView extends EventDispatcher {
if (!error) {
return;
}
-
- widgets.push(
- editor.addLineWidget(0, ErrorMessage.create(error), {
- coverGutter: false,
- noHScroll: true,
- above: true,
- })
- );
+ if (typeof error === "string" && error instanceof String) {
+ widgets.push(
+ editor.addLineWidget(0, ErrorMessage.create(error), {
+ coverGutter: false,
+ noHScroll: true,
+ above: true,
+ })
+ );
+ } else {
+ for (const e of error) {
+ const message = ErrorMessage.create(e.message);
+ widgets.push(
+ editor.addLineWidget(0, message, {
+ coverGutter: false,
+ noHScroll: true,
+ above: true,
+ })
+ );
+ }
+ }
});
}
diff --git a/Public/js/views/expression_field.js b/Public/js/views/expression_field.js
index 88a3dcf..0b7a054 100644
--- a/Public/js/views/expression_field.js
+++ b/Public/js/views/expression_field.js
@@ -30,17 +30,34 @@ export class ExpressionField extends EventDispatcher {
set error(error) {
if (error) {
- const content = Utils.htmlSafe(error);
- this.errorMessageTooltip.setContent(
- `Parse Error: ${content}`
- );
+ let message = "";
+ if (typeof error === "string" && error instanceof String) {
+ const errorMessage = Utils.htmlSafe(error);
+ message = `Parse Error: ${errorMessage}`;
+ } else {
+ message = error
+ .map((e) => {
+ const errorMessage = Utils.htmlSafe(e.message);
+ return `${e.behavior}: ${errorMessage}`;
+ })
+ .join("
");
+ this.highlighter.drawError(error);
+ }
+ this.errorMessageTooltip.setContent(message);
document
.getElementById("expression-field-error")
.classList.remove("d-none");
} else {
this.errorMessageTooltip.setContent("");
document.getElementById("expression-field-error").classList.add("d-none");
+ this.highlighter.clearError();
}
+
+ tippy(".exp-error", {
+ allowHTML: true,
+ animation: false,
+ placement: "bottom",
+ });
}
init(container) {
@@ -85,6 +102,9 @@ export class ExpressionField extends EventDispatcher {
...tooltipProps,
onShow: (instance) => {
const index = instance.reference.dataset.tokenIndex;
+ if (index === undefined) {
+ return false;
+ }
const token = this.expressionTokens[index];
this.onHover(token, instance);
return false;
diff --git a/Public/js/views/expression_highlighter.js b/Public/js/views/expression_highlighter.js
index eb8e21c..385887c 100644
--- a/Public/js/views/expression_highlighter.js
+++ b/Public/js/views/expression_highlighter.js
@@ -10,6 +10,7 @@ export default class ExpressionHighlighter extends EventDispatcher {
this.editor = editor;
this.activeMarks = [];
this.hoverMarks = [];
+ this.widgets = [];
}
draw(tokens) {
@@ -60,13 +61,52 @@ export default class ExpressionHighlighter extends EventDispatcher {
});
}
+ drawError(errors) {
+ this.clearError();
+
+ const pre = ExpressionHighlighter.CSS_PREFIX;
+ const editor = this.editor;
+ editor.operation(() => {
+ for (const error of errors) {
+ const location = Editor.calcRangePos(
+ this.editor,
+ error.location.start,
+ error.location.end - error.location.start
+ );
+ const widget = document.createElement("span");
+ widget.className = `${pre}-error`;
+
+ widget.style.height = `2px`;
+ widget.style.zIndex = "10";
+ widget.setAttribute("data-tippy-content", error.message);
+
+ editor.addWidget(location.startPos, widget);
+ const startCoords = editor.charCoords(location.startPos, "local");
+ const endCoords = editor.charCoords(location.endPos, "local");
+ widget.style.left = `${startCoords.left + 1}px`;
+ widget.style.top = `${startCoords.bottom}px`;
+ widget.style.width = `${endCoords.left - startCoords.left - 2}px`;
+
+ this.widgets.push(widget);
+ }
+ });
+ }
+
clear() {
this.editor.operation(() => {
- let marks = this.activeMarks;
- for (var i = 0, l = marks.length; i < l; i++) {
- marks[i].clear();
+ for (const mark of this.activeMarks) {
+ mark.clear();
+ }
+ this.activeMarks.length = 0;
+ });
+ }
+
+ clearError() {
+ this.editor.operation(() => {
+ for (const widget of this.widgets) {
+ widget.parentNode.removeChild(widget);
}
- marks.length = 0;
+ this.widgets.length = 0;
});
}
@@ -77,9 +117,7 @@ export default class ExpressionHighlighter extends EventDispatcher {
return;
}
- while (this.hoverMarks.length) {
- this.hoverMarks.pop().clear();
- }
+ this.clearHover();
if (selection) {
this.drawBorder(selection, "selected");
@@ -119,9 +157,12 @@ export default class ExpressionHighlighter extends EventDispatcher {
}
clearHover() {
- while (this.hoverMarks.length) {
- this.hoverMarks.pop().clear();
- }
+ this.editor.operation(() => {
+ for (const mark of this.hoverMarks) {
+ mark.clear();
+ }
+ this.hoverMarks.length = 0;
+ });
}
}
diff --git a/Sources/DSLConverter/DSLConverter.swift b/Sources/DSLConverter/DSLConverter.swift
index c90550a..020d815 100644
--- a/Sources/DSLConverter/DSLConverter.swift
+++ b/Sources/DSLConverter/DSLConverter.swift
@@ -3,9 +3,13 @@ import Foundation
@testable @_spi(RegexBuilder) import _StringProcessing
@testable @_spi(PatternConverter) import _StringProcessing
-struct DSLConverter {
+class DSLConverter {
+ private(set) var diagnostics: Diagnostics?
+
func convert(_ pattern: String, matchingOptions: [String] = []) throws -> String {
- let ast = try _RegexParser.parse(pattern, .traditional)
+ let ast = _RegexParser.parseWithRecovery(pattern, .traditional)
+ diagnostics = ast.diags
+
var builderDSL = renderAsBuilderDSL(ast: ast)
if builderDSL.last == "\n" {
builderDSL = String(builderDSL.dropLast())
diff --git a/Sources/DSLConverter/Main.swift b/Sources/DSLConverter/Main.swift
index fa2079c..5c318f4 100644
--- a/Sources/DSLConverter/Main.swift
+++ b/Sources/DSLConverter/Main.swift
@@ -14,8 +14,34 @@ struct Main {
let data = try JSONEncoder().encode(builderDSL)
print(String(data: data, encoding: .utf8) ?? "")
+
+ if let diagnostics = converter.diagnostics {
+ let errors = diagnostics.diags.map {
+ let location = $0.location
+ let (start, end) = (location.start, location.end)
+
+ let behavior = switch $0.behavior {
+ case .fatalError:
+ "Fatal Error"
+ case .error:
+ "Error"
+ case .warning:
+ "Warning"
+ }
+ return LocatedMessage(
+ behavior: behavior,
+ message: $0.message,
+ location: Location(
+ start: start.utf16Offset(in: pattern), end: end.utf16Offset(in: pattern)
+ )
+ )
+ }
+
+ let data = try JSONEncoder().encode(errors)
+ print(String(data: data, encoding: .utf8) ?? "", to: &standardError)
+ }
} catch {
- print("\(error)", to:&standardError)
+ print("\(error)", to: &standardError)
}
}
}
@@ -28,3 +54,14 @@ extension FileHandle: @retroactive TextOutputStream {
self.write(data)
}
}
+
+struct Location: Codable {
+ let start: Int
+ let end: Int
+}
+
+struct LocatedMessage: Codable {
+ let behavior: String
+ let message: String
+ let location: Location
+}
diff --git a/Sources/ExpressionParser/Main.swift b/Sources/ExpressionParser/Main.swift
index 8550d72..fb41d74 100644
--- a/Sources/ExpressionParser/Main.swift
+++ b/Sources/ExpressionParser/Main.swift
@@ -12,15 +12,37 @@ struct Main {
var parser = ExpressionParser(pattern: pattern, matchingOptions: matchingOptions)
parser.parse()
- let data = try JSONEncoder().encode(parser.tokens)
+ let encoder = JSONEncoder()
+ let data = try encoder.encode(parser.tokens)
print(String(data: data, encoding: .utf8) ?? "")
+
if let diagnostics = parser.diagnostics {
- for diag in diagnostics.diags {
- print("\(diag.message)", to:&standardError)
+ let errors = diagnostics.diags.map {
+ let location = $0.location
+ let (start, end) = (location.start, location.end)
+
+ let behavior = switch $0.behavior {
+ case .fatalError:
+ "Fatal Error"
+ case .error:
+ "Error"
+ case .warning:
+ "Warning"
+ }
+ return LocatedMessage(
+ behavior: behavior,
+ message: $0.message,
+ location: Location(
+ start: start.utf16Offset(in: pattern), end: end.utf16Offset(in: pattern)
+ )
+ )
}
+
+ let data = try JSONEncoder().encode(errors)
+ print(String(data: data, encoding: .utf8) ?? "", to: &standardError)
}
} catch {
- print("\(error)", to:&standardError)
+ print("\(error)", to: &standardError)
}
}
}
@@ -33,3 +55,9 @@ extension FileHandle: @retroactive TextOutputStream {
self.write(data)
}
}
+
+struct LocatedMessage: Codable {
+ let behavior: String
+ let message: String
+ let location: Location
+}
diff --git a/Sources/Matcher/Main.swift b/Sources/Matcher/Main.swift
index 7c17232..1947aa6 100644
--- a/Sources/Matcher/Main.swift
+++ b/Sources/Matcher/Main.swift
@@ -22,7 +22,7 @@ struct Main {
let data = try JSONEncoder().encode(matches)
print(String(data: data, encoding: .utf8) ?? "")
} catch {
- print("\(error)", to:&standardError)
+ print("\(error)", to: &standardError)
}
}
}
diff --git a/Tests/RegexTests/ConverterTests.swift b/Tests/RegexTests/ConverterTests.swift
index 905bea4..20a80cf 100644
--- a/Tests/RegexTests/ConverterTests.swift
+++ b/Tests/RegexTests/ConverterTests.swift
@@ -4,15 +4,22 @@ import XCTest
class ConverterTests: XCTestCase {
func testConvertPattern() throws {
+// do {
+// let converter = DSLConverter()
+// let builderDSL = try converter.convert(#"gray|grey"#)
+// print(builderDSL)
+// }
+// do {
+// let converter = DSLConverter()
+// let builderDSL = try converter.convert(#"\b(?:[a-eg-z]|f(?!oo))\w*\b"#)
+// print(builderDSL)
+// }
do {
let converter = DSLConverter()
- let builderDSL = try converter.convert(#"gray|grey"#)
- print(builderDSL)
- }
- do {
- let converter = DSLConverter()
- let builderDSL = try converter.convert(#"\b(?:[a-eg-z]|f(?!oo))\w*\b"#)
+ let builderDSL = try converter.convert(#"\K\K"#)
print(builderDSL)
+ } catch {
+ print(error)
}
}
}
diff --git a/Tests/RegexTests/ExpressionParserTests.swift b/Tests/RegexTests/ExpressionParserTests.swift
index d1b480c..0629865 100644
--- a/Tests/RegexTests/ExpressionParserTests.swift
+++ b/Tests/RegexTests/ExpressionParserTests.swift
@@ -4,188 +4,189 @@ import XCTest
class ParserTests: XCTestCase {
func testParseExpression() {
+ let options: [String] = []
do {
- var parser = ExpressionParser(pattern: #"a(?R)?b"#)
+ var parser = ExpressionParser(pattern: #"a(?R)?b"#, matchingOptions: options)
parser.parse()
print(parser.tokens)
}
do {
- var parser = ExpressionParser(pattern: #"\d+(?(?=regex)then|else(?(?=regex)then|else))(a)^(START)?\d+(?(1)END|\b)"#)
+ var parser = ExpressionParser(pattern: #"\d+(?(?=regex)then|else(?(?=regex)then|else))(a)^(START)?\d+(?(1)END|\b)"#, matchingOptions: options)
parser.parse()
print(parser.tokens)
}
do {
- var parser = ExpressionParser(pattern: #"^[^<>]*(((?'Open'<)[^<>]*)+((?'Close-Open'>)[^<>]*)+)*(?(Open)(?!))$"#)
+ var parser = ExpressionParser(pattern: #"^[^<>]*(((?'Open'<)[^<>]*)+((?'Close-Open'>)[^<>]*)+)*(?(Open)(?!))$"#, matchingOptions: options)
parser.parse()
print(parser.tokens)
}
do {
- var parser = ExpressionParser(pattern: #"hello"#)
+ var parser = ExpressionParser(pattern: #"hello"#, matchingOptions: options)
parser.parse()
print(parser.tokens)
}
do {
- var parser = ExpressionParser(pattern: #"gray|grey"#)
+ var parser = ExpressionParser(pattern: #"gray|grey"#, matchingOptions: options)
parser.parse()
print(parser.tokens)
}
do {
- var parser = ExpressionParser(pattern: #"gr(a|e)y"#)
+ var parser = ExpressionParser(pattern: #"gr(a|e)y"#, matchingOptions: options)
parser.parse()
print(parser.tokens)
}
do {
- var parser = ExpressionParser(pattern: #"gr[ae]y"#)
+ var parser = ExpressionParser(pattern: #"gr[ae]y"#, matchingOptions: options)
parser.parse()
print(parser.tokens)
}
do {
- var parser = ExpressionParser(pattern: #"colou?r"#)
+ var parser = ExpressionParser(pattern: #"colou?r"#, matchingOptions: options)
parser.parse()
print(parser.tokens)
}
do {
- var parser = ExpressionParser(pattern: #"rege(x(es)?|xps?)"#)
+ var parser = ExpressionParser(pattern: #"rege(x(es)?|xps?)"#, matchingOptions: options)
parser.parse()
print(parser.tokens)
}
do {
- var parser = ExpressionParser(pattern: #"go*gle"#)
+ var parser = ExpressionParser(pattern: #"go*gle"#, matchingOptions: options)
parser.parse()
print(parser.tokens)
}
do {
- var parser = ExpressionParser(pattern: #"go+gle"#)
+ var parser = ExpressionParser(pattern: #"go+gle"#, matchingOptions: options)
parser.parse()
print(parser.tokens)
}
do {
- var parser = ExpressionParser(pattern: #"g(oog)+le"#)
+ var parser = ExpressionParser(pattern: #"g(oog)+le"#, matchingOptions: options)
parser.parse()
print(parser.tokens)
}
do {
- var parser = ExpressionParser(pattern: #"z{3}"#)
+ var parser = ExpressionParser(pattern: #"z{3}"#, matchingOptions: options)
parser.parse()
print(parser.tokens)
}
do {
- var parser = ExpressionParser(pattern: #"z{3,6}"#)
+ var parser = ExpressionParser(pattern: #"z{3,6}"#, matchingOptions: options)
parser.parse()
print(parser.tokens)
}
do {
- var parser = ExpressionParser(pattern: #"z{3,}"#)
+ var parser = ExpressionParser(pattern: #"z{3,}"#, matchingOptions: options)
parser.parse()
print(parser.tokens)
}
do {
- var parser = ExpressionParser(pattern: #"[Bb]rainf\*\*k"#)
+ var parser = ExpressionParser(pattern: #"[Bb]rainf\*\*k"#, matchingOptions: options)
parser.parse()
print(parser.tokens)
}
do {
- var parser = ExpressionParser(pattern: #"\d"#)
+ var parser = ExpressionParser(pattern: #"\d"#, matchingOptions: options)
parser.parse()
print(parser.tokens)
}
do {
- var parser = ExpressionParser(pattern: #"\d+"#)
+ var parser = ExpressionParser(pattern: #"\d+"#, matchingOptions: options)
parser.parse()
print(parser.tokens)
}
do {
- var parser = ExpressionParser(pattern: #"\d{5}(-\d{4})?"#)
+ var parser = ExpressionParser(pattern: #"\d{5}(-\d{4})?"#, matchingOptions: options)
parser.parse()
print(parser.tokens)
}
do {
- var parser = ExpressionParser(pattern: #"1\d{10}"#)
+ var parser = ExpressionParser(pattern: #"1\d{10}"#, matchingOptions: options)
parser.parse()
print(parser.tokens)
}
do {
- var parser = ExpressionParser(pattern: #"[2-9]|[12]\d|3[0-6]"#)
+ var parser = ExpressionParser(pattern: #"[2-9]|[12]\d|3[0-6]"#, matchingOptions: options)
parser.parse()
print(parser.tokens)
}
do {
- var parser = ExpressionParser(pattern: #"Hello\nworld"#)
+ var parser = ExpressionParser(pattern: #"Hello\nworld"#, matchingOptions: options)
parser.parse()
print(parser.tokens)
}
do {
- var parser = ExpressionParser(pattern: #"mi.....ft"#)
+ var parser = ExpressionParser(pattern: #"mi.....ft"#, matchingOptions: options)
parser.parse()
print(parser.tokens)
}
do {
- var parser = ExpressionParser(pattern: #"\d+(\.\d\d)?"#)
+ var parser = ExpressionParser(pattern: #"\d+(\.\d\d)?"#, matchingOptions: options)
parser.parse()
print(parser.tokens)
}
do {
- var parser = ExpressionParser(pattern: #"[^i*&2@]"#)
+ var parser = ExpressionParser(pattern: #"[^i*&2@]"#, matchingOptions: options)
parser.parse()
print(parser.tokens)
}
do {
- var parser = ExpressionParser(pattern: #"//[^\r\n]*[\r\n]"#)
+ var parser = ExpressionParser(pattern: #"//[^\r\n]*[\r\n]"#, matchingOptions: options)
parser.parse()
print(parser.tokens)
}
do {
- var parser = ExpressionParser(pattern: #"^dog"#)
+ var parser = ExpressionParser(pattern: #"^dog"#, matchingOptions: options)
parser.parse()
print(parser.tokens)
}
do {
- var parser = ExpressionParser(pattern: #"dog$"#)
+ var parser = ExpressionParser(pattern: #"dog$"#, matchingOptions: options)
parser.parse()
print(parser.tokens)
}
do {
- var parser = ExpressionParser(pattern: #"^dog$"#)
+ var parser = ExpressionParser(pattern: #"^dog$"#, matchingOptions: options)
parser.parse()
print(parser.tokens)
}
do {
- var parser = ExpressionParser(pattern: #"\w++\d\d\w+"#)
+ var parser = ExpressionParser(pattern: #"\w++\d\d\w+"#, matchingOptions: options)
parser.parse()
print(parser.tokens)
}
do {
- var parser = ExpressionParser(pattern: #"<(\w+)>[^<]*\1>"#)
+ var parser = ExpressionParser(pattern: #"<(\w+)>[^<]*\1>"#, matchingOptions: options)
parser.parse()
print(parser.tokens)
}
do {
- var parser = ExpressionParser(pattern: #"Hillary(?=\s+Clinton)"#)
+ var parser = ExpressionParser(pattern: #"Hillary(?=\s+Clinton)"#, matchingOptions: options)
parser.parse()
print(parser.tokens)
}
do {
- var parser = ExpressionParser(pattern: #"q(?!u)"#)
+ var parser = ExpressionParser(pattern: #"q(?!u)"#, matchingOptions: options)
parser.parse()
print(parser.tokens)
}
do {
- var parser = ExpressionParser(pattern: #"(?<=-)\p{L}+"#)
+ var parser = ExpressionParser(pattern: #"(?<=-)\p{L}+"#, matchingOptions: options)
parser.parse()
print(parser.tokens)
}
do {
- var parser = ExpressionParser(pattern: #"[\x41-\x45]{3}"#)
+ var parser = ExpressionParser(pattern: #"[\x41-\x45]{3}"#, matchingOptions: options)
parser.parse()
print(parser.tokens)
}
do {
- var parser = ExpressionParser(pattern: #"(?(?=regex)then|else)"#)
+ var parser = ExpressionParser(pattern: #"(?(?=regex)then|else)"#, matchingOptions: options)
parser.parse()
print(parser.tokens)
}
do {
- var parser = ExpressionParser(pattern: #"(?\w+)\W+(?<-word>\w+)"#)
+ var parser = ExpressionParser(pattern: #"(?\w+)\W+(?<-word>\w+)"#, matchingOptions: options)
parser.parse()
print(parser.tokens)
}