mirror of
https://github.com/angular/angular
synced 2026-05-24 09:28:37 +00:00
feat(core): support regular expressions in templates (#63857)
Updates the template syntax to support inline regular expressions. PR Close #63857
This commit is contained in:
parent
8a69c0629b
commit
328a2bf719
8 changed files with 138 additions and 0 deletions
|
|
@ -26,6 +26,7 @@ import {
|
|||
ParenthesizedExpression,
|
||||
PrefixNot,
|
||||
PropertyRead,
|
||||
RegularExpressionLiteral,
|
||||
SafeCall,
|
||||
SafeKeyedRead,
|
||||
SafePropertyRead,
|
||||
|
|
@ -199,6 +200,10 @@ class AstTranslator implements AstVisitor {
|
|||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
visitRegularExpressionLiteral(ast: RegularExpressionLiteral, context: any) {
|
||||
throw new Error('TODO');
|
||||
}
|
||||
|
||||
visitInterpolation(ast: Interpolation): ts.Expression {
|
||||
// Build up a chain of binary + operations to simulate the string concatenation of the
|
||||
// interpolation's expressions. The chain is started using an actual string literal to ensure
|
||||
|
|
@ -603,4 +608,7 @@ class VeSafeLhsInferenceBugDetector implements AstVisitor {
|
|||
visitParenthesizedExpression(ast: ParenthesizedExpression, context: any) {
|
||||
return ast.expression.visit(this);
|
||||
}
|
||||
visitRegularExpressionLiteral(ast: RegularExpressionLiteral, context: any) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -488,6 +488,21 @@ export class ParenthesizedExpression extends AST {
|
|||
}
|
||||
}
|
||||
|
||||
export class RegularExpressionLiteral extends AST {
|
||||
constructor(
|
||||
span: ParseSpan,
|
||||
sourceSpan: AbsoluteSourceSpan,
|
||||
readonly body: string,
|
||||
readonly flags: string | null,
|
||||
) {
|
||||
super(span, sourceSpan);
|
||||
}
|
||||
|
||||
override visit(visitor: AstVisitor, context?: any) {
|
||||
return visitor.visitRegularExpressionLiteral(this, context);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Records the absolute position of a text span in a source file, where `start` and `end` are the
|
||||
* starting and ending byte offsets, respectively, of the text span in a source file.
|
||||
|
|
@ -617,6 +632,7 @@ export interface AstVisitor {
|
|||
visitTemplateLiteralElement(ast: TemplateLiteralElement, context: any): any;
|
||||
visitTaggedTemplateLiteral(ast: TaggedTemplateLiteral, context: any): any;
|
||||
visitParenthesizedExpression(ast: ParenthesizedExpression, context: any): any;
|
||||
visitRegularExpressionLiteral(ast: RegularExpressionLiteral, context: any): any;
|
||||
visitASTWithSource?(ast: ASTWithSource, context: any): any;
|
||||
/**
|
||||
* This function is optionally defined to allow classes that implement this
|
||||
|
|
@ -719,6 +735,7 @@ export class RecursiveAstVisitor implements AstVisitor {
|
|||
visitParenthesizedExpression(ast: ParenthesizedExpression, context: any) {
|
||||
this.visit(ast.expression, context);
|
||||
}
|
||||
visitRegularExpressionLiteral(ast: RegularExpressionLiteral, context: any) {}
|
||||
// This is not part of the AstVisitor interface, just a helper method
|
||||
visitAll(asts: AST[], context: any): any {
|
||||
for (const ast of asts) {
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ import {
|
|||
PrefixNot,
|
||||
PropertyRead,
|
||||
RecursiveAstVisitor,
|
||||
RegularExpressionLiteral,
|
||||
SafeCall,
|
||||
SafeKeyedRead,
|
||||
SafePropertyRead,
|
||||
|
|
@ -581,6 +582,9 @@ enum ParseContextFlags {
|
|||
Writable = 1,
|
||||
}
|
||||
|
||||
/** Possible flags that can be used in a regex literal. */
|
||||
const SUPPORTED_REGEX_FLAGS = new Set(['d', 'g', 'i', 'm', 's', 'u', 'v', 'y']);
|
||||
|
||||
class _ParseAST {
|
||||
private rparensExpected = 0;
|
||||
private rbracketsExpected = 0;
|
||||
|
|
@ -1178,6 +1182,8 @@ class _ParseAST {
|
|||
} else if (this.next.isPrivateIdentifier()) {
|
||||
this._reportErrorForPrivateIdentifier(this.next, null);
|
||||
return new EmptyExpr(this.span(start), this.sourceSpan(start));
|
||||
} else if (this.next.isRegExpBody()) {
|
||||
return this.parseRegularExpressionLiteral();
|
||||
} else if (this.index >= this.tokens.length) {
|
||||
this.error(`Unexpected end of expression: ${this.input}`);
|
||||
return new EmptyExpr(this.span(start), this.sourceSpan(start));
|
||||
|
|
@ -1618,6 +1624,49 @@ class _ParseAST {
|
|||
return new TemplateLiteral(this.span(start), this.sourceSpan(start), elements, expressions);
|
||||
}
|
||||
|
||||
private parseRegularExpressionLiteral() {
|
||||
const bodyToken = this.next;
|
||||
this.advance();
|
||||
|
||||
if (!bodyToken.isRegExpBody()) {
|
||||
return new EmptyExpr(this.span(this.inputIndex), this.sourceSpan(this.inputIndex));
|
||||
}
|
||||
|
||||
let flagsToken: Token | null = null;
|
||||
|
||||
if (this.next.isRegExpFlags()) {
|
||||
flagsToken = this.next;
|
||||
this.advance();
|
||||
const seenFlags = new Set<string>();
|
||||
|
||||
for (let i = 0; i < flagsToken.strValue.length; i++) {
|
||||
const char = flagsToken.strValue[i];
|
||||
|
||||
if (!SUPPORTED_REGEX_FLAGS.has(char)) {
|
||||
this.error(
|
||||
`Unsupported regular expression flag "${char}". The supported flags are: ` +
|
||||
Array.from(SUPPORTED_REGEX_FLAGS, (f) => `"${f}"`).join(', '),
|
||||
flagsToken.index + i,
|
||||
);
|
||||
} else if (seenFlags.has(char)) {
|
||||
this.error(`Duplicate regular expression flag "${char}"`, flagsToken.index + i);
|
||||
} else {
|
||||
seenFlags.add(char);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const start = bodyToken.index;
|
||||
const end = flagsToken ? flagsToken.end : bodyToken.end;
|
||||
|
||||
return new RegularExpressionLiteral(
|
||||
this.span(start, end),
|
||||
this.sourceSpan(start, end),
|
||||
bodyToken.strValue,
|
||||
flagsToken ? flagsToken.strValue : null,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Consume the optional statement terminator: semicolon or comma.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -129,6 +129,10 @@ class SerializeExpressionVisitor implements expr.AstVisitor {
|
|||
return `void ${ast.expression.visit(this, context)}`;
|
||||
}
|
||||
|
||||
visitRegularExpressionLiteral(ast: expr.RegularExpressionLiteral, context: any) {
|
||||
return `/${ast.body}/${ast.flags || ''}`;
|
||||
}
|
||||
|
||||
visitASTWithSource(ast: expr.ASTWithSource, context: any): string {
|
||||
return ast.ast.visit(this, context);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -510,6 +510,36 @@ describe('parser', () => {
|
|||
checkBinding('typeof `hello ${name}!`');
|
||||
});
|
||||
});
|
||||
|
||||
describe('regular expression literals', () => {
|
||||
it('should parse a regular expression literal without flags', () => {
|
||||
checkBinding('/abc/');
|
||||
checkBinding('/[a/]$/');
|
||||
checkBinding('/a\\w+/');
|
||||
checkBinding('/^http:\\/\\/foo\\.bar/');
|
||||
});
|
||||
|
||||
it('should parse a regular expression literal with flags', () => {
|
||||
checkBinding('/abc/g');
|
||||
checkBinding('/[a/]$/gi');
|
||||
checkBinding('/a\\w+/gim');
|
||||
checkBinding('/^http:\\/\\/foo\\.bar/i');
|
||||
});
|
||||
|
||||
it('should parse a regular expression that is a part of other expressions', () => {
|
||||
checkBinding('/abc/.test("foo")');
|
||||
checkBinding('"foo".match(/(abc)/)[1].toUpperCase()');
|
||||
checkBinding('/abc/.test("foo") && something || somethingElse');
|
||||
});
|
||||
|
||||
it('should report invalid regular expression flag', () => {
|
||||
expectBindingError('"foo".match(/abc/O)', 'Unsupported regular expression flag "O"');
|
||||
});
|
||||
|
||||
it('should report duplicated regular expression flags', () => {
|
||||
expectBindingError('"foo".match(/abc/gig)', 'Duplicate regular expression flag "g"');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('parse spans', () => {
|
||||
|
|
@ -691,6 +721,22 @@ describe('parser', () => {
|
|||
expect(unparseWithSpan(parseBinding(input))).toContain([jasmine.any(String), input]);
|
||||
}
|
||||
});
|
||||
|
||||
it('should record span for a regex without flags', () => {
|
||||
const ast = parseBinding('/^http:\\/\\/foo\\.bar/');
|
||||
expect(unparseWithSpan(ast)).toContain([
|
||||
'/^http:\\/\\/foo\\.bar/',
|
||||
'/^http:\\/\\/foo\\.bar/',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should record span for a regex with flags', () => {
|
||||
const ast = parseBinding('/^http:\\/\\/foo\\.bar/gim');
|
||||
expect(unparseWithSpan(ast)).toContain([
|
||||
'/^http:\\/\\/foo\\.bar/gim',
|
||||
'/^http:\\/\\/foo\\.bar/gim',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('general error handling', () => {
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import {
|
|||
PrefixNot,
|
||||
PropertyRead,
|
||||
RecursiveAstVisitor,
|
||||
RegularExpressionLiteral,
|
||||
SafeCall,
|
||||
SafeKeyedRead,
|
||||
SafePropertyRead,
|
||||
|
|
@ -237,6 +238,10 @@ class Unparser implements AstVisitor {
|
|||
this._expression += ')';
|
||||
}
|
||||
|
||||
visitRegularExpressionLiteral(ast: RegularExpressionLiteral, context: any) {
|
||||
this._expression += `/${ast.body}/${ast.flags || ''}`;
|
||||
}
|
||||
|
||||
private _visit(ast: AST) {
|
||||
ast.visit(this);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import {
|
|||
PrefixNot,
|
||||
PropertyRead,
|
||||
RecursiveAstVisitor,
|
||||
RegularExpressionLiteral,
|
||||
SafeCall,
|
||||
SafeKeyedRead,
|
||||
SafePropertyRead,
|
||||
|
|
@ -155,6 +156,10 @@ class ASTValidator extends RecursiveAstVisitor {
|
|||
override visitParenthesizedExpression(ast: ParenthesizedExpression, context: any): void {
|
||||
this.validate(ast, () => super.visitParenthesizedExpression(ast, context));
|
||||
}
|
||||
|
||||
override visitRegularExpressionLiteral(ast: RegularExpressionLiteral, context: any): void {
|
||||
this.validate(ast, () => super.visitRegularExpressionLiteral(ast, context));
|
||||
}
|
||||
}
|
||||
|
||||
function inSpan(span: ParseSpan, parentSpan: ParseSpan | undefined): parentSpan is ParseSpan {
|
||||
|
|
|
|||
|
|
@ -127,6 +127,10 @@ class ExpressionSourceHumanizer extends e.RecursiveAstVisitor implements t.Visit
|
|||
this.recordAst(ast);
|
||||
super.visitParenthesizedExpression(ast, null);
|
||||
}
|
||||
override visitRegularExpressionLiteral(ast: e.RegularExpressionLiteral, context: any): void {
|
||||
this.recordAst(ast);
|
||||
super.visitRegularExpressionLiteral(ast, null);
|
||||
}
|
||||
|
||||
visitTemplate(ast: t.Template) {
|
||||
t.visitAll(this, ast.directives);
|
||||
|
|
|
|||
Loading…
Reference in a new issue