Visitar URL original
fix(core): ignore processing instruction syntax in templates · angular/angular@68ac204 · GitHub
Skip to content

Commit 68ac204

Browse files
crisbetomattrbeck
authored andcommitted
fix(core): ignore processing instruction syntax in templates
Updates the template parser to detect and ignore processing instruction syntax (e.g. `<? foo ?>` or `<? foo >`). Currently it is being printed out as text. Fixes #34371.
1 parent ea83f13 commit 68ac204

4 files changed

Lines changed: 113 additions & 30 deletions

File tree

‎packages/compiler/src/ml_parser/lexer.ts‎

Lines changed: 44 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,8 @@ class _Tokenizer {
241241
}
242242
} else if (this._attemptCharCode(chars.$SLASH)) {
243243
this._consumeTagClose(start);
244+
} else if (this._attemptCharCode(chars.$QUESTION)) {
245+
this._consumeProcessingInstruction(start);
244246
} else {
245247
this._consumeTagOpen(start);
246248
}
@@ -470,30 +472,7 @@ class _Tokenizer {
470472
private _consumeLetDeclarationValue(): void {
471473
const start = this._cursor.clone();
472474
this._beginToken(TokenType.LET_VALUE, start);
473-
474-
while (this._cursor.peek() !== chars.$EOF) {
475-
const char = this._cursor.peek();
476-
477-
// `@let` declarations terminate with a semicolon.
478-
if (char === chars.$SEMICOLON) {
479-
break;
480-
}
481-
482-
// If we hit a quote, skip over its content since we don't care what's inside.
483-
if (chars.isQuote(char)) {
484-
this._cursor.advance();
485-
this._attemptCharCodeUntilFn((inner) => {
486-
if (inner === chars.$BACKSLASH) {
487-
this._cursor.advance();
488-
return false;
489-
}
490-
return inner === char;
491-
});
492-
}
493-
494-
this._cursor.advance();
495-
}
496-
475+
this._attemptUntilIgnoreQuotes((char) => char === chars.$SEMICOLON); // `@let` declarations terminate with a semicolon.
497476
this._endToken([this._cursor.getChars(start)]);
498477
}
499478

@@ -665,6 +644,29 @@ class _Tokenizer {
665644
}
666645
}
667646

