mirror of
https://github.com/exogen/t2-mapper.git
synced 2026-02-14 04:03:49 +00:00
add TorqueScript transpiler and runtime
This commit is contained in:
parent
c8391a1056
commit
7d10fb7dee
49 changed files with 12324 additions and 2075 deletions
659
TorqueScript.pegjs
Normal file
659
TorqueScript.pegjs
Normal file
|
|
@ -0,0 +1,659 @@
|
|||
{{
|
||||
// Collect exec() script paths during parsing (deduplicated)
|
||||
const execScriptPathsSet = new Set();
|
||||
let hasDynamicExec = false;
|
||||
|
||||
function buildBinaryExpression(head, tail) {
|
||||
return tail.reduce((left, [op, right]) => ({
|
||||
type: 'BinaryExpression',
|
||||
operator: op,
|
||||
left,
|
||||
right
|
||||
}), head);
|
||||
}
|
||||
|
||||
function buildUnaryExpression(operator, argument) {
|
||||
return {
|
||||
type: 'UnaryExpression',
|
||||
operator,
|
||||
argument
|
||||
};
|
||||
}
|
||||
|
||||
function buildCallExpression(callee, args) {
|
||||
// Check if this is an exec() call
|
||||
if (callee.type === 'Identifier' && callee.name.toLowerCase() === 'exec') {
|
||||
if (args.length > 0 && args[0].type === 'StringLiteral') {
|
||||
execScriptPathsSet.add(args[0].value);
|
||||
} else {
|
||||
hasDynamicExec = true;
|
||||
}
|
||||
}
|
||||
return {
|
||||
type: 'CallExpression',
|
||||
callee,
|
||||
arguments: args
|
||||
};
|
||||
}
|
||||
|
||||
function getExecScriptPaths() {
|
||||
return Array.from(execScriptPathsSet);
|
||||
}
|
||||
}}
|
||||
|
||||
// Main entry point
|
||||
Program
|
||||
= ws items:((Comment / Statement) ws)* {
|
||||
return {
|
||||
type: 'Program',
|
||||
body: items.map(([item]) => item).filter(Boolean),
|
||||
execScriptPaths: getExecScriptPaths(),
|
||||
hasDynamicExec
|
||||
};
|
||||
}
|
||||
|
||||
// Statements
|
||||
Statement
|
||||
= PackageDeclaration
|
||||
/ FunctionDeclaration
|
||||
/ DatablockStatement
|
||||
/ ObjectStatement
|
||||
/ IfStatement
|
||||
/ ForStatement
|
||||
/ DoWhileStatement
|
||||
/ WhileStatement
|
||||
/ SwitchStatement
|
||||
/ ReturnStatement
|
||||
/ BreakStatement
|
||||
/ ContinueStatement
|
||||
/ ExpressionStatement
|
||||
/ BlockStatement
|
||||
/ Comment
|
||||
/ _ ";" _ { return null; }
|
||||
|
||||
DatablockStatement
|
||||
= decl:DatablockDeclaration _ ";"? _ { return decl; }
|
||||
|
||||
ObjectStatement
|
||||
= decl:ObjectDeclaration _ ";"? _ { return decl; }
|
||||
|
||||
PackageDeclaration
|
||||
= "package" __ name:Identifier _ "{" ws items:((Comment / Statement) ws)* "}" _ ";"? {
|
||||
return {
|
||||
type: 'PackageDeclaration',
|
||||
name,
|
||||
body: items.map(([item]) => item).filter(Boolean)
|
||||
};
|
||||
}
|
||||
|
||||
FunctionDeclaration
|
||||
= "function" __ name:FunctionName _ "(" _ params:ParameterList? _ ")" _ body:BlockStatement {
|
||||
return {
|
||||
type: 'FunctionDeclaration',
|
||||
name,
|
||||
params: params || [],
|
||||
body
|
||||
};
|
||||
}
|
||||
|
||||
FunctionName
|
||||
= namespace:Identifier "::" method:Identifier {
|
||||
return { type: 'MethodName', namespace, method };
|
||||
}
|
||||
/ Identifier
|
||||
|
||||
ParameterList
|
||||
= head:Identifier tail:(_ "," _ Identifier)* {
|
||||
return [head, ...tail.map(([,,,id]) => id)];
|
||||
}
|
||||
|
||||
DatablockDeclaration
|
||||
= "datablock" __ className:Identifier _ "(" _ instanceName:ObjectName? _ ")" _ parent:(":" _ Identifier)? _ body:("{" _ ObjectBody* _ "}" _)? {
|
||||
return {
|
||||
type: 'DatablockDeclaration',
|
||||
className,
|
||||
instanceName,
|
||||
parent: parent ? parent[2] : null,
|
||||
body: body ? body[2].filter(Boolean) : []
|
||||
};
|
||||
}
|
||||
|
||||
ObjectDeclaration
|
||||
= "new" __ className:ClassNameExpression _ "(" _ instanceName:ObjectName? _ ")" _ body:("{" _ ObjectBody* _ "}" _)? {
|
||||
return {
|
||||
type: 'ObjectDeclaration',
|
||||
className,
|
||||
instanceName,
|
||||
body: body ? body[2].filter(Boolean) : []
|
||||
};
|
||||
}
|
||||
|
||||
ClassNameExpression
|
||||
= "(" _ expr:Expression _ ")" { return expr; }
|
||||
/ base:Identifier accessors:(_ "[" _ IndexList _ "]")* {
|
||||
return accessors.reduce((obj, [,,,indices]) => ({
|
||||
type: 'IndexExpression',
|
||||
object: obj,
|
||||
index: indices
|
||||
}), base);
|
||||
}
|
||||
|
||||
ObjectBody
|
||||
= obj:ObjectDeclaration _ ";"? _ { return obj; }
|
||||
/ db:DatablockDeclaration _ ";"? _ { return db; }
|
||||
/ Assignment
|
||||
/ Comment
|
||||
/ Whitespace
|
||||
|
||||
ObjectName
|
||||
= StringConcatExpression
|
||||
/ Identifier
|
||||
/ NumberLiteral
|
||||
|
||||
Assignment
|
||||
= _ target:LeftHandSide _ "=" _ value:Expression _ ";"? _ {
|
||||
return {
|
||||
type: 'Assignment',
|
||||
target,
|
||||
value
|
||||
};
|
||||
}
|
||||
|
||||
LeftHandSide
|
||||
= base:CallExpression accessors:Accessor* {
|
||||
return accessors.reduce((obj, accessor) => {
|
||||
if (accessor.type === 'property') {
|
||||
return {
|
||||
type: 'MemberExpression',
|
||||
object: obj,
|
||||
property: accessor.value
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
type: 'IndexExpression',
|
||||
object: obj,
|
||||
index: accessor.value
|
||||
};
|
||||
}
|
||||
}, base);
|
||||
}
|
||||
|
||||
Accessor
|
||||
= "." _ property:Identifier { return { type: 'property', value: property }; }
|
||||
/ "[" _ indices:IndexList _ "]" { return { type: 'index', value: indices }; }
|
||||
|
||||
IndexList
|
||||
= head:Expression tail:(_ "," _ Expression)* {
|
||||
return tail.length > 0 ? [head, ...tail.map(([,,,expr]) => expr)] : head;
|
||||
}
|
||||
|
||||
IfStatement
|
||||
= "if" _ "(" _ test:Expression _ ")" _ consequent:Statement alternate:(_ "else" _ Statement)? {
|
||||
return {
|
||||
type: 'IfStatement',
|
||||
test,
|
||||
consequent,
|
||||
alternate: alternate ? alternate[3] : null
|
||||
};
|
||||
}
|
||||
|
||||
ForStatement
|
||||
= "for" _ "(" _ init:Expression? _ ";" _ test:Expression? _ ";" _ update:Expression? _ ")" _ body:Statement {
|
||||
return {
|
||||
type: 'ForStatement',
|
||||
init,
|
||||
test,
|
||||
update,
|
||||
body
|
||||
};
|
||||
}
|
||||
|
||||
WhileStatement
|
||||
= "while" _ "(" _ test:Expression _ ")" _ body:Statement {
|
||||
return {
|
||||
type: 'WhileStatement',
|
||||
test,
|
||||
body
|
||||
};
|
||||
}
|
||||
|
||||
DoWhileStatement
|
||||
= "do" _ body:Statement _ "while" _ "(" _ test:Expression _ ")" _ ";"? {
|
||||
return {
|
||||
type: 'DoWhileStatement',
|
||||
test,
|
||||
body
|
||||
};
|
||||
}
|
||||
|
||||
SwitchStatement
|
||||
= "switch$" _ "(" _ discriminant:Expression _ ")" _ "{" ws items:((Comment / SwitchCase) ws)* "}" {
|
||||
return {
|
||||
type: 'SwitchStatement',
|
||||
stringMode: true,
|
||||
discriminant,
|
||||
cases: items.map(([item]) => item).filter(i => i && i.type === 'SwitchCase')
|
||||
};
|
||||
}
|
||||
/ "switch" _ "(" _ discriminant:Expression _ ")" _ "{" ws items:((Comment / SwitchCase) ws)* "}" {
|
||||
return {
|
||||
type: 'SwitchStatement',
|
||||
stringMode: false,
|
||||
discriminant,
|
||||
cases: items.map(([item]) => item).filter(i => i && i.type === 'SwitchCase')
|
||||
};
|
||||
}
|
||||
|
||||
SwitchCase
|
||||
= "case" __ tests:CaseTestList _ ":" ws items:((Comment / Statement) ws)* {
|
||||
return {
|
||||
type: 'SwitchCase',
|
||||
test: tests,
|
||||
consequent: items.map(([item]) => item).filter(Boolean)
|
||||
};
|
||||
}
|
||||
/ "default" _ ":" ws items:((Comment / Statement) ws)* {
|
||||
return {
|
||||
type: 'SwitchCase',
|
||||
test: null,
|
||||
consequent: items.map(([item]) => item).filter(Boolean)
|
||||
};
|
||||
}
|
||||
|
||||
CaseTestList
|
||||
= head:CaseTestExpression tail:(_ "or" __ CaseTestExpression)* {
|
||||
return tail.length > 0 ? [head, ...tail.map(([,,,expr]) => expr)] : head;
|
||||
}
|
||||
|
||||
CaseTestExpression
|
||||
= AdditiveExpression
|
||||
|
||||
ReturnStatement
|
||||
= "return" value:(__ Expression)? _ ";" {
|
||||
return {
|
||||
type: 'ReturnStatement',
|
||||
value: value ? value[1] : null
|
||||
};
|
||||
}
|
||||
|
||||
BreakStatement
|
||||
= "break" _ ";" {
|
||||
return { type: 'BreakStatement' };
|
||||
}
|
||||
|
||||
ContinueStatement
|
||||
= "continue" _ ";" {
|
||||
return { type: 'ContinueStatement' };
|
||||
}
|
||||
|
||||
ExpressionStatement
|
||||
= expr:Expression _ ";" {
|
||||
return {
|
||||
type: 'ExpressionStatement',
|
||||
expression: expr
|
||||
};
|
||||
}
|
||||
|
||||
BlockStatement
|
||||
= "{" ws items:((Comment / Statement) ws)* "}" {
|
||||
return {
|
||||
type: 'BlockStatement',
|
||||
body: items.map(([item]) => item).filter(Boolean)
|
||||
};
|
||||
}
|
||||
|
||||
// Expressions (in order of precedence)
|
||||
Expression
|
||||
= AssignmentExpression
|
||||
|
||||
AssignmentExpression
|
||||
= target:LeftHandSide _ operator:AssignmentOperator _ value:AssignmentExpression {
|
||||
return {
|
||||
type: 'AssignmentExpression',
|
||||
operator,
|
||||
target,
|
||||
value
|
||||
};
|
||||
}
|
||||
/ ConditionalExpression
|
||||
|
||||
AssignmentOperator
|
||||
= "=" / "+=" / "-=" / "*=" / "/=" / "%=" / "<<=" / ">>=" / "&=" / "|=" / "^="
|
||||
|
||||
ConditionalExpression
|
||||
= test:LogicalOrExpression _ "?" _ consequent:Expression _ ":" _ alternate:Expression {
|
||||
return {
|
||||
type: 'ConditionalExpression',
|
||||
test,
|
||||
consequent,
|
||||
alternate
|
||||
};
|
||||
}
|
||||
/ LogicalOrExpression
|
||||
|
||||
LogicalOrExpression
|
||||
= head:LogicalAndExpression tail:(_ "||" _ LogicalAndExpression)* {
|
||||
return buildBinaryExpression(head, tail.map(([,op,,right]) => [op, right]));
|
||||
}
|
||||
|
||||
LogicalAndExpression
|
||||
= head:BitwiseOrExpression tail:(_ "&&" _ BitwiseOrExpression)* {
|
||||
return buildBinaryExpression(head, tail.map(([,op,,right]) => [op, right]));
|
||||
}
|
||||
|
||||
BitwiseOrExpression
|
||||
= head:BitwiseXorExpression tail:(_ "|" !"|" _ BitwiseXorExpression)* {
|
||||
return buildBinaryExpression(head, tail.map(([,op,,,right]) => [op, right]));
|
||||
}
|
||||
|
||||
BitwiseXorExpression
|
||||
= head:BitwiseAndExpression tail:(_ "^" _ BitwiseAndExpression)* {
|
||||
return buildBinaryExpression(head, tail.map(([,op,,right]) => [op, right]));
|
||||
}
|
||||
|
||||
BitwiseAndExpression
|
||||
= head:EqualityExpression tail:(_ "&" !"&" _ EqualityExpression)* {
|
||||
return buildBinaryExpression(head, tail.map(([,op,,,right]) => [op, right]));
|
||||
}
|
||||
|
||||
EqualityExpression
|
||||
= head:RelationalExpression tail:(_ EqualityOperator _ RelationalExpression)* {
|
||||
return buildBinaryExpression(head, tail.map(([,op,,right]) => [op, right]));
|
||||
}
|
||||
|
||||
EqualityOperator
|
||||
= "==" / "!="
|
||||
|
||||
RelationalExpression
|
||||
= head:StringConcatExpression tail:(_ RelationalOperator _ StringConcatExpression)* {
|
||||
return buildBinaryExpression(head, tail.map(([,op,,right]) => [op, right]));
|
||||
}
|
||||
|
||||
RelationalOperator
|
||||
= "<=" / ">=" / "<" / ">"
|
||||
|
||||
StringConcatExpression
|
||||
= head:ShiftExpression tail:(_ StringConcatOperator _ StringConcatRightSide)* {
|
||||
return buildBinaryExpression(head, tail.map(([,op,,right]) => [op, right]));
|
||||
}
|
||||
|
||||
// Right side of string concat: allow assignments or normal expressions, but minimize backtracking
|
||||
StringConcatRightSide
|
||||
= target:LeftHandSide _ assignOp:AssignmentOperator _ value:AssignmentExpression {
|
||||
return {
|
||||
type: 'AssignmentExpression',
|
||||
operator: assignOp,
|
||||
target,
|
||||
value
|
||||
};
|
||||
}
|
||||
/ ShiftExpression
|
||||
|
||||
StringConcatOperator
|
||||
= "$=" / "!$=" / "@" / "NL" / "TAB" / "SPC"
|
||||
|
||||
ShiftExpression
|
||||
= head:AdditiveExpression tail:(_ ShiftOperator _ AdditiveExpression)* {
|
||||
return buildBinaryExpression(head, tail.map(([,op,,right]) => [op, right]));
|
||||
}
|
||||
|
||||
ShiftOperator
|
||||
= "<<" / ">>"
|
||||
|
||||
AdditiveExpression
|
||||
= head:MultiplicativeExpression tail:(_ ("+" / "-") _ MultiplicativeExpression)* {
|
||||
return buildBinaryExpression(head, tail.map(([,op,,right]) => [op, right]));
|
||||
}
|
||||
|
||||
MultiplicativeExpression
|
||||
= head:UnaryExpression tail:(_ ("*" / "/" / "%") _ UnaryExpression)* {
|
||||
return buildBinaryExpression(head, tail.map(([,op,,right]) => [op, right]));
|
||||
}
|
||||
|
||||
UnaryExpression
|
||||
= operator:("-" / "!" / "~") _ argument:AssignmentExpression {
|
||||
return buildUnaryExpression(operator, argument);
|
||||
}
|
||||
/ operator:("++" / "--") _ argument:UnaryExpression {
|
||||
return buildUnaryExpression(operator, argument);
|
||||
}
|
||||
/ "*" _ argument:UnaryExpression {
|
||||
return {
|
||||
type: 'TagDereferenceExpression',
|
||||
argument
|
||||
};
|
||||
}
|
||||
/ PostfixExpression
|
||||
|
||||
PostfixExpression
|
||||
= argument:CallExpression _ operator:("++" / "--") {
|
||||
return {
|
||||
type: 'PostfixExpression',
|
||||
operator,
|
||||
argument
|
||||
};
|
||||
}
|
||||
/ CallExpression
|
||||
|
||||
CallExpression
|
||||
= base:MemberExpression tail:((_ "(" _ ArgumentList? _ ")") / (_ Accessor))* {
|
||||
return tail.reduce((obj, item) => {
|
||||
// Check if it's a function call
|
||||
if (item[1] === '(') {
|
||||
const [,,,argList] = item;
|
||||
return buildCallExpression(obj, argList || []);
|
||||
}
|
||||
// Otherwise it's an accessor
|
||||
const accessor = item[1];
|
||||
if (accessor.type === 'property') {
|
||||
return {
|
||||
type: 'MemberExpression',
|
||||
object: obj,
|
||||
property: accessor.value
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
type: 'IndexExpression',
|
||||
object: obj,
|
||||
index: accessor.value
|
||||
};
|
||||
}
|
||||
}, base);
|
||||
}
|
||||
|
||||
MemberExpression
|
||||
= base:PrimaryExpression accessors:(_ Accessor)* {
|
||||
return accessors.reduce((obj, [, accessor]) => {
|
||||
if (accessor.type === 'property') {
|
||||
return {
|
||||
type: 'MemberExpression',
|
||||
object: obj,
|
||||
property: accessor.value
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
type: 'IndexExpression',
|
||||
object: obj,
|
||||
index: accessor.value
|
||||
};
|
||||
}
|
||||
}, base);
|
||||
}
|
||||
|
||||
ArgumentList
|
||||
= head:Expression tail:(_ "," _ Expression)* {
|
||||
return [head, ...tail.map(([,,,expr]) => expr)];
|
||||
}
|
||||
|
||||
PrimaryExpression
|
||||
= ObjectDeclaration
|
||||
/ DatablockDeclaration
|
||||
/ StringLiteral
|
||||
/ NumberLiteral
|
||||
/ BooleanLiteral
|
||||
/ Variable
|
||||
/ ParenthesizedExpression
|
||||
|
||||
ParenthesizedExpression
|
||||
= "(" _ expr:Expression _ ")" { return expr; }
|
||||
|
||||
// Variables
|
||||
Variable
|
||||
= LocalVariable
|
||||
/ GlobalVariable
|
||||
/ PlainIdentifier
|
||||
|
||||
LocalVariable
|
||||
= "%" name:$([a-zA-Z_][a-zA-Z0-9_]*) {
|
||||
return {
|
||||
type: 'Variable',
|
||||
scope: 'local',
|
||||
name
|
||||
};
|
||||
}
|
||||
|
||||
GlobalVariable
|
||||
= "$" name:$("::"? [a-zA-Z_][a-zA-Z0-9_]* ("::" [a-zA-Z_][a-zA-Z0-9_]*)*) {
|
||||
return {
|
||||
type: 'Variable',
|
||||
scope: 'global',
|
||||
name
|
||||
};
|
||||
}
|
||||
|
||||
PlainIdentifier
|
||||
= name:$("parent" [ \t]* "::" [ \t]* [a-zA-Z_][a-zA-Z0-9_]*) {
|
||||
return {
|
||||
type: 'Identifier',
|
||||
name: name.replace(/\s+/g, '')
|
||||
};
|
||||
}
|
||||
/ name:$("parent" ("::" [a-zA-Z_][a-zA-Z0-9_]*)+) {
|
||||
return {
|
||||
type: 'Identifier',
|
||||
name
|
||||
};
|
||||
}
|
||||
/ name:$([a-zA-Z_][a-zA-Z0-9_]* ("::" [a-zA-Z_][a-zA-Z0-9_]*)*) {
|
||||
return {
|
||||
type: 'Identifier',
|
||||
name
|
||||
};
|
||||
}
|
||||
|
||||
Identifier
|
||||
= LocalVariable
|
||||
/ GlobalVariable
|
||||
/ PlainIdentifier
|
||||
|
||||
// Literals
|
||||
StringLiteral
|
||||
= '"' chars:DoubleQuotedChar* '"' {
|
||||
return {
|
||||
type: 'StringLiteral',
|
||||
value: chars.join('')
|
||||
};
|
||||
}
|
||||
/ "'" chars:SingleQuotedChar* "'" {
|
||||
// Single-quoted strings are "tagged" strings in TorqueScript,
|
||||
// used for network optimization (string sent once, then only tag ID)
|
||||
return {
|
||||
type: 'StringLiteral',
|
||||
value: chars.join(''),
|
||||
tagged: true
|
||||
};
|
||||
}
|
||||
|
||||
DoubleQuotedChar
|
||||
= "\\" char:EscapeSequence { return char; }
|
||||
/ [^"\\\n\r]
|
||||
|
||||
SingleQuotedChar
|
||||
= "\\" char:EscapeSequence { return char; }
|
||||
/ [^'\\\n\r]
|
||||
|
||||
EscapeSequence
|
||||
= "n" { return "\n"; }
|
||||
/ "r" { return "\r"; }
|
||||
/ "t" { return "\t"; }
|
||||
/ "x" hex:$([0-9a-fA-F][0-9a-fA-F]) { return String.fromCharCode(parseInt(hex, 16)); }
|
||||
// TorqueScript color codes - mapped to byte values that skip \t (9), \n (10), \r (13)
|
||||
// These are processed by the game's text rendering system
|
||||
/ "cr" { return String.fromCharCode(0x0F); } // reset color
|
||||
/ "cp" { return String.fromCharCode(0x10); } // push color
|
||||
/ "co" { return String.fromCharCode(0x11); } // pop color
|
||||
/ "c" code:$([0-9]) {
|
||||
// collapseRemap: \c0-\c9 map to bytes that avoid \t, \n, \r
|
||||
const collapseRemap = [0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x0B, 0x0C, 0x0E];
|
||||
return String.fromCharCode(collapseRemap[parseInt(code, 10)]);
|
||||
}
|
||||
/ char:. { return char; }
|
||||
|
||||
NumberLiteral
|
||||
= hex:$("0" [xX] [0-9a-fA-F]+) !IdentifierChar {
|
||||
return {
|
||||
type: 'NumberLiteral',
|
||||
value: parseInt(hex, 16)
|
||||
};
|
||||
}
|
||||
/ number:$(("-"? [0-9]+ ("." [0-9]+)?) / ("-"? "." [0-9]+)) !IdentifierChar {
|
||||
return {
|
||||
type: 'NumberLiteral',
|
||||
value: parseFloat(number)
|
||||
};
|
||||
}
|
||||
|
||||
BooleanLiteral
|
||||
= value:("true" / "false") !IdentifierChar {
|
||||
return {
|
||||
type: 'BooleanLiteral',
|
||||
value: value === "true"
|
||||
};
|
||||
}
|
||||
|
||||
// Comments
|
||||
Comment
|
||||
= SingleLineComment
|
||||
/ MultiLineComment
|
||||
|
||||
SingleLineComment
|
||||
= "//" text:$[^\n\r]* [\n\r]? {
|
||||
return {
|
||||
type: 'Comment',
|
||||
value: text
|
||||
};
|
||||
}
|
||||
|
||||
MultiLineComment
|
||||
= "/*" text:$(!"*/" .)* "*/" {
|
||||
return {
|
||||
type: 'Comment',
|
||||
value: text
|
||||
};
|
||||
}
|
||||
|
||||
// Whitespace
|
||||
Whitespace
|
||||
= [ \t\n\r]+ { return null; }
|
||||
|
||||
// Optional whitespace and comments (comments are consumed but not captured)
|
||||
// Use this between tokens and in expressions
|
||||
_
|
||||
= ([ \t\n\r] / SkipComment)*
|
||||
|
||||
// Required whitespace (at least one whitespace char, may include comments)
|
||||
// Use this between keywords that must be separated
|
||||
__
|
||||
= [ \t\n\r]+ ([ \t\n\r] / SkipComment)*
|
||||
|
||||
// Pure whitespace only (no comments) - used in statement lists where comments are captured separately
|
||||
ws
|
||||
= [ \t\n\r]*
|
||||
|
||||
// Comments consumed silently (no return value) - used in whitespace rules
|
||||
SkipComment
|
||||
= "//" [^\n\r]* [\n\r]?
|
||||
/ "/*" (!"*/" .)* "*/"
|
||||
|
||||
IdentifierChar
|
||||
= [a-zA-Z0-9_]
|
||||
Loading…
Add table
Add a link
Reference in a new issue