Skip to content

Commit b7bade4

Browse files
authored
Merge pull request #63 from devongovett/source_locations
Add optional support for source locations
2 parents f0b209b + e18551c commit b7bade4

File tree

5 files changed

+124
-2
lines changed

5 files changed

+124
-2
lines changed

readme.md

+5
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,11 @@ Type: `Boolean`
115115
Default: `false`
116116
Description: *If set to true, self-closing tags will trigger the `onclosetag` event even if `xmlMode` is not set to `true`. NOTE: If `xmlMode` is set to `true` then self-closing tags will always be recognized.*
117117

118+
### `sourceLocations`
119+
Type: `Boolean`
120+
Default: `false`
121+
Description: *If set to true, AST nodes will have a `location` property containing the `start` and `end` line and column position of the node.*
122+
118123
## License
119124

120125
[MIT](LICENSE)

src/index.ts

+16-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {Parser, ParserOptions} from 'htmlparser2';
2-
import {Directive, Node, Options, Attributes} from '../types/index.d';
2+
import {Directive, Node, NodeTag, Options, Attributes} from '../types/index.d';
3+
import {LocationTracker} from './location-tracker';
34

45
const defaultOptions: ParserOptions = {
56
lowerCaseTags: false,
@@ -16,6 +17,7 @@ const defaultDirectives: Directive[] = [
1617
];
1718

1819
const parser = (html: string, options: Options = {}): Node[] => {
20+
const locationTracker = new LocationTracker(html);
1921
const bufArray: Node[] = [];
2022
const results: Node[] = [];
2123

@@ -92,7 +94,15 @@ const parser = (html: string, options: Options = {}): Node[] => {
9294
}
9395

9496
function onopentag(tag: string, attrs: Attributes) {
95-
const buf: Node = {tag};
97+
const start = locationTracker.getPosition(parser.startIndex);
98+
const buf: NodeTag = {tag};
99+
100+
if (options.sourceLocations) {
101+
buf.location = {
102+
start,
103+
end: start
104+
};
105+
}
96106

97107
if (Object.keys(attrs).length > 0) {
98108
buf.attrs = normalizeArributes(attrs);
@@ -104,6 +114,10 @@ const parser = (html: string, options: Options = {}): Node[] => {
104114
function onclosetag() {
105115
const buf: Node | undefined = bufArray.pop();
106116

117+
if (buf && typeof buf === 'object' && buf.location && parser.endIndex !== null) {
118+
buf.location.end = locationTracker.getPosition(parser.endIndex);
119+
}
120+
107121
if (buf) {
108122
const last = bufferArrayLast();
109123

src/location-tracker.ts

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import {Position} from '../types/index.d';
2+
3+
export class LocationTracker {
4+
private readonly source: string;
5+
private lastPosition: Position;
6+
private lastIndex: number;
7+
8+
constructor(source: string) {
9+
this.source = source;
10+
this.lastPosition = {
11+
line: 1,
12+
column: 1
13+
};
14+
15+
this.lastIndex = 0;
16+
}
17+
18+
getPosition(index: number): Position {
19+
if (index < this.lastIndex) {
20+
throw new Error('Source indices must be monotonic');
21+
}
22+
23+
while (this.lastIndex < index) {
24+
if (this.source.charCodeAt(this.lastIndex) === /* \n */ 10) {
25+
this.lastPosition.line++;
26+
this.lastPosition.column = 1;
27+
} else {
28+
this.lastPosition.column++;
29+
}
30+
31+
this.lastIndex++;
32+
}
33+
34+
return {
35+
line: this.lastPosition.line,
36+
column: this.lastPosition.column
37+
};
38+
}
39+
}

test/test-core.spec.ts

+52
Original file line numberDiff line numberDiff line change
@@ -240,3 +240,55 @@ test('should be not converting html entity name', t => {
240240
const expected = ['&zwnj;&nbsp;&copy;'];
241241
t.deepEqual(tree, expected);
242242
});
243+
244+
test('should parse with source locations', t => {
245+
const html = '<h1>Test</h1>\n<p><b>Foo</b></p>';
246+
const tree = parser(html, {sourceLocations: true});
247+
const expected = [
248+
{
249+
tag: 'h1',
250+
content: ['Test'],
251+
location: {
252+
start: {
253+
line: 1,
254+
column: 1
255+
},
256+
end: {
257+
line: 1,
258+
column: 13
259+
}
260+
}
261+
},
262+
'\n',
263+
{
264+
tag: 'p',
265+
content: [
266+
{
267+
tag: 'b',
268+
content: ['Foo'],
269+
location: {
270+
start: {
271+
line: 2,
272+
column: 4
273+
},
274+
end: {
275+
line: 2,
276+
column: 13
277+
}
278+
}
279+
}
280+
],
281+
location: {
282+
start: {
283+
line: 2,
284+
column: 1
285+
},
286+
end: {
287+
line: 2,
288+
column: 17
289+
}
290+
}
291+
}
292+
];
293+
t.deepEqual(tree, expected);
294+
});

types/index.d.ts

+12
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export type Directive = {
1212

1313
export type Options = {
1414
directives?: Directive[];
15+
sourceLocations?: boolean;
1516
} & ParserOptions;
1617

1718
export type Tag = string | boolean;
@@ -23,6 +24,17 @@ export type NodeTag = {
2324
tag?: Tag;
2425
attrs?: Attributes;
2526
content?: Content;
27+
location?: SourceLocation;
2628
};
2729

2830
export type Node = NodeText | NodeTag;
31+
32+
export type SourceLocation = {
33+
start: Position;
34+
end: Position;
35+
};
36+
37+
export type Position = {
38+
line: number;
39+
column: number;
40+
};

0 commit comments

Comments
 (0)