647+
private _attemptUntilIgnoreQuotes(predicate: (code: number) => boolean) {
648+
while (this._cursor.peek() !== chars.$EOF) {
649+
const char = this._cursor.peek();
650+
651+
if (predicate(char)) {
652+
break;
653+
}
654+
655+
if (chars.isQuote(char)) {
656+
this._cursor.advance();
657+
this._attemptCharCodeUntilFn((inner) => {
658+
if (inner === chars.$BACKSLASH) {
659+
this._cursor.advance();
660+
return false;
661+
}
662+
return inner === char;
663+
});
664+
}
665+
666+
this._cursor.advance();
667+
}
668+
}
669+
668670
private _readChar(): string {
669671
// Don't rely upon reading directly from `_input` as the actual char value
670672
// may have been generated from an escape sequence.
@@ -800,6 +802,21 @@ class _Tokenizer {
800802
this._endToken([content]);
801803
}
802804

805+
private _consumeProcessingInstruction(start: CharacterCursor) {
806+
this._beginToken(TokenType.PROCESSING_INSTRUCTION, start);
807+
const contentStart = this._cursor.clone();
808+
this._attemptUntilIgnoreQuotes((char) => char === chars.$QUESTION || char === chars.$GT);
809+
const endChar = this._cursor.peek();
810+
const content = this._cursor.getChars(contentStart);
811+
this._cursor.advance();
812+
813+
if (endChar === chars.$QUESTION) {
814+
this._requireCharCode(chars.$GT);
815+
}
816+
817+
this._endToken([content]);
818+
}
819+
803820
private _consumePrefixAndName(endPredicate: (code: number) => boolean): string[] {
804821
const nameOrPrefixStart = this._cursor.clone();
805822
let prefix: string = '';
@@ -1420,13 +1437,14 @@ class _Tokenizer {
14201437
// We assume that `<` followed by whitespace is not the start of an HTML element.
14211438
const tmp = this._cursor.clone();
14221439
tmp.advance();
1423-
// If the next character is alphabetic, ! nor / then it is a tag start
1440+
// If the next character is alphabetic, !, ? nor / then it is a tag start
14241441
const code = tmp.peek();
14251442
if (
14261443
(chars.$a <= code && code <= chars.$z) ||
14271444
(chars.$A <= code && code <= chars.$Z) ||
14281445
code === chars.$SLASH ||
1429-
code === chars.$BANG
1446+
code === chars.$BANG ||
1447+
code === chars.$QUESTION
14301448
) {
14311449
return true;
14321450
}

‎packages/compiler/src/ml_parser/tokens.ts‎

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ export const enum TokenType {
5151
DIRECTIVE_NAME,
5252
DIRECTIVE_OPEN,
5353
DIRECTIVE_CLOSE,
54+
PROCESSING_INSTRUCTION,
5455
EOF,
5556
}
5657

@@ -95,14 +96,13 @@ export type Token =
9596
| IncompleteComponentOpenToken
9697
| DirectiveNameToken
9798
| DirectiveOpenToken
98-
| DirectiveCloseToken;
99+
| DirectiveCloseToken
100+
| ProcessingInstructionToken;
99101

100102
export type InterpolatedTextToken = TextToken | InterpolationToken | EncodedEntityToken;
101103

102104
export type InterpolatedAttributeToken =
103-
| AttributeValueTextToken
104-
| AttributeValueInterpolationToken
105-
| EncodedEntityToken;
105+
AttributeValueTextToken | AttributeValueInterpolationToken | EncodedEntityToken;
106106

107107
export interface TokenBase {
108108
type: TokenType;
@@ -318,3 +318,8 @@ export interface DirectiveCloseToken extends TokenBase {
318318
type: TokenType.DIRECTIVE_CLOSE;
319319
parts: [];
320320
}
321+
322+
export interface ProcessingInstructionToken extends TokenBase {
323+
type: TokenType.PROCESSING_INSTRUCTION;
324+
parts: [content: string];
325+
}

‎packages/compiler/test/ml_parser/html_parser_spec.ts‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1629,6 +1629,20 @@ describe('HtmlParser', () => {
16291629
});
16301630
});
16311631

1632+
describe('ignored syntax', () => {
1633+
it('should ignore doctype declaration', () => {
1634+
expect(humanizeDom(parser.parse(`<!DOCTYPE html>hello`, 'TestComp'))).toEqual([
1635+
[html.Text, 'hello', 0, ['hello']],
1636+
]);
1637+
});
1638+
1639+
it('should ignore processing instruction', () => {
1640+
expect(
1641+
humanizeDom(parser.parse(`<?xml version="1.0" encoding="UTF-8"?>hello`, 'TestComp')),
1642+
).toEqual([[html.Text, 'hello', 0, ['hello']]]);
1643+
});
1644+
});
1645+
16321646
describe('source spans', () => {
16331647
it('should store the location', () => {
16341648
expect(

‎packages/compiler/test/ml_parser/lexer_spec.ts‎

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,52 @@ describe('HtmlLexer', () => {
158158
});
159159
});
160160

161+
describe('processing instructions', () => {
162+
it('should parse a processing instruction', () => {
163+
expect(tokenizeAndHumanizeParts('<?xml version="1.0" encoding="UTF-8"?>')).toEqual([
164+
[TokenType.PROCESSING_INSTRUCTION, 'xml version="1.0" encoding="UTF-8"'],
165+
[TokenType.EOF],
166+
]);
167+
});
168+
169+
it('should parse a processing instruction ending with > instead of ?>', () => {
170+
expect(tokenizeAndHumanizeParts('<?xml version="1.0" encoding="UTF-8">')).toEqual([
171+
[TokenType.PROCESSING_INSTRUCTION, 'xml version="1.0" encoding="UTF-8"'],
172+
[TokenType.EOF],
173+
]);
174+
});
175+
176+
it('should parse a processing instruction surrounded by other content', () => {
177+
expect(
178+
tokenizeAndHumanizeParts('<!DOCTYPE html>\n<?xml version="1.0" encoding="UTF-8"?> hello'),
179+
).toEqual([
180+
[TokenType.DOC_TYPE, 'DOCTYPE html'],
181+
[TokenType.TEXT, '\n'],
182+
[TokenType.PROCESSING_INSTRUCTION, 'xml version="1.0" encoding="UTF-8"'],
183+
[TokenType.TEXT, ' hello'],
184+
[TokenType.EOF],
185+
]);
186+
});
187+
188+
it('should parse a processing instruction that contains question marks inside quoted content', () => {
189+
expect(tokenizeAndHumanizeParts('<?xml version="?" encoding="UTF?>-8"?>')).toEqual([
190+
[TokenType.PROCESSING_INSTRUCTION, 'xml version="?" encoding="UTF?>-8"'],
191+
[TokenType.EOF],
192+
]);
193+
});
194+
195+
it('should store the location of a processing instruction', () => {
196+
expect(tokenizeAndHumanizeSourceSpans('<?xml version="1.0" encoding="UTF-8"?>')).toEqual([
197+
[TokenType.PROCESSING_INSTRUCTION, '<?xml version="1.0" encoding="UTF-8"?>'],
198+
[TokenType.EOF, ''],
199+
]);
200+
});
201+
202+
it('should report missing end of a processing instruction', () => {
203+
expect(tokenizeAndHumanizeErrors('<?')).toEqual([['Unexpected character "EOF"', '0:2']]);
204+
});
205+
});
206+
161207
describe('CDATA', () => {
162208
it('should parse CDATA', () => {
163209
expect(tokenizeAndHumanizeParts('<![CDATA[t\ne\rs\r\nt]]>')).toEqual([

0 commit comments

Comments
 (0)