diff --git a/org.eclipse.tm4e.core/src/main/java/org/eclipse/tm4e/core/internal/oniguruma/OnigRegExp.java b/org.eclipse.tm4e.core/src/main/java/org/eclipse/tm4e/core/internal/oniguruma/OnigRegExp.java index 96f11a6fc..90caeaf2a 100644 --- a/org.eclipse.tm4e.core/src/main/java/org/eclipse/tm4e/core/internal/oniguruma/OnigRegExp.java +++ b/org.eclipse.tm4e.core/src/main/java/org/eclipse/tm4e/core/internal/oniguruma/OnigRegExp.java @@ -94,7 +94,7 @@ private Regex parsePattern(final String pattern, final boolean ignoreCase) throw } /** - * Rewrites the given pattern to work around limitations of the Joni library, which does not support variable-length lookbehinds. + * Rewrites the given pattern to work around lookbehind limitations of the Joni library. * * Strategy: * * * @see github.com/eclipse-tm4e/tm4e/issues/677 + * @see github.com/eclipse-tm4e/tm4e/issues/1027 */ private String rewritePatternIfRequired(final String pattern) { if (pattern.isEmpty()) @@ -144,7 +145,12 @@ private String rewritePatternIfRequired(final String pattern) { if (pattern.startsWith(negLB)) return "(? (?!(?<=_)\1\w)(\1) + // Joni rejects backreferences inside lookbehinds. Test Markdown's closing delimiter before consuming it so the + // backreference stays outside the lookbehind and the original capture groups and boundary semantics are preserved. + // Keep this rewrite narrow because arbitrary backreference lookbehinds cannot be moved without changing their meaning. + return pattern.replace("(\\1)(?!(?<=_\\1)\\w)", "(?!(?<=_)\\1\\w)(\\1)"); } /** diff --git a/org.eclipse.tm4e.core/src/test/java/org/eclipse/tm4e/core/internal/oniguruma/OnigRegExpTest.java b/org.eclipse.tm4e.core/src/test/java/org/eclipse/tm4e/core/internal/oniguruma/OnigRegExpTest.java index f14d59f2f..83bf4272f 100644 --- a/org.eclipse.tm4e.core/src/test/java/org/eclipse/tm4e/core/internal/oniguruma/OnigRegExpTest.java +++ b/org.eclipse.tm4e.core/src/test/java/org/eclipse/tm4e/core/internal/oniguruma/OnigRegExpTest.java @@ -98,4 +98,22 @@ void testNegativeLookBehinds() { assertOnigRegExpSearch("(?<=\\s*\\.)\\w+", ".foo", 0, true, ".foo"); assertOnigRegExpSearch("(?<=\\s*\\.)\\w+", " .foo", 0, true, " .foo"); } + + @Test + void testMarkdownBackReferenceInLookBehind() { + // Keep the exact Markdown strikethrough pattern so this test exercises the same Joni compatibility path as the grammar. + final var pattern = + "(? patterns) { @@ -224,10 +234,7 @@ private void assertParseablePatterns(final @Nullable Collection patter return; for (final var rule : patterns) { - assertParseablePattern(rule.getBegin()); - assertParseablePattern(rule.getEnd()); - assertParseablePattern(rule.getMatch()); - assertParseablePattern(rule.getWhile()); + assertParseableRule(rule); assertParseablePatterns(rule.getPatterns()); } } @@ -254,6 +261,10 @@ public FileVisitResult visitFile(final Path file, final BasicFileAttributes attr final var patterns = castNonNull(rawGrammar.getPatterns()); assertThat(patterns).isNotEmpty(); assertParseablePatterns(patterns); + // TextMate include rules reference named repository entries by string, so walking + // getPatterns() never reaches their regex fields. Keep this check to the named rule; + // recursively expanding repository subtrees exposes separate existing incompatibilities. + rawGrammar.getRepository().putEntries((name, rule) -> assertParseableRule(rule)); final var reg = new Registry(); final var grammar = reg.addGrammar(IGrammarSource.fromFile(file));