Skip to content

Commit 68509ac

Browse files
committed
Second attempt at landing #1553
Provide source spans for the open and close tags without adding new AST nodes, also parse the element's tag name into a path which has the required source span.
1 parent 13aeaa0 commit 68509ac

8 files changed

+265
-38
lines changed

packages/@glimmer/syntax/lib/parser.ts

+2
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ export type ParserNodeBuilder<N extends { loc: src.SourceSpan }> = Omit<N, 'loc'
1717
export interface StartTag {
1818
readonly type: 'StartTag';
1919
name: string;
20+
nameStart: Nullable<src.SourceOffset>;
21+
nameEnd: Nullable<src.SourceOffset>;
2022
readonly attributes: ASTv1.AttrNode[];
2123
readonly modifiers: ASTv1.ElementModifierStatement[];
2224
readonly comments: ASTv1.MustacheCommentStatement[];

packages/@glimmer/syntax/lib/parser/tokenizer-event-handlers.ts

+47-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { Nullable } from '@glimmer/interfaces';
22
import type { TokenizerState } from 'simple-html-tokenizer';
33
import {
4+
asPresentArray,
45
assert,
56
assertPresentArray,
67
assign,
@@ -87,6 +88,8 @@ export class TokenizerEventHandlers extends HandlebarsNodeVisitors {
8788
this.currentNode = {
8889
type: 'StartTag',
8990
name: '',
91+
nameStart: null,
92+
nameEnd: null,
9093
attributes: [],
9194
modifiers: [],
9295
comments: [],
@@ -129,32 +132,59 @@ export class TokenizerEventHandlers extends HandlebarsNodeVisitors {
129132
}
130133

131134
finishStartTag(): void {
132-
let { name, attributes, modifiers, comments, params, selfClosing, loc } = this.finish(
135+
let { name, nameStart, nameEnd } = this.currentStartTag;
136+
137+
// <> should probably be a syntax error, but s-h-t is currently broken for that case
138+
assert(name !== '', 'tag name cannot be empty');
139+
assert(nameStart !== null, 'nameStart unexpectedly null');
140+
assert(nameEnd !== null, 'nameEnd unexpectedly null');
141+
142+
let nameLoc = nameStart.until(nameEnd);
143+
let [head, ...tail] = asPresentArray(name.split('.'));
144+
let path = b.path({
145+
head: b.head({ original: head, loc: nameLoc.sliceStartChars({ chars: head.length }) }),
146+
tail,
147+
loc: nameLoc,
148+
});
149+
150+
let { attributes, modifiers, comments, params, selfClosing, loc } = this.finish(
133151
this.currentStartTag
134152
);
135153

136154
let element = b.element({
137-
tag: name,
155+
path,
138156
selfClosing,
139157
attributes,
140158
modifiers,
141159
comments,
142160
params,
143161
children: [],
162+
openTag: loc,
163+
closeTag: selfClosing ? null : src.SourceSpan.broken(),
144164
loc,
145165
});
146166
this.elementStack.push(element);
147167
}
148168

149169
finishEndTag(isVoid: boolean): void {
170+
let { start: closeTagStart } = this.currentTag;
150171
let tag = this.finish<StartTag | EndTag>(this.currentTag);
151172

152173
let element = this.elementStack.pop() as ASTv1.ElementNode;
153174

154175
this.validateEndTag(tag, element, isVoid);
155176
let parent = this.currentElement();
156177

178+
if (isVoid) {
179+
element.closeTag = null;
180+
} else if (element.selfClosing) {
181+
assert(element.closeTag === null, 'element.closeTag unexpectedly present');
182+
} else {
183+
element.closeTag = closeTagStart.until(this.offset());
184+
}
185+
157186
element.loc = element.loc.withEnd(this.offset());
187+
158188
appendChild(parent, b.element(element));
159189
}
160190

@@ -174,7 +204,21 @@ export class TokenizerEventHandlers extends HandlebarsNodeVisitors {
174204
// Tags - name
175205

176206
appendToTagName(char: string): void {
177-
this.currentTag.name += char;
207+
let tag = this.currentTag;
208+
tag.name += char;
209+
210+
if (tag.type === 'StartTag') {
211+
let offset = this.offset();
212+
213+
if (tag.nameStart === null) {
214+
assert(tag.nameEnd === null, 'nameStart and nameEnd must both be null');
215+
216+
// Note that the tokenizer already consumed the token here
217+
tag.nameStart = offset.move(-1);
218+
}
219+
220+
tag.nameEnd = offset;
221+
}
178222
}
179223

180224
// Tags - attributes

packages/@glimmer/syntax/lib/v1/nodes-v1.ts

+16-1
Original file line numberDiff line numberDiff line change
@@ -104,14 +104,29 @@ export interface MustacheCommentStatement extends BaseNode {
104104

105105
export interface ElementNode extends BaseNode {
106106
type: 'ElementNode';
107-
tag: string;
107+
path: PathExpression;
108108
selfClosing: boolean;
109109
attributes: AttrNode[];
110110
params: VarHead[];
111111
modifiers: ElementModifierStatement[];
112112
comments: MustacheCommentStatement[];
113113
children: Statement[];
114114

115+
/**
116+
* span for the open tag
117+
*/
118+
openTag: src.SourceSpan;
119+
120+
/**
121+
* span for the close tag, null for void or self-closing tags
122+
*/
123+
closeTag: Nullable<src.SourceSpan>;
124+
125+
/**
126+
* string accessor for path.original
127+
*/
128+
tag: string;
129+
115130
/**
116131
* string accessor for params.name
117132
*/

packages/@glimmer/syntax/lib/v1/parser-builders.ts

+29-4
Original file line numberDiff line numberDiff line change
@@ -171,34 +171,47 @@ class Builders {
171171
}
172172

173173
element({
174-
tag,
174+
path,
175175
selfClosing,
176176
attributes,
177177
modifiers,
178178
params,
179179
comments,
180180
children,
181+
openTag,
182+
closeTag,
181183
loc,
182184
}: {
183-
tag: string;
185+
path: ASTv1.PathExpression;
184186
selfClosing: boolean;
185187
attributes: ASTv1.AttrNode[];
186188
modifiers: ASTv1.ElementModifierStatement[];
187189
params: ASTv1.VarHead[];
188190
children: ASTv1.Statement[];
189191
comments: ASTv1.MustacheCommentStatement[];
192+
openTag: SourceSpan;
193+
closeTag: Nullable<SourceSpan>;
190194
loc: SourceSpan;
191195
}): ASTv1.ElementNode {
196+
let _selfClosing = selfClosing;
197+
192198
return {
193199
type: 'ElementNode',
194-
tag,
195-
selfClosing: selfClosing,
200+
path,
196201
attributes,
197202
modifiers,
198203
params,
199204
comments,
200205
children,
206+
openTag,
207+
closeTag,
201208
loc,
209+
get tag() {
210+
return this.path.original;
211+
},
212+
set tag(name: string) {
213+
this.path.original = name;
214+
},
202215
get blockParams() {
203216
return this.params.map((p) => p.name);
204217
},
@@ -207,6 +220,18 @@ class Builders {
207220
return b.var({ name, loc: SourceSpan.synthetic(name) });
208221
});
209222
},
223+
get selfClosing() {
224+
return _selfClosing;
225+
},
226+
set selfClosing(selfClosing: boolean) {
227+
_selfClosing = selfClosing;
228+
229+
if (selfClosing) {
230+
this.closeTag = null;
231+
} else {
232+
this.closeTag = SourceSpan.synthetic(`</${this.tag}>`);
233+
}
234+
},
210235
};
211236
}
212237

packages/@glimmer/syntax/lib/v1/public-builders.ts

+59-16
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { asPresentArray, assert, deprecate, isPresentArray } from '@glimmer/util
44
import type { SourceLocation, SourcePosition } from '../source/location';
55
import type * as ASTv1 from './api';
66

7+
import { isVoidTag } from '../generation/printer';
78
import { SYNTHETIC_LOCATION } from '../source/location';
89
import { Source } from '../source/source';
910
import { SourceSpan } from '../source/span';
@@ -24,7 +25,11 @@ function SOURCE(): Source {
2425
// Statements
2526

2627
export type BuilderHead = string | ASTv1.CallableExpression;
27-
export type TagDescriptor = string | { name: string; selfClosing: boolean };
28+
export type TagDescriptor =
29+
| string
30+
| ASTv1.PathExpression
31+
| { path: ASTv1.PathExpression; selfClosing?: boolean }
32+
| { name: string; selfClosing?: boolean };
2833

2934
function buildMustache(
3035
path: BuilderHead | ASTv1.Literal,
@@ -71,7 +76,7 @@ function buildBlock(
7176
defaultBlock = _defaultBlock;
7277
}
7378

74-
if (_elseBlock !== undefined && _elseBlock !== null && _elseBlock.type === 'Template') {
79+
if (_elseBlock?.type === 'Template') {
7580
deprecate(`b.program is deprecated. Use b.blockItself instead.`);
7681
assert(_elseBlock.locals.length === 0, '{{else}} block cannot have block params');
7782

@@ -177,23 +182,51 @@ export interface BuildElementOptions {
177182
children?: ASTv1.Statement[];
178183
comments?: ASTv1.MustacheCommentStatement[];
179184
blockParams?: ASTv1.VarHead[] | string[];
185+
openTag?: SourceLocation;
186+
closeTag?: Nullable<SourceLocation>;
180187
loc?: SourceLocation;
181188
}
182189

183190
function buildElement(tag: TagDescriptor, options: BuildElementOptions = {}): ASTv1.ElementNode {
184-
let { attrs, blockParams, modifiers, comments, children, loc } = options;
191+
let {
192+
attrs,
193+
blockParams,
194+
modifiers,
195+
comments,
196+
children,
197+
openTag,
198+
closeTag: _closeTag,
199+
loc,
200+
} = options;
185201

186202
// this is used for backwards compat, prior to `selfClosing` being part of the ElementNode AST
187-
let tagName: string;
188-
let selfClosing = false;
189-
if (typeof tag === 'object') {
203+
let path: ASTv1.PathExpression;
204+
let selfClosing: boolean | undefined;
205+
206+
if (typeof tag === 'string') {
207+
if (tag.endsWith('/')) {
208+
path = buildPath(tag.slice(0, -1));
209+
selfClosing = true;
210+
} else {
211+
path = buildPath(tag);
212+
}
213+
} else if ('type' in tag) {
214+
assert(tag.type === 'PathExpression', `Invalid tag type ${tag.type}`);
215+
path = tag;
216+
} else if ('path' in tag) {
217+
assert(tag.path.type === 'PathExpression', `Invalid tag type ${tag.path.type}`);
218+
path = tag.path;
190219
selfClosing = tag.selfClosing;
191-
tagName = tag.name;
192-
} else if (tag.slice(-1) === '/') {
193-
tagName = tag.slice(0, -1);
194-
selfClosing = true;
195220
} else {
196-
tagName = tag;
221+
path = buildPath(tag.name);
222+
selfClosing = tag.selfClosing;
223+
}
224+
225+
if (selfClosing) {
226+
assert(
227+
_closeTag === null || _closeTag === undefined,
228+
'Cannot build a self-closing tag with a closeTag source location'
229+
);
197230
}
198231

199232
let params = blockParams?.map((param) => {
@@ -204,14 +237,24 @@ function buildElement(tag: TagDescriptor, options: BuildElementOptions = {}): AS
204237
}
205238
});
206239

240+
let closeTag: Nullable<SourceSpan> = null;
241+
242+
if (_closeTag) {
243+
closeTag = buildLoc(_closeTag || null);
244+
} else if (_closeTag === undefined) {
245+
closeTag = selfClosing || isVoidTag(path.original) ? null : buildLoc(null);
246+
}
247+
207248
return b.element({
208-
tag: tagName,
209-
selfClosing,
249+
path,
250+
selfClosing: selfClosing || false,
210251
attributes: attrs || [],
211252
params: params || [],
212253
modifiers: modifiers || [],
213254
comments: comments || [],
214255
children: children || [],
256+
openTag: buildLoc(openTag || null),
257+
closeTag,
215258
loc: buildLoc(loc || null),
216259
});
217260
}
@@ -253,7 +296,7 @@ function buildHead(original: string, loc?: SourceLocation): ASTv1.PathExpression
253296
return b.path({ head: headNode, tail, loc: buildLoc(loc || null) });
254297
}
255298

256-
function buildThis(loc: SourceLocation): ASTv1.ThisHead {
299+
function buildThis(loc?: SourceLocation): ASTv1.ThisHead {
257300
return b.this({ loc: buildLoc(loc || null) });
258301
}
259302

@@ -271,8 +314,8 @@ function buildHeadFromString(original: string, loc?: SourceLocation): ASTv1.Path
271314

272315
function buildCleanPath(
273316
head: ASTv1.PathHead,
274-
tail: string[],
275-
loc: SourceLocation
317+
tail: string[] = [],
318+
loc?: SourceLocation
276319
): ASTv1.PathExpression {
277320
return b.path({ head, tail, loc: buildLoc(loc || null) });
278321
}

0 commit comments

Comments
 (0)