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+)>[^<]*"#) + var parser = ExpressionParser(pattern: #"<(\w+)>[^<]*"#, 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) }