Compare commits
17 Commits
Author | SHA1 | Date | |
---|---|---|---|
81db6a3171 | |||
8224e65e70 | |||
9e50cef507 | |||
66d8c223a9 | |||
814b43647d | |||
6b98567b6b | |||
7f2a3e4b6b | |||
1775f351d6 | |||
1df52d9acd | |||
77e8db4484 | |||
c90262e619 | |||
87bbf69ce8 | |||
552853648c | |||
07b99f5975 | |||
57d1a483fc | |||
d662a6449e | |||
e57a2b3c47 |
18
CHANGELOG.md
18
CHANGELOG.md
@ -1,3 +1,21 @@
|
||||
<a name="10.0.12"></a>
|
||||
## 10.0.12 (2020-08-24)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **compiler-cli:** adding references to const enums in runtime code ([#38542](https://github.com/angular/angular/issues/38542)) ([814b436](https://github.com/angular/angular/commit/814b436)), closes [#38513](https://github.com/angular/angular/issues/38513)
|
||||
* **core:** remove closing body tag from inert DOM builder ([#38454](https://github.com/angular/angular/issues/38454)) ([5528536](https://github.com/angular/angular/commit/5528536))
|
||||
* **localize:** include the last placeholder in parsed translation text ([#38452](https://github.com/angular/angular/issues/38452)) ([57d1a48](https://github.com/angular/angular/commit/57d1a48))
|
||||
* **localize:** parse all parts of a translation with nested HTML ([#38452](https://github.com/angular/angular/issues/38452)) ([07b99f5](https://github.com/angular/angular/commit/07b99f5)), closes [#38422](https://github.com/angular/angular/issues/38422)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **language-service:** introduce hybrid visitor to locate AST node ([#38540](https://github.com/angular/angular/issues/38540)) ([66d8c22](https://github.com/angular/angular/commit/66d8c22))
|
||||
|
||||
|
||||
|
||||
<a name="10.0.11"></a>
|
||||
## 10.0.11 (2020-08-19)
|
||||
|
||||
|
@ -154,7 +154,7 @@ Attributes can be changed by `setAttribute()`, which re-initializes correspondin
|
||||
</div>
|
||||
|
||||
For more information, see the [MDN Interfaces documentation](https://developer.mozilla.org/en-US/docs/Web/API#Interfaces) which has API docs for all the standard DOM elements and their properties.
|
||||
Comparing the [`<td>` attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/td) attributes to the [`<td>` properties](https://developer.mozilla.org/en-US/docs/Web/API/HTMLTableCellElement) provides a helpful example for differentiation.
|
||||
Comparing the [`<td>` attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/td) to the [`<td>` properties](https://developer.mozilla.org/en-US/docs/Web/API/HTMLTableCellElement) provides a helpful example for differentiation.
|
||||
In particular, you can navigate from the attributes page to the properties via "DOM interface" link, and navigate the inheritance hierarchy up to `HTMLTableCellElement`.
|
||||
|
||||
|
||||
@ -195,7 +195,7 @@ To control the state of the button, set the `disabled` *property*,
|
||||
|
||||
<div class="alert is-helpful">
|
||||
|
||||
Though you could technically set the `[attr.disabled]` attribute binding, the values are different in that the property binding requires to a boolean value, while its corresponding attribute binding relies on whether the value is `null` or not. Consider the following:
|
||||
Though you could technically set the `[attr.disabled]` attribute binding, the values are different in that the property binding requires to be a boolean value, while its corresponding attribute binding relies on whether the value is `null` or not. Consider the following:
|
||||
|
||||
```html
|
||||
<input [disabled]="condition ? true : false">
|
||||
|
@ -766,8 +766,10 @@ The HTML `base` tag with the `href` attribute specifies the base URI, or URL, fo
|
||||
"i18n": {
|
||||
"sourceLocale": "en-US",
|
||||
"locales": {
|
||||
"fr": "src/locale/messages.fr.xlf"
|
||||
"baseHref": ""
|
||||
"fr": {
|
||||
"translation": "src/locale/messages.fr.xlf",
|
||||
"baseHref": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"architect": {
|
||||
|
@ -37,9 +37,9 @@ by HTML.
|
||||
|
||||
<code-example path="template-reference-variables/src/app/app.component.html" region="ngForm" header="src/app/hero-form.component.html"></code-example>
|
||||
|
||||
The reference value of itemForm, without the ngForm attribute value, would be
|
||||
The reference value of `itemForm`, without the `ngForm` attribute value, would be
|
||||
the [HTMLFormElement](https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement).
|
||||
There is, however, a difference between a Component and a Directive in that a `Component`
|
||||
There is, however, a difference between a `Component` and a `Directive` in that a `Component`
|
||||
will be referenced without specifying the attribute value, and a `Directive` will not
|
||||
change the implicit reference (that is, the element).
|
||||
|
||||
|
@ -713,9 +713,9 @@
|
||||
"santosh": {
|
||||
"name": "Santosh Yadav",
|
||||
"picture": "santoshyadav.jpg",
|
||||
"twitter": "Santosh19742211",
|
||||
"twitter": "SantoshYadavDev",
|
||||
"website": "https://www.santoshyadav.dev",
|
||||
"bio": "Santosh is a GDE for Angular and Web Technologies and loves to contribute to Open Source. He is the creator of ng deploy for netlify and core team member for NestJS Addons. He writes for AngularInDepth, mentors for DotNetTricks, organizes Pune Tech Meetup, and conducts free workshops on Angular.",
|
||||
"bio": "Santosh is a GDE for Angular and loves to contribute to Open Source. He is the creator of ng deploy for netlify and core team member for NestJS Addons. He writes for AngularInDepth, mentors for DotNetTricks, organizes Pune Tech Meetup, and conducts free workshops on Angular.",
|
||||
"groups": ["GDE"]
|
||||
},
|
||||
"josephperrott": {
|
||||
|
@ -214,7 +214,7 @@ code {
|
||||
margin-left: 2px;
|
||||
position: relative;
|
||||
@include line-height(24);
|
||||
vertical-align: bottom;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -82,4 +82,24 @@ describe('commit message parsing:', () => {
|
||||
const message2 = buildCommitMessage({prefix: 'squash! '});
|
||||
expect(parseCommitMessage(message2).isSquash).toBe(true);
|
||||
});
|
||||
|
||||
it('ignores comment lines', () => {
|
||||
const message = buildCommitMessage({
|
||||
prefix: '# This is a comment line before the header.\n' +
|
||||
'## This is another comment line before the headers.\n',
|
||||
body: '# This is a comment line befor the body.\n' +
|
||||
'This is line 1 of the actual body.\n' +
|
||||
'## This is another comment line inside the body.\n' +
|
||||
'This is line 2 of the actual body (and it also contains a # but it not a comment).\n' +
|
||||
'### This is yet another comment line after the body.\n',
|
||||
});
|
||||
const parsedMessage = parseCommitMessage(message);
|
||||
|
||||
expect(parsedMessage.header)
|
||||
.toBe(`${commitValues.type}(${commitValues.scope}): ${commitValues.summary}`);
|
||||
expect(parsedMessage.body)
|
||||
.toBe(
|
||||
'This is line 1 of the actual body.\n' +
|
||||
'This is line 2 of the actual body (and it also contains a # but it not a comment).\n');
|
||||
});
|
||||
});
|
||||
|
@ -36,6 +36,10 @@ const COMMIT_BODY_RE = /^.*\n\n([\s\S]*)$/;
|
||||
|
||||
/** Parse a full commit message into its composite parts. */
|
||||
export function parseCommitMessage(commitMsg: string): ParsedCommitMessage {
|
||||
// Ignore comments (i.e. lines starting with `#`). Comments are automatically removed by git and
|
||||
// should not be considered part of the final commit message.
|
||||
commitMsg = commitMsg.split('\n').filter(line => !line.startsWith('#')).join('\n');
|
||||
|
||||
let header = '';
|
||||
let body = '';
|
||||
let bodyWithoutLinking = '';
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "angular-srcs",
|
||||
"version": "10.0.11",
|
||||
"version": "10.0.12",
|
||||
"private": true,
|
||||
"description": "Angular - a web framework for modern web apps",
|
||||
"homepage": "https://github.com/angular/angular",
|
||||
|
@ -35,8 +35,9 @@ export function typeToValue(
|
||||
|
||||
const {local, decl} = symbols;
|
||||
// It's only valid to convert a type reference to a value reference if the type actually
|
||||
// has a value declaration associated with it.
|
||||
if (decl.valueDeclaration === undefined) {
|
||||
// has a value declaration associated with it. Note that const enums are an exception,
|
||||
// because while they do have a value declaration, they don't exist at runtime.
|
||||
if (decl.valueDeclaration === undefined || decl.flags & ts.SymbolFlags.ConstEnum) {
|
||||
return noValueDeclaration(typeNode, decl.declarations[0]);
|
||||
}
|
||||
|
||||
|
@ -28,6 +28,14 @@ export interface TemplateTypeChecker {
|
||||
*/
|
||||
resetOverrides(): void;
|
||||
|
||||
/**
|
||||
* Retrieve the template in use for the given component.
|
||||
*
|
||||
* If the template has been overridden via `overrideComponentTemplate`, this will retrieve the
|
||||
* overridden template nodes.
|
||||
*/
|
||||
getTemplate(component: ts.ClassDeclaration): TmplAstNode[]|null;
|
||||
|
||||
/**
|
||||
* Provide a new template string that will be used in place of the user-defined template when
|
||||
* checking or operating on the given component.
|
||||
|
@ -48,6 +48,29 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker {
|
||||
}
|
||||
}
|
||||
|
||||
getTemplate(component: ts.ClassDeclaration): TmplAstNode[]|null {
|
||||
this.ensureShimForComponent(component);
|
||||
|
||||
const sf = component.getSourceFile();
|
||||
const sfPath = absoluteFromSourceFile(sf);
|
||||
const shimPath = this.typeCheckingStrategy.shimPathForComponent(component);
|
||||
|
||||
const fileRecord = this.getFileData(sfPath);
|
||||
|
||||
if (!fileRecord.shimData.has(shimPath)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const templateId = fileRecord.sourceManager.getTemplateId(component);
|
||||
const shimRecord = fileRecord.shimData.get(shimPath)!;
|
||||
|
||||
if (!shimRecord.templates.has(templateId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return shimRecord.templates.get(templateId)!.template;
|
||||
}
|
||||
|
||||
overrideComponentTemplate(component: ts.ClassDeclaration, template: string):
|
||||
{nodes: TmplAstNode[], errors?: ParseError[]} {
|
||||
const {nodes, errors} = parseTemplate(template, 'override.html', {
|
||||
|
@ -6,14 +6,14 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {ParseSourceFile, R3TargetBinder, SchemaMetadata, TmplAstNode} from '@angular/compiler';
|
||||
import {BoundTarget, ParseSourceFile, R3TargetBinder, SchemaMetadata, TmplAstNode} from '@angular/compiler';
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {absoluteFromSourceFile, AbsoluteFsPath} from '../../file_system';
|
||||
import {NoopImportRewriter, Reference, ReferenceEmitter} from '../../imports';
|
||||
import {ClassDeclaration, ReflectionHost} from '../../reflection';
|
||||
import {ImportManager} from '../../translator';
|
||||
import {ComponentToShimMappingStrategy, TemplateSourceMapping, TypeCheckableDirectiveMeta, TypeCheckBlockMetadata, TypeCheckContext, TypeCheckingConfig, TypeCtorMetadata} from '../api';
|
||||
import {ComponentToShimMappingStrategy, TemplateId, TemplateSourceMapping, TypeCheckableDirectiveMeta, TypeCheckBlockMetadata, TypeCheckContext, TypeCheckingConfig, TypeCtorMetadata} from '../api';
|
||||
|
||||
import {TemplateDiagnostic} from './diagnostics';
|
||||
import {DomSchemaChecker, RegistryDomSchemaChecker} from './dom';
|
||||
@ -41,6 +41,28 @@ export interface ShimTypeCheckingData {
|
||||
* Whether any inline operations for the input file were required to generate this shim.
|
||||
*/
|
||||
hasInlines: boolean;
|
||||
|
||||
/**
|
||||
* Map of `TemplateId` to information collected about the template during the template
|
||||
* type-checking process.
|
||||
*/
|
||||
templates: Map<TemplateId, TemplateData>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Data tracked for each template processed by the template type-checking system.
|
||||
*/
|
||||
export interface TemplateData {
|
||||
/**
|
||||
* Template nodes for which the TCB was generated.
|
||||
*/
|
||||
template: TmplAstNode[];
|
||||
|
||||
/**
|
||||
* `BoundTarget` which was used to generate the TCB, and contains bindings for the associated
|
||||
* template nodes.
|
||||
*/
|
||||
boundTarget: BoundTarget<TypeCheckableDirectiveMeta>;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -79,6 +101,12 @@ export interface PendingShimData {
|
||||
* Shim file in the process of being generated.
|
||||
*/
|
||||
file: TypeCheckFile;
|
||||
|
||||
|
||||
/**
|
||||
* Map of `TemplateId` to information collected about the template as it's ingested.
|
||||
*/
|
||||
templates: Map<TemplateId, TemplateData>;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -195,6 +223,7 @@ export class TypeCheckContextImpl implements TypeCheckContext {
|
||||
const fileData = this.dataForFile(ref.node.getSourceFile());
|
||||
const shimData = this.pendingShimForComponent(ref.node);
|
||||
const boundTarget = binder.bind({template});
|
||||
|
||||
// Get all of the directives used in the template and record type constructors for all of them.
|
||||
for (const dir of boundTarget.getUsedDirectives()) {
|
||||
const dirRef = dir.ref as Reference<ClassDeclaration<ts.ClassDeclaration>>;
|
||||
@ -221,6 +250,11 @@ export class TypeCheckContextImpl implements TypeCheckContext {
|
||||
});
|
||||
}
|
||||
}
|
||||
const templateId = fileData.sourceManager.getTemplateId(ref.node);
|
||||
shimData.templates.set(templateId, {
|
||||
template,
|
||||
boundTarget,
|
||||
});
|
||||
|
||||
const tcbRequiresInline = requiresInlineTypeCheckBlock(ref.node);
|
||||
|
||||
@ -231,7 +265,6 @@ export class TypeCheckContextImpl implements TypeCheckContext {
|
||||
// and inlining would be required.
|
||||
|
||||
// Record diagnostics to indicate the issues with this template.
|
||||
const templateId = fileData.sourceManager.getTemplateId(ref.node);
|
||||
if (tcbRequiresInline) {
|
||||
shimData.oobRecorder.requiresInlineTcb(templateId, ref.node);
|
||||
}
|
||||
@ -348,6 +381,7 @@ export class TypeCheckContextImpl implements TypeCheckContext {
|
||||
],
|
||||
hasInlines: pendingFileData.hasInlines,
|
||||
path: pendingShimData.file.fileName,
|
||||
templates: pendingShimData.templates,
|
||||
});
|
||||
updates.set(pendingShimData.file.fileName, pendingShimData.file.render());
|
||||
}
|
||||
@ -380,6 +414,7 @@ export class TypeCheckContextImpl implements TypeCheckContext {
|
||||
oobRecorder: new OutOfBandDiagnosticRecorderImpl(fileData.sourceManager),
|
||||
file: new TypeCheckFile(
|
||||
shimPath, this.config, this.refEmitter, this.reflector, this.compilerHost),
|
||||
templates: new Map<TemplateId, TemplateData>(),
|
||||
});
|
||||
}
|
||||
return fileData.shimData.get(shimPath)!;
|
||||
|
@ -349,5 +349,40 @@ runInEachFileSystem(os => {
|
||||
expect(diags2[0].messageText).toContain('invalid-element-b');
|
||||
expect(diags2[0].messageText).not.toContain('invalid-element-a');
|
||||
});
|
||||
|
||||
describe('getTemplateOfComponent()', () => {
|
||||
it('should provide access to a component\'s real template', () => {
|
||||
const fileName = absoluteFrom('/main.ts');
|
||||
const {program, templateTypeChecker} = setup([{
|
||||
fileName,
|
||||
templates: {
|
||||
'Cmp': '<div>Template</div>',
|
||||
},
|
||||
}]);
|
||||
const cmp = getClass(getSourceFileOrError(program, fileName), 'Cmp');
|
||||
|
||||
const nodes = templateTypeChecker.getTemplate(cmp)!;
|
||||
expect(nodes).not.toBeNull();
|
||||
expect(nodes[0].sourceSpan.start.file.content).toBe('<div>Template</div>');
|
||||
});
|
||||
|
||||
it('should provide access to an overridden template', () => {
|
||||
const fileName = absoluteFrom('/main.ts');
|
||||
const {program, templateTypeChecker} = setup([{
|
||||
fileName,
|
||||
templates: {
|
||||
'Cmp': '<div>Template</div>',
|
||||
},
|
||||
}]);
|
||||
const cmp = getClass(getSourceFileOrError(program, fileName), 'Cmp');
|
||||
|
||||
templateTypeChecker.overrideComponentTemplate(cmp, '<div>Overridden</div>');
|
||||
templateTypeChecker.getDiagnosticsForComponent(cmp);
|
||||
|
||||
const nodes = templateTypeChecker.getTemplate(cmp)!;
|
||||
expect(nodes).not.toBeNull();
|
||||
expect(nodes[0].sourceSpan.start.file.content).toBe('<div>Overridden</div>');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -298,15 +298,20 @@ function typeReferenceToExpression(
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the given symbol refers to a value (as distinct from a type).
|
||||
* Checks whether a given symbol refers to a value that exists at runtime (as distinct from a type).
|
||||
*
|
||||
* Expands aliases, which is important for the case where
|
||||
* import * as x from 'some-module';
|
||||
* and x is now a value (the module object).
|
||||
*/
|
||||
function symbolIsValue(tc: ts.TypeChecker, sym: ts.Symbol): boolean {
|
||||
if (sym.flags & ts.SymbolFlags.Alias) sym = tc.getAliasedSymbol(sym);
|
||||
return (sym.flags & ts.SymbolFlags.Value) !== 0;
|
||||
function symbolIsRuntimeValue(typeChecker: ts.TypeChecker, symbol: ts.Symbol): boolean {
|
||||
if (symbol.flags & ts.SymbolFlags.Alias) {
|
||||
symbol = typeChecker.getAliasedSymbol(symbol);
|
||||
}
|
||||
|
||||
// Note that const enums are a special case, because
|
||||
// while they have a value, they don't exist at runtime.
|
||||
return (symbol.flags & ts.SymbolFlags.Value & ts.SymbolFlags.ConstEnumExcludes) !== 0;
|
||||
}
|
||||
|
||||
/** ParameterDecorationInfo describes the information for a single constructor parameter. */
|
||||
@ -351,7 +356,7 @@ export function getDownlevelDecoratorsTransform(
|
||||
const symbol = typeChecker.getSymbolAtLocation(name);
|
||||
// Check if the entity name references a symbol that is an actual value. If it is not, it
|
||||
// cannot be referenced by an expression, so return undefined.
|
||||
if (!symbol || !symbolIsValue(typeChecker, symbol) || !symbol.declarations ||
|
||||
if (!symbol || !symbolIsRuntimeValue(typeChecker, symbol) || !symbol.declarations ||
|
||||
symbol.declarations.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
@ -4362,6 +4362,53 @@ runInEachFileSystem(os => {
|
||||
expect(jsContents).toMatch(setClassMetadataRegExp('type: undefined'));
|
||||
});
|
||||
|
||||
it('should use `undefined` in setClassMetadata for const enums', () => {
|
||||
env.write(`keycodes.ts`, `
|
||||
export const enum KeyCodes {A, B};
|
||||
`);
|
||||
env.write(`test.ts`, `
|
||||
import {Component, Inject} from '@angular/core';
|
||||
import {KeyCodes} from './keycodes';
|
||||
|
||||
@Component({
|
||||
selector: 'some-comp',
|
||||
template: '...',
|
||||
})
|
||||
export class SomeComp {
|
||||
constructor(@Inject('arg-token') arg: KeyCodes) {}
|
||||
}
|
||||
`);
|
||||
|
||||
env.driveMain();
|
||||
const jsContents = trim(env.getContents('test.js'));
|
||||
expect(jsContents).not.toContain(`import { KeyCodes } from './keycodes';`);
|
||||
// Note: `type: undefined` below, since KeyCodes can't be represented as a value
|
||||
expect(jsContents).toMatch(setClassMetadataRegExp('type: undefined'));
|
||||
});
|
||||
|
||||
it('should preserve the types of non-const enums in setClassMetadata', () => {
|
||||
env.write(`keycodes.ts`, `
|
||||
export enum KeyCodes {A, B};
|
||||
`);
|
||||
env.write(`test.ts`, `
|
||||
import {Component, Inject} from '@angular/core';
|
||||
import {KeyCodes} from './keycodes';
|
||||
|
||||
@Component({
|
||||
selector: 'some-comp',
|
||||
template: '...',
|
||||
})
|
||||
export class SomeComp {
|
||||
constructor(@Inject('arg-token') arg: KeyCodes) {}
|
||||
}
|
||||
`);
|
||||
|
||||
env.driveMain();
|
||||
const jsContents = trim(env.getContents('test.js'));
|
||||
expect(jsContents).toContain(`import { KeyCodes } from './keycodes';`);
|
||||
expect(jsContents).toMatch(setClassMetadataRegExp('type: i1.KeyCodes'));
|
||||
});
|
||||
|
||||
it('should use `undefined` in setClassMetadata if types originate from type-only imports',
|
||||
() => {
|
||||
env.write(`types.ts`, `
|
||||
|
@ -192,7 +192,7 @@ describe('downlevel decorator transform', () => {
|
||||
it('should downlevel Angular-decorated class member', () => {
|
||||
const {output} = transform(`
|
||||
import {Input} from '@angular/core';
|
||||
|
||||
|
||||
export class MyDir {
|
||||
@Input() disabled: boolean = false;
|
||||
}
|
||||
@ -231,7 +231,7 @@ describe('downlevel decorator transform', () => {
|
||||
const {output} = transform(`
|
||||
import {Input} from '@angular/core';
|
||||
import {MyOtherClass} from './other-file';
|
||||
|
||||
|
||||
export class MyDir {
|
||||
@Input() trigger: HTMLElement;
|
||||
@Input() fromOtherFile: MyOtherClass;
|
||||
@ -255,7 +255,7 @@ describe('downlevel decorator transform', () => {
|
||||
`
|
||||
import {Directive} from '@angular/core';
|
||||
import {MyOtherClass} from './other-file';
|
||||
|
||||
|
||||
@Directive()
|
||||
export class MyDir {
|
||||
constructor(other: MyOtherClass) {}
|
||||
@ -281,7 +281,7 @@ describe('downlevel decorator transform', () => {
|
||||
`
|
||||
import {Directive} from '@angular/core';
|
||||
import {MyOtherClass} from './other-file';
|
||||
|
||||
|
||||
@Directive()
|
||||
export class MyDir {
|
||||
constructor(other: MyOtherClass) {}
|
||||
@ -307,7 +307,7 @@ describe('downlevel decorator transform', () => {
|
||||
const {output} = transform(`
|
||||
import {Directive} from '@angular/core';
|
||||
import * as externalFile from './other-file';
|
||||
|
||||
|
||||
@Directive()
|
||||
export class MyDir {
|
||||
constructor(other: externalFile.MyOtherClass) {}
|
||||
@ -329,11 +329,11 @@ describe('downlevel decorator transform', () => {
|
||||
it('should properly serialize constructor parameter with local qualified name type', () => {
|
||||
const {output} = transform(`
|
||||
import {Directive} from '@angular/core';
|
||||
|
||||
|
||||
namespace other {
|
||||
export class OtherClass {}
|
||||
};
|
||||
|
||||
|
||||
@Directive()
|
||||
export class MyDir {
|
||||
constructor(other: other.OtherClass) {}
|
||||
@ -355,7 +355,7 @@ describe('downlevel decorator transform', () => {
|
||||
it('should properly downlevel constructor parameter decorators', () => {
|
||||
const {output} = transform(`
|
||||
import {Inject, Directive, DOCUMENT} from '@angular/core';
|
||||
|
||||
|
||||
@Directive()
|
||||
export class MyDir {
|
||||
constructor(@Inject(DOCUMENT) document: Document) {}
|
||||
@ -376,7 +376,7 @@ describe('downlevel decorator transform', () => {
|
||||
it('should properly downlevel constructor parameters with union type', () => {
|
||||
const {output} = transform(`
|
||||
import {Optional, Directive, NgZone} from '@angular/core';
|
||||
|
||||
|
||||
@Directive()
|
||||
export class MyDir {
|
||||
constructor(@Optional() ngZone: NgZone|null) {}
|
||||
@ -546,18 +546,20 @@ describe('downlevel decorator transform', () => {
|
||||
export default interface {
|
||||
hello: false;
|
||||
}
|
||||
export const enum KeyCodes {A, B}
|
||||
`);
|
||||
const {output} = transform(`
|
||||
import {Directive, Inject} from '@angular/core';
|
||||
import * as angular from './external';
|
||||
import {IOverlay} from './external';
|
||||
import {IOverlay, KeyCodes} from './external';
|
||||
import TypeFromDefaultImport from './external';
|
||||
|
||||
@Directive()
|
||||
export class MyDir {
|
||||
constructor(@Inject('$state') param: angular.IState,
|
||||
@Inject('$overlay') other: IOverlay,
|
||||
@Inject('$default') default: TypeFromDefaultImport) {}
|
||||
@Inject('$default') default: TypeFromDefaultImport,
|
||||
@Inject('$keyCodes') keyCodes: KeyCodes) {}
|
||||
}
|
||||
`);
|
||||
|
||||
@ -570,7 +572,8 @@ describe('downlevel decorator transform', () => {
|
||||
MyDir.ctorParameters = () => [
|
||||
{ type: undefined, decorators: [{ type: core_1.Inject, args: ['$state',] }] },
|
||||
{ type: undefined, decorators: [{ type: core_1.Inject, args: ['$overlay',] }] },
|
||||
{ type: undefined, decorators: [{ type: core_1.Inject, args: ['$default',] }] }
|
||||
{ type: undefined, decorators: [{ type: core_1.Inject, args: ['$default',] }] },
|
||||
{ type: undefined, decorators: [{ type: core_1.Inject, args: ['$keyCodes',] }] }
|
||||
];
|
||||
`);
|
||||
});
|
||||
@ -593,7 +596,7 @@ describe('downlevel decorator transform', () => {
|
||||
const {output} = transform(
|
||||
`
|
||||
import {Directive} from '@angular/core';
|
||||
|
||||
|
||||
export class MyInjectedClass {}
|
||||
|
||||
@Directive()
|
||||
@ -609,13 +612,36 @@ describe('downlevel decorator transform', () => {
|
||||
expect(output).not.toContain('tslib');
|
||||
});
|
||||
|
||||
it('should capture a non-const enum used as a constructor type', () => {
|
||||
const {output} = transform(`
|
||||
import {Component} from '@angular/core';
|
||||
|
||||
export enum Values {A, B};
|
||||
|
||||
@Component({template: 'hello'})
|
||||
export class MyComp {
|
||||
constructor(v: Values) {}
|
||||
}
|
||||
`);
|
||||
|
||||
expect(diagnostics.length).toBe(0);
|
||||
expect(output).toContain(dedent`
|
||||
MyComp.decorators = [
|
||||
{ type: core_1.Component, args: [{ template: 'hello' },] }
|
||||
];
|
||||
MyComp.ctorParameters = () => [
|
||||
{ type: Values }
|
||||
];`);
|
||||
expect(output).not.toContain('tslib');
|
||||
});
|
||||
|
||||
describe('class decorators skipped', () => {
|
||||
beforeEach(() => skipClassDecorators = true);
|
||||
|
||||
it('should not downlevel Angular class decorators', () => {
|
||||
const {output} = transform(`
|
||||
import {Injectable} from '@angular/core';
|
||||
|
||||
|
||||
@Injectable()
|
||||
export class MyService {}
|
||||
`);
|
||||
@ -632,10 +658,10 @@ describe('downlevel decorator transform', () => {
|
||||
it('should downlevel constructor parameters', () => {
|
||||
const {output} = transform(`
|
||||
import {Injectable} from '@angular/core';
|
||||
|
||||
|
||||
@Injectable()
|
||||
export class InjectClass {}
|
||||
|
||||
|
||||
@Injectable()
|
||||
export class MyService {
|
||||
constructor(dep: InjectClass) {}
|
||||
@ -658,10 +684,10 @@ describe('downlevel decorator transform', () => {
|
||||
it('should downlevel constructor parameter decorators', () => {
|
||||
const {output} = transform(`
|
||||
import {Injectable, Inject} from '@angular/core';
|
||||
|
||||
|
||||
@Injectable()
|
||||
export class InjectClass {}
|
||||
|
||||
|
||||
@Injectable()
|
||||
export class MyService {
|
||||
constructor(@Inject('test') dep: InjectClass) {}
|
||||
@ -684,7 +710,7 @@ describe('downlevel decorator transform', () => {
|
||||
it('should downlevel class member Angular decorators', () => {
|
||||
const {output} = transform(`
|
||||
import {Injectable, Input} from '@angular/core';
|
||||
|
||||
|
||||
export class MyService {
|
||||
@Input() disabled: boolean;
|
||||
}
|
||||
|
@ -26,7 +26,7 @@ export function isType(v: any): v is Type<any> {
|
||||
* @description
|
||||
*
|
||||
* Represents an abstract class `T`, if applied to a concrete class it would stop being
|
||||
* instantiatable.
|
||||
* instantiable.
|
||||
*
|
||||
* @publicApi
|
||||
*/
|
||||
|
@ -32,8 +32,9 @@ class DOMParserHelper implements InertBodyHelper {
|
||||
getInertBodyElement(html: string): HTMLElement|null {
|
||||
// We add these extra elements to ensure that the rest of the content is parsed as expected
|
||||
// e.g. leading whitespace is maintained and tags like `<meta>` do not get hoisted to the
|
||||
// `<head>` tag.
|
||||
html = '<body><remove></remove>' + html + '</body>';
|
||||
// `<head>` tag. Note that the `<body>` tag is closed implicitly to prevent unclosed tags
|
||||
// in `html` from consuming the otherwise explicit `</body>` tag.
|
||||
html = '<body><remove></remove>' + html;
|
||||
try {
|
||||
const body = new (window as any).DOMParser().parseFromString(html, 'text/html').body as
|
||||
HTMLBodyElement;
|
||||
|
@ -173,6 +173,27 @@ import {isDOMParserAvailable} from '../../src/sanitization/inert_body';
|
||||
expect(logMsgs.join('\n')).toMatch(/sanitizing HTML stripped some content/);
|
||||
});
|
||||
|
||||
it('should strip unclosed iframe tag', () => {
|
||||
expect(_sanitizeHtml(defaultDoc, '<iframe>')).toEqual('');
|
||||
expect([
|
||||
'<iframe>',
|
||||
// Double-escaped on IE
|
||||
'&lt;iframe&gt;'
|
||||
]).toContain(_sanitizeHtml(defaultDoc, '<iframe><iframe>'));
|
||||
expect([
|
||||
'<script>evil();</script>',
|
||||
// Double-escaped on IE
|
||||
'&lt;script&gt;evil();&lt;/script&gt;'
|
||||
]).toContain(_sanitizeHtml(defaultDoc, '<iframe><script>evil();</script>'));
|
||||
});
|
||||
|
||||
it('should ignore extraneous body tags', () => {
|
||||
expect(_sanitizeHtml(defaultDoc, '</body>')).toEqual('');
|
||||
expect(_sanitizeHtml(defaultDoc, 'foo</body>bar')).toEqual('foobar');
|
||||
expect(_sanitizeHtml(defaultDoc, 'foo<body>bar')).toEqual('foobar');
|
||||
expect(_sanitizeHtml(defaultDoc, 'fo<body>ob</body>ar')).toEqual('foobar');
|
||||
});
|
||||
|
||||
it('should not enter an infinite loop on clobbered elements', () => {
|
||||
// Some browsers are vulnerable to clobbered elements and will throw an expected exception
|
||||
// IE and EDGE does not seems to be affected by those cases
|
||||
|
@ -6,6 +6,7 @@ ts_library(
|
||||
name = "ivy",
|
||||
srcs = glob(["*.ts"]),
|
||||
deps = [
|
||||
"//packages/compiler",
|
||||
"//packages/compiler-cli",
|
||||
"//packages/compiler-cli/src/ngtsc/core",
|
||||
"//packages/compiler-cli/src/ngtsc/core:api",
|
||||
|
180
packages/language-service/ivy/hybrid_visitor.ts
Normal file
180
packages/language-service/ivy/hybrid_visitor.ts
Normal file
@ -0,0 +1,180 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {ParseSourceSpan} from '@angular/compiler';
|
||||
import * as e from '@angular/compiler/src/expression_parser/ast'; // e for expression AST
|
||||
import * as t from '@angular/compiler/src/render3/r3_ast'; // t for template AST
|
||||
|
||||
/**
|
||||
* Return the template AST node or expression AST node that most accurately
|
||||
* represents the node at the specified cursor `position`.
|
||||
* @param ast AST tree
|
||||
* @param position cursor position
|
||||
*/
|
||||
export function findNodeAtPosition(ast: t.Node[], position: number): t.Node|e.AST|undefined {
|
||||
const visitor = new R3Visitor(position);
|
||||
visitor.visitAll(ast);
|
||||
return visitor.path[visitor.path.length - 1];
|
||||
}
|
||||
|
||||
class R3Visitor implements t.Visitor {
|
||||
// We need to keep a path instead of the last node because we might need more
|
||||
// context for the last node, for example what is the parent node?
|
||||
readonly path: Array<t.Node|e.AST> = [];
|
||||
|
||||
// Position must be absolute in the source file.
|
||||
constructor(private readonly position: number) {}
|
||||
|
||||
visit(node: t.Node) {
|
||||
const {start, end} = getSpanIncludingEndTag(node);
|
||||
// Note both start and end are inclusive because we want to match conditions
|
||||
// like ¦start and end¦ where ¦ is the cursor.
|
||||
if (start <= this.position && this.position <= end) {
|
||||
const length = end - start;
|
||||
const last: t.Node|e.AST|undefined = this.path[this.path.length - 1];
|
||||
if (last) {
|
||||
const {start, end} = isTemplateNode(last) ? getSpanIncludingEndTag(last) : last.sourceSpan;
|
||||
const lastLength = end - start;
|
||||
if (length > lastLength) {
|
||||
// The current node has a span that is larger than the last node found
|
||||
// so we do not descend into it. This typically means we have found
|
||||
// a candidate in one of the root nodes so we do not need to visit
|
||||
// other root nodes.
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (node instanceof t.BoundEvent &&
|
||||
this.path.find((n => n instanceof t.BoundAttribute && node.name === n.name + 'Change'))) {
|
||||
// For two-way binding aka banana-in-a-box, there are two matches:
|
||||
// BoundAttribute and BoundEvent. Both have the same spans. We choose to
|
||||
// return BoundAttribute because it matches the identifier name verbatim.
|
||||
// TODO: For operations like go to definition, ideally we want to return
|
||||
// both.
|
||||
return;
|
||||
}
|
||||
this.path.push(node);
|
||||
node.visit(this);
|
||||
}
|
||||
}
|
||||
|
||||
visitElement(element: t.Element) {
|
||||
this.visitAll(element.attributes);
|
||||
this.visitAll(element.inputs);
|
||||
this.visitAll(element.outputs);
|
||||
this.visitAll(element.references);
|
||||
this.visitAll(element.children);
|
||||
}
|
||||
|
||||
visitTemplate(template: t.Template) {
|
||||
this.visitAll(template.attributes);
|
||||
this.visitAll(template.inputs);
|
||||
this.visitAll(template.outputs);
|
||||
this.visitAll(template.templateAttrs);
|
||||
this.visitAll(template.references);
|
||||
this.visitAll(template.variables);
|
||||
this.visitAll(template.children);
|
||||
}
|
||||
|
||||
visitContent(content: t.Content) {
|
||||
t.visitAll(this, content.attributes);
|
||||
}
|
||||
|
||||
visitVariable(variable: t.Variable) {
|
||||
// Variable has no template nodes or expression nodes.
|
||||
}
|
||||
|
||||
visitReference(reference: t.Reference) {
|
||||
// Reference has no template nodes or expression nodes.
|
||||
}
|
||||
|
||||
visitTextAttribute(attribute: t.TextAttribute) {
|
||||
// Text attribute has no template nodes or expression nodes.
|
||||
}
|
||||
|
||||
visitBoundAttribute(attribute: t.BoundAttribute) {
|
||||
const visitor = new ExpressionVisitor(this.position);
|
||||
visitor.visit(attribute.value, this.path);
|
||||
}
|
||||
|
||||
visitBoundEvent(attribute: t.BoundEvent) {
|
||||
const visitor = new ExpressionVisitor(this.position);
|
||||
visitor.visit(attribute.handler, this.path);
|
||||
}
|
||||
|
||||
visitText(text: t.Text) {
|
||||
// Text has no template nodes or expression nodes.
|
||||
}
|
||||
|
||||
visitBoundText(text: t.BoundText) {
|
||||
const visitor = new ExpressionVisitor(this.position);
|
||||
visitor.visit(text.value, this.path);
|
||||
}
|
||||
|
||||
visitIcu(icu: t.Icu) {
|
||||
for (const boundText of Object.values(icu.vars)) {
|
||||
this.visit(boundText);
|
||||
}
|
||||
for (const boundTextOrText of Object.values(icu.placeholders)) {
|
||||
this.visit(boundTextOrText);
|
||||
}
|
||||
}
|
||||
|
||||
visitAll(nodes: t.Node[]) {
|
||||
for (const node of nodes) {
|
||||
this.visit(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ExpressionVisitor extends e.RecursiveAstVisitor {
|
||||
// Position must be absolute in the source file.
|
||||
constructor(private readonly position: number) {
|
||||
super();
|
||||
}
|
||||
|
||||
visit(node: e.AST, path: Array<t.Node|e.AST>) {
|
||||
if (node instanceof e.ASTWithSource) {
|
||||
// In order to reduce noise, do not include `ASTWithSource` in the path.
|
||||
// For the purpose of source spans, there is no difference between
|
||||
// `ASTWithSource` and and underlying node that it wraps.
|
||||
node = node.ast;
|
||||
}
|
||||
const {start, end} = node.sourceSpan;
|
||||
// The third condition is to account for the implicit receiver, which should
|
||||
// not be visited.
|
||||
if (start <= this.position && this.position <= end && !(node instanceof e.ImplicitReceiver)) {
|
||||
path.push(node);
|
||||
node.visit(this, path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function isTemplateNode(node: t.Node|e.AST): node is t.Node {
|
||||
// Template node implements the Node interface so we cannot use instanceof.
|
||||
return node.sourceSpan instanceof ParseSourceSpan;
|
||||
}
|
||||
|
||||
export function isExpressionNode(node: t.Node|e.AST): node is e.AST {
|
||||
return node instanceof e.AST;
|
||||
}
|
||||
|
||||
function getSpanIncludingEndTag(ast: t.Node) {
|
||||
const result = {
|
||||
start: ast.sourceSpan.start.offset,
|
||||
end: ast.sourceSpan.end.offset,
|
||||
};
|
||||
// For Element and Template node, sourceSpan.end is the end of the opening
|
||||
// tag. For the purpose of language service, we need to actually recognize
|
||||
// the end of the closing tag. Otherwise, for situation like
|
||||
// <my-component></my-comp¦onent> where the cursor is in the closing tag
|
||||
// we will not be able to return any information.
|
||||
if ((ast instanceof t.Element || ast instanceof t.Template) && ast.endSourceSpan) {
|
||||
result.end = ast.endSourceSpan.end.offset;
|
||||
}
|
||||
return result;
|
||||
}
|
@ -5,6 +5,7 @@ ts_library(
|
||||
testonly = True,
|
||||
srcs = glob(["*.ts"]),
|
||||
deps = [
|
||||
"//packages/compiler",
|
||||
"//packages/language-service/ivy",
|
||||
"@npm//typescript",
|
||||
],
|
||||
|
@ -26,13 +26,13 @@ describe('diagnostic', () => {
|
||||
});
|
||||
|
||||
it('should report member does not exist', () => {
|
||||
const content = service.overwriteInlineTemplate(APP_COMPONENT, '{{ nope }}');
|
||||
const {text} = service.overwriteInlineTemplate(APP_COMPONENT, '{{ nope }}');
|
||||
const diags = ngLS.getSemanticDiagnostics(APP_COMPONENT);
|
||||
expect(diags.length).toBe(1);
|
||||
const {category, file, start, length, messageText} = diags[0];
|
||||
expect(category).toBe(ts.DiagnosticCategory.Error);
|
||||
expect(file?.fileName).toBe(APP_COMPONENT);
|
||||
expect(content.substring(start!, start! + length!)).toBe('nope');
|
||||
expect(text.substring(start!, start! + length!)).toBe('nope');
|
||||
expect(messageText).toBe(`Property 'nope' does not exist on type 'AppComponent'.`);
|
||||
});
|
||||
});
|
||||
|
568
packages/language-service/ivy/test/hybrid_visitor_spec.ts
Normal file
568
packages/language-service/ivy/test/hybrid_visitor_spec.ts
Normal file
@ -0,0 +1,568 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {ParseError, parseTemplate} from '@angular/compiler';
|
||||
import * as e from '@angular/compiler/src/expression_parser/ast'; // e for expression AST
|
||||
import * as t from '@angular/compiler/src/render3/r3_ast'; // t for template AST
|
||||
|
||||
import {findNodeAtPosition, isExpressionNode, isTemplateNode} from '../hybrid_visitor';
|
||||
|
||||
interface ParseResult {
|
||||
nodes: t.Node[];
|
||||
errors?: ParseError[];
|
||||
position: number;
|
||||
}
|
||||
|
||||
function parse(template: string): ParseResult {
|
||||
const position = template.indexOf('¦');
|
||||
if (position < 0) {
|
||||
throw new Error(`Template "${template}" does not contain the cursor`);
|
||||
}
|
||||
template = template.replace('¦', '');
|
||||
const templateUrl = '/foo';
|
||||
return {
|
||||
...parseTemplate(template, templateUrl),
|
||||
position,
|
||||
};
|
||||
}
|
||||
|
||||
describe('findNodeAtPosition for template AST', () => {
|
||||
it('should locate element in opening tag', () => {
|
||||
const {errors, nodes, position} = parse(`<di¦v></div>`);
|
||||
expect(errors).toBeUndefined();
|
||||
const node = findNodeAtPosition(nodes, position);
|
||||
expect(isTemplateNode(node!)).toBe(true);
|
||||
expect(node).toBeInstanceOf(t.Element);
|
||||
});
|
||||
|
||||
it('should locate element in closing tag', () => {
|
||||
const {errors, nodes, position} = parse(`<div></di¦v>`);
|
||||
expect(errors).toBeUndefined();
|
||||
const node = findNodeAtPosition(nodes, position);
|
||||
expect(isTemplateNode(node!)).toBe(true);
|
||||
expect(node).toBeInstanceOf(t.Element);
|
||||
});
|
||||
|
||||
it('should locate element when cursor is at the beginning', () => {
|
||||
const {errors, nodes, position} = parse(`<¦div></div>`);
|
||||
expect(errors).toBeUndefined();
|
||||
const node = findNodeAtPosition(nodes, position);
|
||||
expect(isTemplateNode(node!)).toBe(true);
|
||||
expect(node).toBeInstanceOf(t.Element);
|
||||
});
|
||||
|
||||
it('should locate element when cursor is at the end', () => {
|
||||
const {errors, nodes, position} = parse(`<div¦></div>`);
|
||||
expect(errors).toBeUndefined();
|
||||
const node = findNodeAtPosition(nodes, position);
|
||||
expect(isTemplateNode(node!)).toBe(true);
|
||||
expect(node).toBeInstanceOf(t.Element);
|
||||
});
|
||||
|
||||
it('should locate attribute key', () => {
|
||||
const {errors, nodes, position} = parse(`<div cla¦ss="foo"></div>`);
|
||||
expect(errors).toBeUndefined();
|
||||
const node = findNodeAtPosition(nodes, position);
|
||||
expect(isTemplateNode(node!)).toBe(true);
|
||||
expect(node).toBeInstanceOf(t.TextAttribute);
|
||||
});
|
||||
|
||||
it('should locate attribute value', () => {
|
||||
const {errors, nodes, position} = parse(`<div class="fo¦o"></div>`);
|
||||
expect(errors).toBeUndefined();
|
||||
const node = findNodeAtPosition(nodes, position);
|
||||
expect(isTemplateNode(node!)).toBe(true);
|
||||
// TODO: Note that we do not have the ability to detect the RHS (yet)
|
||||
expect(node).toBeInstanceOf(t.TextAttribute);
|
||||
});
|
||||
|
||||
it('should locate bound attribute key', () => {
|
||||
const {errors, nodes, position} = parse(`<test-cmp [fo¦o]="bar"></test-cmp>`);
|
||||
expect(errors).toBeUndefined();
|
||||
const node = findNodeAtPosition(nodes, position);
|
||||
expect(isTemplateNode(node!)).toBe(true);
|
||||
expect(node).toBeInstanceOf(t.BoundAttribute);
|
||||
});
|
||||
|
||||
it('should locate bound attribute value', () => {
|
||||
const {errors, nodes, position} = parse(`<test-cmp [foo]="b¦ar"></test-cmp>`);
|
||||
expect(errors).toBeUndefined();
|
||||
const node = findNodeAtPosition(nodes, position);
|
||||
expect(isExpressionNode(node!)).toBe(true);
|
||||
expect(node).toBeInstanceOf(e.PropertyRead);
|
||||
});
|
||||
|
||||
it('should locate bound event key', () => {
|
||||
const {errors, nodes, position} = parse(`<test-cmp (fo¦o)="bar()"></test-cmp>`);
|
||||
expect(errors).toBeUndefined();
|
||||
const node = findNodeAtPosition(nodes, position);
|
||||
expect(isTemplateNode(node!)).toBe(true);
|
||||
expect(node).toBeInstanceOf(t.BoundEvent);
|
||||
});
|
||||
|
||||
it('should locate bound event value', () => {
|
||||
const {errors, nodes, position} = parse(`<test-cmp (foo)="b¦ar()"></test-cmp>`);
|
||||
expect(errors).toBeUndefined();
|
||||
const node = findNodeAtPosition(nodes, position);
|
||||
expect(isExpressionNode(node!)).toBe(true);
|
||||
expect(node).toBeInstanceOf(e.MethodCall);
|
||||
});
|
||||
|
||||
it('should locate element children', () => {
|
||||
const {errors, nodes, position} = parse(`<div><sp¦an></span></div>`);
|
||||
expect(errors).toBeUndefined();
|
||||
const node = findNodeAtPosition(nodes, position);
|
||||
expect(isTemplateNode(node!)).toBe(true);
|
||||
expect(node).toBeInstanceOf(t.Element);
|
||||
expect((node as t.Element).name).toBe('span');
|
||||
});
|
||||
|
||||
it('should locate element reference', () => {
|
||||
const {errors, nodes, position} = parse(`<div #my¦div></div>`);
|
||||
expect(errors).toBeUndefined();
|
||||
const node = findNodeAtPosition(nodes, position);
|
||||
expect(isTemplateNode(node!)).toBe(true);
|
||||
expect(node).toBeInstanceOf(t.Reference);
|
||||
});
|
||||
|
||||
it('should locate template text attribute', () => {
|
||||
const {errors, nodes, position} = parse(`<ng-template ng¦If></ng-template>`);
|
||||
expect(errors).toBeUndefined();
|
||||
const node = findNodeAtPosition(nodes, position);
|
||||
expect(isTemplateNode(node!)).toBe(true);
|
||||
expect(node).toBeInstanceOf(t.TextAttribute);
|
||||
});
|
||||
|
||||
it('should locate template bound attribute key', () => {
|
||||
const {errors, nodes, position} = parse(`<ng-template [ng¦If]="foo"></ng-template>`);
|
||||
expect(errors).toBeUndefined();
|
||||
const node = findNodeAtPosition(nodes, position);
|
||||
expect(isTemplateNode(node!)).toBe(true);
|
||||
expect(node).toBeInstanceOf(t.BoundAttribute);
|
||||
});
|
||||
|
||||
it('should locate template bound attribute value', () => {
|
||||
const {errors, nodes, position} = parse(`<ng-template [ngIf]="f¦oo"></ng-template>`);
|
||||
expect(errors).toBeUndefined();
|
||||
const node = findNodeAtPosition(nodes, position);
|
||||
expect(isExpressionNode(node!)).toBe(true);
|
||||
expect(node).toBeInstanceOf(e.PropertyRead);
|
||||
});
|
||||
|
||||
it('should locate template bound attribute key in two-way binding', () => {
|
||||
const {errors, nodes, position} = parse(`<ng-template [(f¦oo)]="bar"></ng-template>`);
|
||||
expect(errors).toBeUndefined();
|
||||
const node = findNodeAtPosition(nodes, position);
|
||||
expect(isTemplateNode(node!)).toBe(true);
|
||||
expect(node).toBeInstanceOf(t.BoundAttribute);
|
||||
expect((node as t.BoundAttribute).name).toBe('foo');
|
||||
});
|
||||
|
||||
it('should locate template bound attribute value in two-way binding', () => {
|
||||
const {errors, nodes, position} = parse(`<ng-template [(foo)]="b¦ar"></ng-template>`);
|
||||
expect(errors).toBeUndefined();
|
||||
const node = findNodeAtPosition(nodes, position);
|
||||
expect(isExpressionNode(node!)).toBe(true);
|
||||
expect(node).toBeInstanceOf(e.PropertyRead);
|
||||
expect((node as e.PropertyRead).name).toBe('bar');
|
||||
});
|
||||
|
||||
it('should locate template bound event key', () => {
|
||||
const {errors, nodes, position} = parse(`<ng-template (cl¦ick)="foo()"></ng-template>`);
|
||||
expect(errors).toBeUndefined();
|
||||
const node = findNodeAtPosition(nodes, position);
|
||||
expect(isTemplateNode(node!)).toBe(true);
|
||||
expect(node).toBeInstanceOf(t.BoundEvent);
|
||||
});
|
||||
|
||||
it('should locate template bound event value', () => {
|
||||
const {errors, nodes, position} = parse(`<ng-template (click)="f¦oo()"></ng-template>`);
|
||||
expect(errors).toBeUndefined();
|
||||
const node = findNodeAtPosition(nodes, position);
|
||||
expect(node).toBeInstanceOf(e.MethodCall);
|
||||
});
|
||||
|
||||
it('should locate template attribute key', () => {
|
||||
const {errors, nodes, position} = parse(`<ng-template i¦d="foo"></ng-template>`);
|
||||
expect(errors).toBeUndefined();
|
||||
const node = findNodeAtPosition(nodes, position);
|
||||
expect(isTemplateNode(node!)).toBe(true);
|
||||
expect(node).toBeInstanceOf(t.TextAttribute);
|
||||
});
|
||||
|
||||
it('should locate template attribute value', () => {
|
||||
const {errors, nodes, position} = parse(`<ng-template id="f¦oo"></ng-template>`);
|
||||
expect(errors).toBeUndefined();
|
||||
const node = findNodeAtPosition(nodes, position);
|
||||
expect(isTemplateNode(node!)).toBe(true);
|
||||
// TODO: Note that we do not have the ability to detect the RHS (yet)
|
||||
expect(node).toBeInstanceOf(t.TextAttribute);
|
||||
});
|
||||
|
||||
it('should locate template reference key via the # notation', () => {
|
||||
const {errors, nodes, position} = parse(`<ng-template #f¦oo></ng-template>`);
|
||||
expect(errors).toBeUndefined();
|
||||
const node = findNodeAtPosition(nodes, position);
|
||||
expect(isTemplateNode(node!)).toBe(true);
|
||||
expect(node).toBeInstanceOf(t.Reference);
|
||||
expect((node as t.Reference).name).toBe('foo');
|
||||
});
|
||||
|
||||
it('should locate template reference key via the ref- notation', () => {
|
||||
const {errors, nodes, position} = parse(`<ng-template ref-fo¦o></ng-template>`);
|
||||
expect(errors).toBeUndefined();
|
||||
const node = findNodeAtPosition(nodes, position);
|
||||
expect(isTemplateNode(node!)).toBe(true);
|
||||
expect(node).toBeInstanceOf(t.Reference);
|
||||
expect((node as t.Reference).name).toBe('foo');
|
||||
});
|
||||
|
||||
it('should locate template reference value via the # notation', () => {
|
||||
const {errors, nodes, position} = parse(`<ng-template #foo="export¦As"></ng-template>`);
|
||||
expect(errors).toBeUndefined();
|
||||
const node = findNodeAtPosition(nodes, position);
|
||||
expect(isTemplateNode(node!)).toBe(true);
|
||||
expect(node).toBeInstanceOf(t.Reference);
|
||||
expect((node as t.Reference).value).toBe('exportAs');
|
||||
// TODO: Note that we do not have the ability to distinguish LHS and RHS
|
||||
});
|
||||
|
||||
it('should locate template reference value via the ref- notation', () => {
|
||||
const {errors, nodes, position} = parse(`<ng-template ref-foo="export¦As"></ng-template>`);
|
||||
expect(errors).toBeUndefined();
|
||||
const node = findNodeAtPosition(nodes, position);
|
||||
expect(isTemplateNode(node!)).toBe(true);
|
||||
expect(node).toBeInstanceOf(t.Reference);
|
||||
expect((node as t.Reference).value).toBe('exportAs');
|
||||
// TODO: Note that we do not have the ability to distinguish LHS and RHS
|
||||
});
|
||||
|
||||
it('should locate template variable key', () => {
|
||||
const {errors, nodes, position} = parse(`<ng-template let-f¦oo="bar"></ng-template>`);
|
||||
expect(errors).toBeUndefined();
|
||||
const node = findNodeAtPosition(nodes, position);
|
||||
expect(isTemplateNode(node!)).toBe(true);
|
||||
expect(node).toBeInstanceOf(t.Variable);
|
||||
});
|
||||
|
||||
it('should locate template variable value', () => {
|
||||
const {errors, nodes, position} = parse(`<ng-template let-foo="b¦ar"></ng-template>`);
|
||||
expect(errors).toBeUndefined();
|
||||
const node = findNodeAtPosition(nodes, position);
|
||||
expect(isTemplateNode(node!)).toBe(true);
|
||||
expect(node).toBeInstanceOf(t.Variable);
|
||||
});
|
||||
|
||||
it('should locate template children', () => {
|
||||
const {errors, nodes, position} = parse(`<ng-template><d¦iv></div></ng-template>`);
|
||||
expect(errors).toBeUndefined();
|
||||
const node = findNodeAtPosition(nodes, position);
|
||||
expect(isTemplateNode(node!)).toBe(true);
|
||||
expect(node).toBeInstanceOf(t.Element);
|
||||
});
|
||||
|
||||
it('should locate ng-content', () => {
|
||||
const {errors, nodes, position} = parse(`<ng-co¦ntent></ng-content>`);
|
||||
expect(errors).toBeUndefined();
|
||||
const node = findNodeAtPosition(nodes, position);
|
||||
expect(isTemplateNode(node!)).toBe(true);
|
||||
expect(node).toBeInstanceOf(t.Content);
|
||||
});
|
||||
|
||||
it('should locate ng-content attribute key', () => {
|
||||
const {errors, nodes, position} = parse('<ng-content cla¦ss="red"></ng-content>');
|
||||
expect(errors).toBeUndefined();
|
||||
const node = findNodeAtPosition(nodes, position);
|
||||
expect(isTemplateNode(node!)).toBe(true);
|
||||
expect(node).toBeInstanceOf(t.TextAttribute);
|
||||
});
|
||||
|
||||
it('should locate ng-content attribute value', () => {
|
||||
const {errors, nodes, position} = parse('<ng-content class="r¦ed"></ng-content>');
|
||||
expect(errors).toBeUndefined();
|
||||
const node = findNodeAtPosition(nodes, position);
|
||||
// TODO: Note that we do not have the ability to detect the RHS (yet)
|
||||
expect(isTemplateNode(node!)).toBe(true);
|
||||
expect(node).toBeInstanceOf(t.TextAttribute);
|
||||
});
|
||||
|
||||
it('should not locate implicit receiver', () => {
|
||||
const {errors, nodes, position} = parse(`<div [foo]="¦bar"></div>`);
|
||||
expect(errors).toBeUndefined();
|
||||
const node = findNodeAtPosition(nodes, position);
|
||||
expect(isExpressionNode(node!)).toBe(true);
|
||||
expect(node).toBeInstanceOf(e.PropertyRead);
|
||||
});
|
||||
|
||||
it('should locate bound attribute key in two-way binding', () => {
|
||||
const {errors, nodes, position} = parse(`<cmp [(f¦oo)]="bar"></cmp>`);
|
||||
expect(errors).toBeUndefined();
|
||||
const node = findNodeAtPosition(nodes, position);
|
||||
expect(isTemplateNode(node!)).toBe(true);
|
||||
expect(node).toBeInstanceOf(t.BoundAttribute);
|
||||
expect((node as t.BoundAttribute).name).toBe('foo');
|
||||
});
|
||||
|
||||
it('should locate bound attribute value in two-way binding', () => {
|
||||
const {errors, nodes, position} = parse(`<cmp [(foo)]="b¦ar"></cmp>`);
|
||||
expect(errors).toBeUndefined();
|
||||
const node = findNodeAtPosition(nodes, position);
|
||||
expect(isExpressionNode(node!)).toBe(true);
|
||||
expect(node).toBeInstanceOf(e.PropertyRead);
|
||||
expect((node as e.PropertyRead).name).toBe('bar');
|
||||
});
|
||||
});
|
||||
|
||||
describe('findNodeAtPosition for expression AST', () => {
|
||||
it('should not locate implicit receiver', () => {
|
||||
const {errors, nodes, position} = parse(`{{ ¦title }}`);
|
||||
expect(errors).toBeUndefined();
|
||||
const node = findNodeAtPosition(nodes, position);
|
||||
expect(isExpressionNode(node!)).toBe(true);
|
||||
expect(node).toBeInstanceOf(e.PropertyRead);
|
||||
expect((node as e.PropertyRead).name).toBe('title');
|
||||
});
|
||||
|
||||
it('should locate property read', () => {
|
||||
const {errors, nodes, position} = parse(`{{ ti¦tle }}`);
|
||||
expect(errors).toBeUndefined();
|
||||
const node = findNodeAtPosition(nodes, position);
|
||||
expect(isExpressionNode(node!)).toBe(true);
|
||||
expect(node).toBeInstanceOf(e.PropertyRead);
|
||||
expect((node as e.PropertyRead).name).toBe('title');
|
||||
});
|
||||
|
||||
it('should locate safe property read', () => {
|
||||
const {errors, nodes, position} = parse(`{{ foo?¦.bar }}`);
|
||||
expect(errors).toBeUndefined();
|
||||
const node = findNodeAtPosition(nodes, position);
|
||||
expect(isExpressionNode(node!)).toBe(true);
|
||||
expect(node).toBeInstanceOf(e.SafePropertyRead);
|
||||
expect((node as e.SafePropertyRead).name).toBe('bar');
|
||||
});
|
||||
|
||||
it('should locate keyed read', () => {
|
||||
const {errors, nodes, position} = parse(`{{ foo['bar']¦ }}`);
|
||||
expect(errors).toBeUndefined();
|
||||
const node = findNodeAtPosition(nodes, position);
|
||||
expect(isExpressionNode(node!)).toBe(true);
|
||||
expect(node).toBeInstanceOf(e.KeyedRead);
|
||||
});
|
||||
|
||||
it('should locate property write', () => {
|
||||
const {errors, nodes, position} = parse(`<div (foo)="b¦ar=$event"></div>`);
|
||||
expect(errors).toBeUndefined();
|
||||
const node = findNodeAtPosition(nodes, position);
|
||||
expect(isExpressionNode(node!)).toBe(true);
|
||||
expect(node).toBeInstanceOf(e.PropertyWrite);
|
||||
});
|
||||
|
||||
it('should locate keyed write', () => {
|
||||
const {errors, nodes, position} = parse(`<div (foo)="bar['baz']¦=$event"></div>`);
|
||||
expect(errors).toBeUndefined();
|
||||
const node = findNodeAtPosition(nodes, position);
|
||||
expect(isExpressionNode(node!)).toBe(true);
|
||||
expect(node).toBeInstanceOf(e.KeyedWrite);
|
||||
});
|
||||
|
||||
it('should locate binary', () => {
|
||||
const {errors, nodes, position} = parse(`{{ 1 +¦ 2 }}`);
|
||||
expect(errors).toBeUndefined();
|
||||
const node = findNodeAtPosition(nodes, position);
|
||||
expect(isExpressionNode(node!)).toBe(true);
|
||||
expect(node).toBeInstanceOf(e.Binary);
|
||||
});
|
||||
|
||||
it('should locate binding pipe with an identifier', () => {
|
||||
const {errors, nodes, position} = parse(`{{ title | p¦ }}`);
|
||||
expect(errors).toBeUndefined();
|
||||
const node = findNodeAtPosition(nodes, position);
|
||||
expect(isExpressionNode(node!)).toBe(true);
|
||||
expect(node).toBeInstanceOf(e.BindingPipe);
|
||||
});
|
||||
|
||||
it('should locate binding pipe without identifier',
|
||||
() => {
|
||||
// TODO: We are not able to locate pipe if identifier is missing because the
|
||||
// parser throws an error. This case is important for autocomplete.
|
||||
// const {errors, nodes, position} = parse(`{{ title | ¦ }}`);
|
||||
// expect(errors).toBeUndefined();
|
||||
// const node = findNodeAtPosition(nodes, position);
|
||||
// expect(isExpressionNode(node!)).toBe(true);
|
||||
// expect(node).toBeInstanceOf(e.BindingPipe);
|
||||
});
|
||||
|
||||
it('should locate method call', () => {
|
||||
const {errors, nodes, position} = parse(`{{ title.toString(¦) }}`);
|
||||
expect(errors).toBeUndefined();
|
||||
const node = findNodeAtPosition(nodes, position);
|
||||
expect(isExpressionNode(node!)).toBe(true);
|
||||
expect(node).toBeInstanceOf(e.MethodCall);
|
||||
});
|
||||
|
||||
it('should locate safe method call', () => {
|
||||
const {errors, nodes, position} = parse(`{{ title?.toString(¦) }}`);
|
||||
expect(errors).toBeUndefined();
|
||||
const node = findNodeAtPosition(nodes, position);
|
||||
expect(isExpressionNode(node!)).toBe(true);
|
||||
expect(node).toBeInstanceOf(e.SafeMethodCall);
|
||||
});
|
||||
|
||||
it('should locate literal primitive in interpolation', () => {
|
||||
const {errors, nodes, position} = parse(`{{ title.indexOf('t¦') }}`);
|
||||
expect(errors).toBeUndefined();
|
||||
const node = findNodeAtPosition(nodes, position);
|
||||
expect(isExpressionNode(node!)).toBe(true);
|
||||
expect(node).toBeInstanceOf(e.LiteralPrimitive);
|
||||
expect((node as e.LiteralPrimitive).value).toBe('t');
|
||||
});
|
||||
|
||||
it('should locate literal primitive in binding', () => {
|
||||
const {errors, nodes, position} = parse(`<div [id]="'t¦'"></div>`);
|
||||
expect(errors).toBeUndefined();
|
||||
const node = findNodeAtPosition(nodes, position);
|
||||
expect(isExpressionNode(node!)).toBe(true);
|
||||
expect(node).toBeInstanceOf(e.LiteralPrimitive);
|
||||
expect((node as e.LiteralPrimitive).value).toBe('t');
|
||||
});
|
||||
|
||||
it('should locate empty expression', () => {
|
||||
const {errors, nodes, position} = parse(`<div [id]="¦"></div>`);
|
||||
expect(errors).toBeUndefined();
|
||||
const node = findNodeAtPosition(nodes, position);
|
||||
expect(isExpressionNode(node!)).toBe(true);
|
||||
expect(node).toBeInstanceOf(e.EmptyExpr);
|
||||
});
|
||||
|
||||
it('should locate literal array', () => {
|
||||
const {errors, nodes, position} = parse(`{{ [1, 2,¦ 3] }}`);
|
||||
expect(errors).toBeUndefined();
|
||||
const node = findNodeAtPosition(nodes, position);
|
||||
expect(isExpressionNode(node!)).toBe(true);
|
||||
expect(node).toBeInstanceOf(e.LiteralArray);
|
||||
});
|
||||
|
||||
it('should locate literal map', () => {
|
||||
const {errors, nodes, position} = parse(`{{ { hello:¦ "world" } }}`);
|
||||
expect(errors).toBeUndefined();
|
||||
const node = findNodeAtPosition(nodes, position);
|
||||
expect(isExpressionNode(node!)).toBe(true);
|
||||
expect(node).toBeInstanceOf(e.LiteralMap);
|
||||
});
|
||||
|
||||
it('should locate conditional', () => {
|
||||
const {errors, nodes, position} = parse(`{{ cond ?¦ true : false }}`);
|
||||
expect(errors).toBeUndefined();
|
||||
const node = findNodeAtPosition(nodes, position);
|
||||
expect(isExpressionNode(node!)).toBe(true);
|
||||
expect(node).toBeInstanceOf(e.Conditional);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findNodeAtPosition for microsyntax expression', () => {
|
||||
it('should locate template key', () => {
|
||||
const {errors, nodes, position} = parse(`<div *ng¦If="foo"></div>`);
|
||||
expect(errors).toBeUndefined();
|
||||
const node = findNodeAtPosition(nodes, position);
|
||||
expect(isTemplateNode(node!)).toBe(true);
|
||||
expect(node).toBeInstanceOf(t.BoundAttribute);
|
||||
});
|
||||
|
||||
it('should locate template value', () => {
|
||||
const {errors, nodes, position} = parse(`<div *ngIf="f¦oo"></div>`);
|
||||
expect(errors).toBeUndefined();
|
||||
const node = findNodeAtPosition(nodes, position);
|
||||
expect(isExpressionNode(node!)).toBe(true);
|
||||
expect(node).toBeInstanceOf(e.PropertyRead);
|
||||
});
|
||||
|
||||
it('should locate text attribute', () => {
|
||||
const {errors, nodes, position} = parse(`<div *ng¦For="let item of items"></div>`);
|
||||
// ngFor is a text attribute because the desugared form is
|
||||
// <ng-template ngFor let-item [ngForOf]="items">
|
||||
expect(errors).toBeUndefined();
|
||||
const node = findNodeAtPosition(nodes, position);
|
||||
// TODO: this is currently wrong because it should point to ngFor text
|
||||
// attribute instead of ngForOf bound attribute
|
||||
});
|
||||
|
||||
it('should locate not let keyword', () => {
|
||||
const {errors, nodes, position} = parse(`<div *ngFor="l¦et item of items"></div>`);
|
||||
expect(errors).toBeUndefined();
|
||||
const node = findNodeAtPosition(nodes, position);
|
||||
// TODO: this is currently wrong because node is currently pointing to
|
||||
// "item". In this case, it should return undefined.
|
||||
});
|
||||
|
||||
it('should locate let variable', () => {
|
||||
const {errors, nodes, position} = parse(`<div *ngFor="let i¦tem of items"></div>`);
|
||||
expect(errors).toBeUndefined();
|
||||
const node = findNodeAtPosition(nodes, position);
|
||||
expect(isTemplateNode(node!)).toBe(true);
|
||||
expect(node).toBeInstanceOf(t.Variable);
|
||||
expect((node as t.Variable).name).toBe('item');
|
||||
});
|
||||
|
||||
it('should locate bound attribute key', () => {
|
||||
const {errors, nodes, position} = parse(`<div *ngFor="let item o¦f items"></div>`);
|
||||
expect(errors).toBeUndefined();
|
||||
const node = findNodeAtPosition(nodes, position);
|
||||
expect(isTemplateNode(node!)).toBe(true);
|
||||
expect(node).toBeInstanceOf(t.BoundAttribute);
|
||||
expect((node as t.BoundAttribute).name).toBe('ngForOf');
|
||||
});
|
||||
|
||||
it('should locate bound attribute value', () => {
|
||||
const {errors, nodes, position} = parse(`<div *ngFor="let item of it¦ems"></div>`);
|
||||
expect(errors).toBeUndefined();
|
||||
const node = findNodeAtPosition(nodes, position);
|
||||
expect(isExpressionNode(node!)).toBe(true);
|
||||
expect(node).toBeInstanceOf(e.PropertyRead);
|
||||
expect((node as e.PropertyRead).name).toBe('items');
|
||||
});
|
||||
|
||||
it('should locate template children', () => {
|
||||
const {errors, nodes, position} = parse(`<di¦v *ngIf></div>`);
|
||||
expect(errors).toBeUndefined();
|
||||
const node = findNodeAtPosition(nodes, position);
|
||||
expect(isTemplateNode(node!)).toBe(true);
|
||||
expect(node).toBeInstanceOf(t.Element);
|
||||
expect((node as t.Element).name).toBe('div');
|
||||
});
|
||||
|
||||
it('should locate property read of variable declared within template', () => {
|
||||
const {errors, nodes, position} = parse(`
|
||||
<div *ngFor="let item of items; let i=index">
|
||||
{{ i¦ }}
|
||||
</div>`);
|
||||
expect(errors).toBeUndefined();
|
||||
const node = findNodeAtPosition(nodes, position);
|
||||
expect(isExpressionNode(node!)).toBe(true);
|
||||
expect(node).toBeInstanceOf(e.PropertyRead);
|
||||
});
|
||||
|
||||
it('should locate LHS of variable declaration', () => {
|
||||
const {errors, nodes, position} = parse(`<div *ngFor="let item of items; let i¦=index">`);
|
||||
expect(errors).toBeUndefined();
|
||||
const node = findNodeAtPosition(nodes, position);
|
||||
expect(isTemplateNode(node!)).toBe(true);
|
||||
expect(node).toBeInstanceOf(t.Variable);
|
||||
// TODO: Currently there is no way to distinguish LHS from RHS
|
||||
expect((node as t.Variable).name).toBe('i');
|
||||
});
|
||||
|
||||
it('should locate RHS of variable declaration', () => {
|
||||
const {errors, nodes, position} = parse(`<div *ngFor="let item of items; let i=in¦dex">`);
|
||||
expect(errors).toBeUndefined();
|
||||
const node = findNodeAtPosition(nodes, position);
|
||||
expect(isTemplateNode(node!)).toBe(true);
|
||||
expect(node).toBeInstanceOf(t.Variable);
|
||||
// TODO: Currently there is no way to distinguish LHS from RHS
|
||||
expect((node as t.Variable).value).toBe('index');
|
||||
});
|
||||
});
|
@ -105,6 +105,17 @@ export function setup() {
|
||||
};
|
||||
}
|
||||
|
||||
interface OverwriteResult {
|
||||
/**
|
||||
* Position of the cursor, -1 if there isn't one.
|
||||
*/
|
||||
position: number;
|
||||
/**
|
||||
* Overwritten content without the cursor.
|
||||
*/
|
||||
text: string;
|
||||
}
|
||||
|
||||
class MockService {
|
||||
private readonly overwritten = new Set<ts.server.NormalizedPath>();
|
||||
|
||||
@ -113,20 +124,32 @@ class MockService {
|
||||
private readonly ps: ts.server.ProjectService,
|
||||
) {}
|
||||
|
||||
overwrite(fileName: string, newText: string): string {
|
||||
/**
|
||||
* Overwrite the entire content of `fileName` with `newText`. If cursor is
|
||||
* present in `newText`, it will be removed and the position of the cursor
|
||||
* will be returned.
|
||||
*/
|
||||
overwrite(fileName: string, newText: string): OverwriteResult {
|
||||
const scriptInfo = this.getScriptInfo(fileName);
|
||||
this.overwriteScriptInfo(scriptInfo, preprocess(newText));
|
||||
return newText;
|
||||
return this.overwriteScriptInfo(scriptInfo, newText);
|
||||
}
|
||||
|
||||
overwriteInlineTemplate(fileName: string, newTemplate: string): string {
|
||||
/**
|
||||
* Overwrite an inline template defined in `fileName` and return the entire
|
||||
* content of the source file (not just the template). If a cursor is present
|
||||
* in `newTemplate`, it will be removed and the position of the cursor in the
|
||||
* source file will be returned.
|
||||
*/
|
||||
overwriteInlineTemplate(fileName: string, newTemplate: string): OverwriteResult {
|
||||
const scriptInfo = this.getScriptInfo(fileName);
|
||||
const snapshot = scriptInfo.getSnapshot();
|
||||
const originalContent = snapshot.getText(0, snapshot.getLength());
|
||||
const newContent =
|
||||
originalContent.replace(/template: `([\s\S]+)`/, `template: \`${newTemplate}\``);
|
||||
this.overwriteScriptInfo(scriptInfo, preprocess(newContent));
|
||||
return newContent;
|
||||
const originalText = snapshot.getText(0, snapshot.getLength());
|
||||
const {position, text} =
|
||||
replaceOnce(originalText, /template: `([\s\S]+?)`/, `template: \`${newTemplate}\``);
|
||||
if (position === -1) {
|
||||
throw new Error(`${fileName} does not contain a component with template`);
|
||||
}
|
||||
return this.overwriteScriptInfo(scriptInfo, text);
|
||||
}
|
||||
|
||||
reset() {
|
||||
@ -151,16 +174,37 @@ class MockService {
|
||||
return scriptInfo;
|
||||
}
|
||||
|
||||
private overwriteScriptInfo(scriptInfo: ts.server.ScriptInfo, newText: string) {
|
||||
/**
|
||||
* Remove the cursor from `newText`, then replace `scriptInfo` with the new
|
||||
* content and return the position of the cursor.
|
||||
* @param scriptInfo
|
||||
* @param newText Text that possibly contains a cursor
|
||||
*/
|
||||
private overwriteScriptInfo(scriptInfo: ts.server.ScriptInfo, newText: string): OverwriteResult {
|
||||
const result = replaceOnce(newText, /¦/, '');
|
||||
const snapshot = scriptInfo.getSnapshot();
|
||||
scriptInfo.editContent(0, snapshot.getLength(), newText);
|
||||
scriptInfo.editContent(0, snapshot.getLength(), result.text);
|
||||
this.overwritten.add(scriptInfo.fileName);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
const REGEX_CURSOR = /¦/g;
|
||||
function preprocess(text: string): string {
|
||||
return text.replace(REGEX_CURSOR, '');
|
||||
/**
|
||||
* Replace at most one occurence that matches `regex` in the specified
|
||||
* `searchText` with the specified `replaceText`. Throw an error if there is
|
||||
* more than one occurrence.
|
||||
*/
|
||||
function replaceOnce(searchText: string, regex: RegExp, replaceText: string): OverwriteResult {
|
||||
regex = new RegExp(regex.source, regex.flags + 'g' /* global */);
|
||||
let position = -1;
|
||||
const text = searchText.replace(regex, (...args) => {
|
||||
if (position !== -1) {
|
||||
throw new Error(`${regex} matches more than one occurrence in text: ${searchText}`);
|
||||
}
|
||||
position = args[args.length - 2]; // second last argument is always the index
|
||||
return replaceText;
|
||||
});
|
||||
return {position, text};
|
||||
}
|
||||
|
||||
const REF_MARKER = /«(((\w|\-)+)|([^ᐱ]*ᐱ(\w+)ᐱ.[^»]*))»/g;
|
||||
|
@ -8,7 +8,7 @@
|
||||
|
||||
import * as ts from 'typescript/lib/tsserverlibrary';
|
||||
|
||||
import {APP_MAIN, setup, TEST_SRCDIR} from './mock_host';
|
||||
import {APP_COMPONENT, APP_MAIN, setup, TEST_SRCDIR} from './mock_host';
|
||||
|
||||
describe('mock host', () => {
|
||||
const {project, service, tsLS} = setup();
|
||||
@ -58,13 +58,93 @@ describe('mock host', () => {
|
||||
expect(getText(scriptInfo)).toBe('const x: string = 0');
|
||||
});
|
||||
|
||||
it('can find the cursor', () => {
|
||||
const content = service.overwrite(APP_MAIN, `const fo¦o = 'hello world';`);
|
||||
// content returned by overwrite() is the original content with cursor
|
||||
expect(content).toBe(`const fo¦o = 'hello world';`);
|
||||
const scriptInfo = service.getScriptInfo(APP_MAIN);
|
||||
// script info content should not contain cursor
|
||||
expect(getText(scriptInfo)).toBe(`const foo = 'hello world';`);
|
||||
describe('overwrite()', () => {
|
||||
it('will return the cursor position', () => {
|
||||
const {position} = service.overwrite(APP_MAIN, `const fo¦o = 'hello world';`);
|
||||
expect(position).toBe(8);
|
||||
});
|
||||
|
||||
it('will remove the cursor in overwritten text', () => {
|
||||
const {text} = service.overwrite(APP_MAIN, `const fo¦o = 'hello world';`);
|
||||
expect(text).toBe(`const foo = 'hello world';`);
|
||||
});
|
||||
|
||||
it('will update script info without cursor', () => {
|
||||
const {text} = service.overwrite(APP_MAIN, `const fo¦o = 'hello world';`);
|
||||
const scriptInfo = service.getScriptInfo(APP_MAIN);
|
||||
const snapshot = getText(scriptInfo);
|
||||
expect(snapshot).toBe(`const foo = 'hello world';`);
|
||||
expect(snapshot).toBe(text);
|
||||
});
|
||||
|
||||
it('will throw if there is more than one cursor', () => {
|
||||
expect(() => service.overwrite(APP_MAIN, `const f¦oo = 'hello wo¦rld';`))
|
||||
.toThrowError(/matches more than one occurrence in text/);
|
||||
});
|
||||
|
||||
it('will return -1 if cursor is not present', () => {
|
||||
const {position} = service.overwrite(APP_MAIN, `const foo = 'hello world';`);
|
||||
expect(position).toBe(-1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('overwriteInlineTemplate()', () => {
|
||||
it('will return the cursor position', () => {
|
||||
const {position, text} = service.overwriteInlineTemplate(APP_COMPONENT, `{{ fo¦o }}`);
|
||||
// The position returned should be relative to the start of the source
|
||||
// file, not the start of the template.
|
||||
expect(position).not.toBe(5);
|
||||
expect(text.substring(position, position + 4)).toBe('o }}');
|
||||
});
|
||||
|
||||
it('will remove the cursor in overwritten text', () => {
|
||||
const {text} = service.overwriteInlineTemplate(APP_COMPONENT, `{{ fo¦o }}`);
|
||||
expect(text).toContain(`{{ foo }}`);
|
||||
});
|
||||
|
||||
it('will return the entire content of the source file', () => {
|
||||
const {text} = service.overwriteInlineTemplate(APP_COMPONENT, `{{ foo }}`);
|
||||
expect(text).toContain(`@Component`);
|
||||
});
|
||||
|
||||
it('will update script info without cursor', () => {
|
||||
service.overwriteInlineTemplate(APP_COMPONENT, `{{ fo¦o }}`);
|
||||
const scriptInfo = service.getScriptInfo(APP_COMPONENT);
|
||||
expect(getText(scriptInfo)).toContain(`{{ foo }}`);
|
||||
});
|
||||
|
||||
it('will throw if there is no template in file', () => {
|
||||
expect(() => service.overwriteInlineTemplate(APP_MAIN, `{{ foo }}`))
|
||||
.toThrowError(/does not contain a component with template/);
|
||||
});
|
||||
|
||||
it('will throw if there is more than one cursor', () => {
|
||||
expect(() => service.overwriteInlineTemplate(APP_COMPONENT, `{{ f¦o¦o }}`))
|
||||
.toThrowError(/matches more than one occurrence in text/);
|
||||
});
|
||||
|
||||
it('will return -1 if cursor is not present', () => {
|
||||
const {position} = service.overwriteInlineTemplate(APP_COMPONENT, `{{ foo }}`);
|
||||
expect(position).toBe(-1);
|
||||
});
|
||||
|
||||
it('will throw if there is more than one component with template', () => {
|
||||
service.overwrite(APP_COMPONENT, `
|
||||
import {Component} from '@angular/core';
|
||||
|
||||
@Component({
|
||||
template: \`<h1></h1>\`,
|
||||
})
|
||||
export class ComponentA {}
|
||||
|
||||
@Component({
|
||||
template: \`<h2></h2>\`,
|
||||
})
|
||||
export class ComponentB {}
|
||||
`);
|
||||
expect(() => service.overwriteInlineTemplate(APP_COMPONENT, `<p></p>`))
|
||||
.toThrowError(/matches more than one occurrence in text/);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -75,29 +75,9 @@ export class MessageSerializer<T> extends BaseVisitor {
|
||||
}
|
||||
|
||||
visitContainedNodes(nodes: Node[]): void {
|
||||
const length = nodes.length;
|
||||
let index = 0;
|
||||
while (index < length) {
|
||||
if (!this.isPlaceholderContainer(nodes[index])) {
|
||||
const startOfContainedNodes = index;
|
||||
while (index < length - 1) {
|
||||
index++;
|
||||
if (this.isPlaceholderContainer(nodes[index])) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (index - startOfContainedNodes > 1) {
|
||||
// Only create a container if there are two or more contained Nodes in a row
|
||||
this.renderer.startContainer();
|
||||
visitAll(this, nodes.slice(startOfContainedNodes, index - 1));
|
||||
this.renderer.closeContainer();
|
||||
}
|
||||
}
|
||||
if (index < length) {
|
||||
nodes[index].visit(this, undefined);
|
||||
}
|
||||
index++;
|
||||
}
|
||||
this.renderer.startContainer();
|
||||
visitAll(this, nodes);
|
||||
this.renderer.closeContainer();
|
||||
}
|
||||
|
||||
visitPlaceholder(name: string, body: string|undefined): void {
|
||||
|
@ -158,6 +158,46 @@ describe('Xliff1TranslationParser', () => {
|
||||
['INTERPOLATION', 'START_BOLD_TEXT', 'CLOSE_BOLD_TEXT']));
|
||||
});
|
||||
|
||||
it('should extract nested placeholder containers (i.e. nested HTML elements)', () => {
|
||||
/**
|
||||
* Source HTML:
|
||||
*
|
||||
* ```
|
||||
* <div i18n>
|
||||
* translatable <span>element <b>with placeholders</b></span> {{ interpolation}}
|
||||
* </div>
|
||||
* ```
|
||||
*/
|
||||
const XLIFF = [
|
||||
`<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">`,
|
||||
` <file source-language="en" target-language="fr" datatype="plaintext" original="ng2.template">`,
|
||||
` <body>`,
|
||||
` <trans-unit id="9051630253697141670" datatype="html">`,
|
||||
` <source>translatable <x id="START_TAG_SPAN"/>element <x id="START_BOLD_TEXT"/>with placeholders<x id="CLOSE_BOLD_TEXT"/><x id="CLOSE_TAG_SPAN"/> <x id="INTERPOLATION"/></source>`,
|
||||
` <target><x id="START_TAG_SPAN"/><x id="INTERPOLATION"/> tnemele<x id="CLOSE_TAG_SPAN"/> elbatalsnart <x id="START_BOLD_TEXT"/>sredlohecalp htiw<x id="CLOSE_BOLD_TEXT"/></target>`,
|
||||
` <context-group purpose="location">`,
|
||||
` <context context-type="sourcefile">file.ts</context>`,
|
||||
` <context context-type="linenumber">3</context>`,
|
||||
` </context-group>`,
|
||||
` </trans-unit>`,
|
||||
` </body>`,
|
||||
` </file>`,
|
||||
`</xliff>`,
|
||||
].join('\n');
|
||||
const result = doParse('/some/file.xlf', XLIFF);
|
||||
expect(result.translations[ɵcomputeMsgId(
|
||||
'translatable {$START_TAG_SPAN}element {$START_BOLD_TEXT}with placeholders' +
|
||||
'{$CLOSE_BOLD_TEXT}{$CLOSE_TAG_SPAN} {$INTERPOLATION}')])
|
||||
.toEqual(ɵmakeParsedTranslation(
|
||||
['', '', ' tnemele', ' elbatalsnart ', 'sredlohecalp htiw', ''], [
|
||||
'START_TAG_SPAN',
|
||||
'INTERPOLATION',
|
||||
'CLOSE_TAG_SPAN',
|
||||
'START_BOLD_TEXT',
|
||||
'CLOSE_BOLD_TEXT',
|
||||
]));
|
||||
});
|
||||
|
||||
it('should extract translations with placeholders containing hyphens', () => {
|
||||
/**
|
||||
* Source HTML:
|
||||
|
@ -114,13 +114,13 @@ describe(
|
||||
* Source HTML:
|
||||
*
|
||||
* ```
|
||||
* <div i18n>translatable element <b>>with placeholders</b> {{ interpolation}}</div>
|
||||
* <div i18n>translatable element <b>with placeholders</b> {{ interpolation}}</div>
|
||||
* ```
|
||||
*/
|
||||
const XLIFF = [
|
||||
`<xliff version="2.0" xmlns="urn:oasis:names:tc:xliff:document:2.0" srcLang="en" trgLang="fr">`,
|
||||
` <file original="ng.template" id="ngi18n">`,
|
||||
` <unit id="5057824347511785081">`,
|
||||
` <unit id="6949438802869886378">`,
|
||||
` <notes>`,
|
||||
` <note category="location">file.ts:3</note>`,
|
||||
` </notes>`,
|
||||
@ -135,12 +135,58 @@ describe(
|
||||
const result = doParse('/some/file.xlf', XLIFF);
|
||||
expect(
|
||||
result.translations[ɵcomputeMsgId(
|
||||
'translatable element {$START_BOLD_TEXT}with placeholders{$LOSE_BOLD_TEXT} {$INTERPOLATION}')])
|
||||
'translatable element {$START_BOLD_TEXT}with placeholders{$CLOSE_BOLD_TEXT} {$INTERPOLATION}')])
|
||||
.toEqual(ɵmakeParsedTranslation(
|
||||
['', ' tnemele elbatalsnart ', 'sredlohecalp htiw', ''],
|
||||
['INTERPOLATION', 'START_BOLD_TEXT', 'CLOSE_BOLD_TEXT']));
|
||||
});
|
||||
|
||||
it('should extract nested placeholder containers (i.e. nested HTML elements)', () => {
|
||||
/**
|
||||
* Source HTML:
|
||||
*
|
||||
* ```
|
||||
* <div i18n>
|
||||
* translatable <span>element <b>with placeholders</b></span> {{ interpolation}}
|
||||
* </div>
|
||||
* ```
|
||||
*/
|
||||
const XLIFF = [
|
||||
`<xliff version="2.0" xmlns="urn:oasis:names:tc:xliff:document:2.0" srcLang="en" trgLang="fr">`,
|
||||
` <file original="ng.template" id="ngi18n">`,
|
||||
` <unit id="9051630253697141670">`,
|
||||
` <notes>`,
|
||||
` <note category="location">file.ts:3</note>`,
|
||||
` </notes>`,
|
||||
` <segment>`,
|
||||
` <source>translatable <pc id="0" equivStart="START_TAG_SPAN" equivEnd="CLOSE_TAG_SPAN" type="other"` +
|
||||
` dispStart="<span>" dispEnd="</span>">element <pc id="1" equivStart="START_BOLD_TEXT" equivEnd=` +
|
||||
`"CLOSE_BOLD_TEXT" type="fmt" dispStart="<b>" dispEnd="</b>">with placeholders</pc></pc>` +
|
||||
` <ph id="2" equiv="INTERPOLATION" disp="{{ interpolation}}"/></source>`,
|
||||
` <target><pc id="0" equivStart="START_TAG_SPAN" equivEnd="CLOSE_TAG_SPAN" type="fmt" dispStart="<` +
|
||||
`span>" dispEnd="</span>"><ph id="2" equiv="INTERPOLATION" disp="{{ interpolation}}"/> tnemele</pc>` +
|
||||
` elbatalsnart <pc id="1" equivStart="START_BOLD_TEXT" equivEnd="CLOSE_BOLD_TEXT" type="fmt" dispStart=` +
|
||||
`"<b>" dispEnd="</b>">sredlohecalp htiw</pc></target>`,
|
||||
` </segment>`,
|
||||
` </unit>`,
|
||||
` </file>`,
|
||||
`</xliff>`,
|
||||
].join('\n');
|
||||
const result = doParse('/some/file.xlf', XLIFF);
|
||||
expect(
|
||||
result.translations[ɵcomputeMsgId(
|
||||
'translatable {$START_TAG_SPAN}element {$START_BOLD_TEXT}with placeholders' +
|
||||
'{$CLOSE_BOLD_TEXT}{$CLOSE_TAG_SPAN} {$INTERPOLATION}')])
|
||||
.toEqual(ɵmakeParsedTranslation(
|
||||
['', '', ' tnemele', ' elbatalsnart ', 'sredlohecalp htiw', ''], [
|
||||
'START_TAG_SPAN',
|
||||
'INTERPOLATION',
|
||||
'CLOSE_TAG_SPAN',
|
||||
'START_BOLD_TEXT',
|
||||
'CLOSE_BOLD_TEXT',
|
||||
]));
|
||||
});
|
||||
|
||||
it('should extract translations with simple ICU expressions', () => {
|
||||
/**
|
||||
* Source HTML:
|
||||
|
@ -96,6 +96,38 @@ describe('XtbTranslationParser', () => {
|
||||
ɵmakeParsedTranslation(['', 'rab', ''], ['START_PARAGRAPH', 'CLOSE_PARAGRAPH']));
|
||||
});
|
||||
|
||||
it('should extract nested placeholder containers (i.e. nested HTML elements)', () => {
|
||||
/**
|
||||
* Source HTML:
|
||||
*
|
||||
* ```
|
||||
* <div i18n>
|
||||
* translatable <span>element <b>with placeholders</b></span> {{ interpolation}}
|
||||
* </div>
|
||||
* ```
|
||||
*/
|
||||
const XLIFF = [
|
||||
`<?xml version="1.0" encoding="UTF-8"?>`,
|
||||
`<translationbundle>`,
|
||||
` <translation id="9051630253697141670">` +
|
||||
`<ph name="START_TAG_SPAN"/><ph name="INTERPOLATION"/> tnemele<ph name="CLOSE_TAG_SPAN"/> elbatalsnart <ph name="START_BOLD_TEXT"/>sredlohecalp htiw<ph name="CLOSE_BOLD_TEXT"/>` +
|
||||
`</translation>`,
|
||||
`</translationbundle>`,
|
||||
].join('\n');
|
||||
const result = doParse('/some/file.xtb', XLIFF);
|
||||
expect(result.translations[ɵcomputeMsgId(
|
||||
'translatable {$START_TAG_SPAN}element {$START_BOLD_TEXT}with placeholders' +
|
||||
'{$CLOSE_BOLD_TEXT}{$CLOSE_TAG_SPAN} {$INTERPOLATION}')])
|
||||
.toEqual(ɵmakeParsedTranslation(
|
||||
['', '', ' tnemele', ' elbatalsnart ', 'sredlohecalp htiw', ''], [
|
||||
'START_TAG_SPAN',
|
||||
'INTERPOLATION',
|
||||
'CLOSE_TAG_SPAN',
|
||||
'START_BOLD_TEXT',
|
||||
'CLOSE_BOLD_TEXT',
|
||||
]));
|
||||
});
|
||||
|
||||
it('should extract translations with simple ICU expressions', () => {
|
||||
const XTB = [
|
||||
`<?xml version="1.0" encoding="UTF-8" ?>`,
|
||||
|
@ -113,7 +113,7 @@ export function parseTranslation(messageString: TargetMessage): ParsedTranslatio
|
||||
export function makeParsedTranslation(
|
||||
messageParts: string[], placeholderNames: string[] = []): ParsedTranslation {
|
||||
let messageString = messageParts[0];
|
||||
for (let i = 0; i < placeholderNames.length - 1; i++) {
|
||||
for (let i = 0; i < placeholderNames.length; i++) {
|
||||
messageString += `{$${placeholderNames[i]}}${messageParts[i + 1]}`;
|
||||
}
|
||||
return {
|
||||
|
@ -5,7 +5,7 @@
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
import {computeMsgId, makeTemplateObject, ParsedTranslation, parseTranslation, TargetMessage, translate} from '..';
|
||||
import {computeMsgId, makeParsedTranslation, makeTemplateObject, ParsedTranslation, parseTranslation, TargetMessage, translate} from '..';
|
||||
|
||||
describe('utils', () => {
|
||||
describe('makeTemplateObject', () => {
|
||||
@ -22,6 +22,24 @@ describe('utils', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('makeParsedTranslation()', () => {
|
||||
it('should compute a template object from the parts', () => {
|
||||
expect(makeParsedTranslation(['a', 'b', 'c'], ['ph1', 'ph2']).messageParts)
|
||||
.toEqual(makeTemplateObject(['a', 'b', 'c'], ['a', 'b', 'c']));
|
||||
});
|
||||
|
||||
it('should include the placeholder names', () => {
|
||||
expect(makeParsedTranslation(['a', 'b', 'c'], ['ph1', 'ph2']).placeholderNames).toEqual([
|
||||
'ph1', 'ph2'
|
||||
]);
|
||||
});
|
||||
|
||||
it('should compute the message string from the parts and placeholder names', () => {
|
||||
expect(makeParsedTranslation(['a', 'b', 'c'], ['ph1', 'ph2']).text)
|
||||
.toEqual('a{$ph1}b{$ph2}c');
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseTranslation', () => {
|
||||
it('should extract the messageParts as a TemplateStringsArray', () => {
|
||||
const translation = parseTranslation('a{$one}b{$two}c');
|
||||
|
Reference in New Issue
Block a user