/*
* Copyright (c) Mike Lischke. All rights reserved.
* Licensed under the BSD 3-clause License. See License.txt in the project root for license information.
*/
lexer grammar ActionSplitter;
// $antlr-format alignTrailingComments on, columnLimit 150, maxEmptyLinesToKeep 1, reflowComments off, useTab off
// $antlr-format allowShortRulesOnASingleLine on, alignSemicolons none, minEmptyLines 0
@header {/* eslint-disable */
import type { IActionSplitterListener } from "../parse/IActionSplitterListener.js";
import { Character } from "../support/Character.js";
const attrValueExpr = /[^=][^;]*/;
const id = /[a-zA-Z_][a-zA-Z0-9_]*/;
const setNonLocalAttr = new RegExp(`\\$(?<x>${id.source})::(?<y>${id.source})(\\s)?=(\\s)?(?<expr>${attrValueExpr.source});`);
const nonLocalAttr = new RegExp(`\\$(?<x>${id.source})::(?<y>${id.source})`);
const qualifiedAttr = new RegExp(`\\$(?<x>${id.source}).(?<y>${id.source})`);
const setAttr = new RegExp(`\\$(?<x>${id.source})(?<s1>\\s)?=(?<s2>\\s)?(?<expr>${attrValueExpr.source});`);
const attr = new RegExp(`\\$(?<x>${id.source})`);
}
@members {/** Force filtering (and return tokens). Sends token values to the delegate. */
public getActionTokens(delegate: IActionSplitterListener, refToken?: Token): Token[] {
const tokens = this.getAllTokens();
for (let i = 0; i < tokens.length; i++) {
const t = tokens[i];
switch (t.type) {
case ActionSplitter.COMMENT:
case ActionSplitter.LINE_COMMENT:
case ActionSplitter.TEXT: {
const text = t.text!;
let result = "";
for (let i = 0; i < text.length; i++) {
switch (text[i]) {
case "\\": {
if (i + 1 >= text.length) {
result += "\\";
break;
}
if (text[i + 1] === "$") {
result += "$";
i++;
} else {
result += text[i++];
result += text[i];
}
break;
}
case "$": {
if (i + 1 >= text.length) {
result += "$";
break;
}
const c = text[i + 1];
if (!(c == '_' || Character.isLetter(c.codePointAt(0)!))) {
result += "$";
}
break;
}
default: {
result += text[i];
break;
}
}
}
delegate.text(result);
break;
}
case ActionSplitter.SET_NONLOCAL_ATTR: {
const text = t.text!;
// Parse the text and extract the named groups from the match result.
const match = text.match(setNonLocalAttr);
if (match === null) {
throw new Error(`Mismatched input '${text}'`);
}
const { x, y, expr } = match.groups!;
const [xToken, yToken] = this.createTokens(t, x.length + 2, refToken); // +2 for "::"
xToken.text = x;
yToken!.text = y;
delegate.setNonLocalAttr(text, xToken, yToken!, expr);
break;
}
case ActionSplitter.NONLOCAL_ATTR: {
const text = t.text!;
const match = text.match(nonLocalAttr);
if (match === null) {
throw new Error(`Mismatched input '${text}'`);
}
const { x, y } = match.groups!;
const [xToken, yToken] = this.createTokens(t, x.length + 2, refToken);
xToken.text = x;
yToken!.text = y;
delegate.nonLocalAttr(text, xToken, yToken!);
break;
}
case ActionSplitter.QUALIFIED_ATTR: {
let text = t.text!;
const match = text.match(qualifiedAttr);
if (match === null) {
throw new Error(`Mismatched input '${text}'`);
}
const { x, y } = match.groups!;
const [xToken, yToken] = this.createTokens(t, x.length + 1, refToken);
xToken.text = x;
yToken!.text = y;
// In the ANTLR3 version of the grammar, QUALIFIED_ATTR was not matched if followed by a '('.
// We have to simulate this behavior here.
if (i + 1 < tokens.length && tokens[i + 1].text?.startsWith("(")) {
// Pretend we matched ATTR instead of QUALIFIED_ATTR, with the first part of the qualified name.
delegate.attr("$" + x, xToken);
// Pretend we matched TEXT instead of QUALIFIED_ATTR, with the rest of the qualified name.
text = "." + y + tokens[++i].text!;
delegate.text(text);
break;
}
delegate.qualifiedAttr(text, xToken, yToken!);
break;
}
case ActionSplitter.SET_ATTR: {
const text = t.text!;
const match = text.match(setAttr);
if (match === null) {
throw new Error(`Mismatched input '${text}'`);
}
const { x, s1 = "", s2 = "", expr } = match.groups!;
// +2 for the second '$' and the '='.
const [xToken, yToken] = this.createTokens(t, x.length + s1.length + s2.length + 2, refToken);
xToken.text = x;
yToken!.text = expr;
delegate.setAttr(text, xToken, yToken!);
break;
}
case ActionSplitter.ATTR: {
const text = t.text!;
const match = text.match(attr);
if (match === null) {
throw new Error(`Mismatched input '${text}'`);
}
const { x } = match.groups!;
const [xToken] = this.createTokens(t, undefined, refToken);
xToken.text = x;
delegate.attr(text, xToken);
break;
}
default:
}
}
// TODO: need to remove the EOF token from the list of tokens?
return tokens;
}
private createTokens(sourceToken: Token, offset?: number, refToken?: Token): [Token, Token | undefined] {
let line = 1;
let column = 1; // 1 for the leading $
if (refToken) {
// If this token starts on the first line, take the ref token's column into account.
if (sourceToken.line === 1) {
column = refToken.column;
}
line = refToken.line;
}
column += sourceToken.column;
line += sourceToken.line - 1;
const xToken = antlr.CommonToken.fromToken(sourceToken);
xToken.line = line;
xToken.column = column;
let yToken: Token | undefined;
if (offset !== undefined) {
yToken = antlr.CommonToken.fromToken(sourceToken);
yToken.line = line;
yToken.column = column + offset;
}
return [xToken, yToken];
}}
// ignore comments right away
COMMENT: '/*' .*? '*/';
LINE_COMMENT: '//' ~('\n' | '\r')* '\r'? '\n';
SET_NONLOCAL_ATTR: '$' ID '::' ID WS? '=' ATTR_VALUE_EXPR ';';
NONLOCAL_ATTR: '$' ID '::' ID;
QUALIFIED_ATTR: '$' ID '.' ID;
SET_ATTR: '$' ID WS? '=' ATTR_VALUE_EXPR ';';
ATTR: '$' ID;
// Anything else is just random text
TEXT: (~'$' | '\\$' | '$' ~[a-zA-Z_])+;
fragment ID: ('a' ..'z' | 'A' ..'Z' | '_') ('a' ..'z' | 'A' ..'Z' | '0' ..'9' | '_')*;
/** Don't allow an = as first char to prevent $x == 3; kind of stuff. */
fragment ATTR_VALUE_EXPR: ~'=' (~';')*;
fragment WS: (' ' | '\t' | '\n' | '\r')+;