muggle-pug 0.0.0-stage → 0.1.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +20 -2
- package/out/index.d.ts +1 -0
- package/out/index.js +18 -0
- package/out/lines.d.ts +15 -0
- package/out/lines.js +74 -0
- package/out/parse.d.ts +89 -0
- package/out/parse.js +347 -0
- package/package.json +29 -5
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Johnson Chu
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,21 @@
|
|
|
1
|
-
|
|
1
|
+
Pug compiled to HTML as pieces that map to the pug code.
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
muggle-pug depends only on `pug-lexer` and `pug-parser`, and builds no mappings: every consumer turns the pieces into the mappings of its own tooling.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import { parse } from 'muggle-pug';
|
|
9
|
+
|
|
10
|
+
const { htmlCode, pieces, ast, error, emptyLineEnds } = parse('div.foo hello');
|
|
11
|
+
// htmlCode: "<div class='foo'>hello</div>"
|
|
12
|
+
// pieces: [..., { start: 8, end: 13 }, ...]
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
- `pieces`: the HTML, as new text and `{ start, end, text? }` ranges of the pug code. A range without `text` copies the pug code there; `text` is HTML that maps to the range as a whole, such as `class` for the `.` of `.foo`, or the closing quote that pug adds to its value (an empty range).
|
|
16
|
+
- `htmlCode`: the text of the pieces.
|
|
17
|
+
- `ast`: the pug AST, with 1-based lines and columns.
|
|
18
|
+
- `error`: the lexer or parser error, with 0-based `line` and `column`.
|
|
19
|
+
- `emptyLineEnds`: the offsets of the line ends of the empty lines before a line.
|
|
20
|
+
|
|
21
|
+
Vue dedents a pug template, but pug rejects an indented first line, so an indented template is parsed as the children of a `template` tag that is blanked out of the HTML and left out of the mapped pieces, the AST and the offsets.
|
package/out/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './parse';
|
package/out/index.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./parse"), exports);
|
|
18
|
+
//# sourceMappingURL=index.js.map
|
package/out/lines.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** Line and column offsets of a text, as `TextDocument` of `vscode-languageserver-textdocument` reads them. */
|
|
2
|
+
export declare class Lines {
|
|
3
|
+
readonly text: string;
|
|
4
|
+
private readonly offsets;
|
|
5
|
+
constructor(text: string);
|
|
6
|
+
/** The offset of the 0-based `line` and `character`, clamped to the text and before the end of the line. */
|
|
7
|
+
offsetAt(line: number, character: number): number;
|
|
8
|
+
/** The 0-based line of `offset`. */
|
|
9
|
+
lineAt(offset: number): number;
|
|
10
|
+
/** The text of the 0-based `line`, without its line break, except a `\r` of a `\r\n`. */
|
|
11
|
+
lineText(line: number): string;
|
|
12
|
+
/** `offset` clamped to the text and moved before the end of its line. */
|
|
13
|
+
private normalize;
|
|
14
|
+
private beforeEOL;
|
|
15
|
+
}
|
package/out/lines.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Lines = void 0;
|
|
4
|
+
/** Line and column offsets of a text, as `TextDocument` of `vscode-languageserver-textdocument` reads them. */
|
|
5
|
+
class Lines {
|
|
6
|
+
constructor(text) {
|
|
7
|
+
this.text = text;
|
|
8
|
+
this.offsets = [0];
|
|
9
|
+
for (let i = 0; i < text.length; i++) {
|
|
10
|
+
const ch = text.charCodeAt(i);
|
|
11
|
+
if (isEOL(ch)) {
|
|
12
|
+
if (ch === 13 && text.charCodeAt(i + 1) === 10) {
|
|
13
|
+
i++;
|
|
14
|
+
}
|
|
15
|
+
this.offsets.push(i + 1);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/** The offset of the 0-based `line` and `character`, clamped to the text and before the end of the line. */
|
|
20
|
+
offsetAt(line, character) {
|
|
21
|
+
const { offsets } = this;
|
|
22
|
+
if (line >= offsets.length) {
|
|
23
|
+
return this.text.length;
|
|
24
|
+
}
|
|
25
|
+
if (line < 0) {
|
|
26
|
+
return 0;
|
|
27
|
+
}
|
|
28
|
+
const lineOffset = offsets[line];
|
|
29
|
+
if (character <= 0) {
|
|
30
|
+
return lineOffset;
|
|
31
|
+
}
|
|
32
|
+
const nextLineOffset = line + 1 < offsets.length ? offsets[line + 1] : this.text.length;
|
|
33
|
+
return this.beforeEOL(Math.min(lineOffset + character, nextLineOffset), lineOffset);
|
|
34
|
+
}
|
|
35
|
+
/** The 0-based line of `offset`. */
|
|
36
|
+
lineAt(offset) {
|
|
37
|
+
const { offsets } = this;
|
|
38
|
+
offset = Math.max(Math.min(offset, this.text.length), 0);
|
|
39
|
+
let low = 0;
|
|
40
|
+
let high = offsets.length;
|
|
41
|
+
while (low < high) {
|
|
42
|
+
const mid = (low + high) >>> 1;
|
|
43
|
+
if (offsets[mid] > offset) {
|
|
44
|
+
high = mid;
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
low = mid + 1;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return low - 1;
|
|
51
|
+
}
|
|
52
|
+
/** The text of the 0-based `line`, without its line break, except a `\r` of a `\r\n`. */
|
|
53
|
+
lineText(line) {
|
|
54
|
+
const endOffset = this.offsetAt(line + 1, 0);
|
|
55
|
+
const end = this.lineAt(endOffset) === line ? endOffset : endOffset - 1;
|
|
56
|
+
return this.text.substring(this.offsetAt(line, 0), this.normalize(end));
|
|
57
|
+
}
|
|
58
|
+
/** `offset` clamped to the text and moved before the end of its line. */
|
|
59
|
+
normalize(offset) {
|
|
60
|
+
offset = Math.max(Math.min(offset, this.text.length), 0);
|
|
61
|
+
return this.beforeEOL(offset, this.offsets[this.lineAt(offset)]);
|
|
62
|
+
}
|
|
63
|
+
beforeEOL(offset, lineOffset) {
|
|
64
|
+
while (offset > lineOffset && isEOL(this.text.charCodeAt(offset - 1))) {
|
|
65
|
+
offset--;
|
|
66
|
+
}
|
|
67
|
+
return offset;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
exports.Lines = Lines;
|
|
71
|
+
function isEOL(ch) {
|
|
72
|
+
return ch === 13 || ch === 10;
|
|
73
|
+
}
|
|
74
|
+
//# sourceMappingURL=lines.js.map
|
package/out/parse.d.ts
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generated HTML: new text, which maps nowhere, or the range `start`..`end` of the pug code. A range without `text` is
|
|
3
|
+
* a copy of the pug code there; `text` is HTML that maps to the range as a whole, such as `class` for the `.` of
|
|
4
|
+
* `.foo`, or the closing quote that pug adds to its value (an empty range). An empty range is an anchor.
|
|
5
|
+
*/
|
|
6
|
+
export type PugPiece = string | {
|
|
7
|
+
start: number;
|
|
8
|
+
end: number;
|
|
9
|
+
text?: string;
|
|
10
|
+
};
|
|
11
|
+
export interface PugError {
|
|
12
|
+
code: string;
|
|
13
|
+
msg: string;
|
|
14
|
+
/** 0-based. */
|
|
15
|
+
line: number;
|
|
16
|
+
/** 0-based. */
|
|
17
|
+
column: number;
|
|
18
|
+
filename: string;
|
|
19
|
+
}
|
|
20
|
+
export interface PugParseResult {
|
|
21
|
+
/** The HTML of the pieces. */
|
|
22
|
+
htmlCode: string;
|
|
23
|
+
pieces: PugPiece[];
|
|
24
|
+
/** The pug AST, with 1-based lines and columns of the pug code. */
|
|
25
|
+
ast: Node | undefined;
|
|
26
|
+
error: PugError | undefined;
|
|
27
|
+
/** The offsets of the line ends of the empty lines that the pug code has before a line. */
|
|
28
|
+
emptyLineEnds: number[];
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Compiles pug to HTML pieces, which map to the pug code.
|
|
32
|
+
*
|
|
33
|
+
* Vue dedents a pug template (`dedent` of `@vue/compiler-sfc`), but pug reads an indented first line as an
|
|
34
|
+
* error, so an indented template is parsed as the children of a `template` tag. The tag is blanked out of
|
|
35
|
+
* the HTML and left out of the mapped pieces, the AST and the offsets, which are all of `pugCode`.
|
|
36
|
+
*/
|
|
37
|
+
export declare function parse(pugCode: string): PugParseResult;
|
|
38
|
+
export type Node = BlockNode | MixinNode | TagNode | TextNode | CommentNode | BlockCommentNode;
|
|
39
|
+
export interface BlockNode {
|
|
40
|
+
type: 'Block';
|
|
41
|
+
nodes: Node[];
|
|
42
|
+
line: number;
|
|
43
|
+
}
|
|
44
|
+
export interface TagNode {
|
|
45
|
+
type: 'Tag';
|
|
46
|
+
name: string;
|
|
47
|
+
selfClosing: boolean;
|
|
48
|
+
block: BlockNode;
|
|
49
|
+
attrs: {
|
|
50
|
+
name: string;
|
|
51
|
+
val: string | true;
|
|
52
|
+
line: number;
|
|
53
|
+
column: number;
|
|
54
|
+
mustEscape: boolean;
|
|
55
|
+
}[];
|
|
56
|
+
attributeBlocks: {}[];
|
|
57
|
+
isInline: boolean;
|
|
58
|
+
line: number;
|
|
59
|
+
column: number;
|
|
60
|
+
}
|
|
61
|
+
export interface TextNode {
|
|
62
|
+
type: 'Text';
|
|
63
|
+
val: string;
|
|
64
|
+
line: number;
|
|
65
|
+
column: number;
|
|
66
|
+
}
|
|
67
|
+
export interface CommentNode {
|
|
68
|
+
type: 'Comment';
|
|
69
|
+
val: string;
|
|
70
|
+
buffer: boolean;
|
|
71
|
+
line: number;
|
|
72
|
+
column: number;
|
|
73
|
+
}
|
|
74
|
+
export interface BlockCommentNode {
|
|
75
|
+
type: 'BlockComment';
|
|
76
|
+
block: BlockNode;
|
|
77
|
+
val: string;
|
|
78
|
+
buffer: boolean;
|
|
79
|
+
line: number;
|
|
80
|
+
column: number;
|
|
81
|
+
}
|
|
82
|
+
export interface MixinNode {
|
|
83
|
+
type: 'Mixin';
|
|
84
|
+
name: string;
|
|
85
|
+
args: null;
|
|
86
|
+
block: BlockNode | null;
|
|
87
|
+
line: number;
|
|
88
|
+
column: number;
|
|
89
|
+
}
|
package/out/parse.js
ADDED
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.parse = parse;
|
|
7
|
+
const pug_lexer_1 = __importDefault(require("pug-lexer"));
|
|
8
|
+
const lines_1 = require("./lines");
|
|
9
|
+
const pugParser = require('pug-parser');
|
|
10
|
+
const indentedPrefix = 'template\n';
|
|
11
|
+
const startTagLength = '<template>'.length;
|
|
12
|
+
const endTagLength = '</template>'.length;
|
|
13
|
+
/**
|
|
14
|
+
* Compiles pug to HTML pieces, which map to the pug code.
|
|
15
|
+
*
|
|
16
|
+
* Vue dedents a pug template (`dedent` of `@vue/compiler-sfc`), but pug reads an indented first line as an
|
|
17
|
+
* error, so an indented template is parsed as the children of a `template` tag. The tag is blanked out of
|
|
18
|
+
* the HTML and left out of the mapped pieces, the AST and the offsets, which are all of `pugCode`.
|
|
19
|
+
*/
|
|
20
|
+
function parse(pugCode) {
|
|
21
|
+
if (calculateMinIndent(pugCode) === 0) {
|
|
22
|
+
return parseCode(pugCode);
|
|
23
|
+
}
|
|
24
|
+
const prefixedCode = indentedPrefix + pugCode;
|
|
25
|
+
const parsed = parseCode(prefixedCode);
|
|
26
|
+
return {
|
|
27
|
+
...blankTemplateTag(parsed.pieces, prefixedCode, parsed.htmlCode.length),
|
|
28
|
+
error: parsed.error && { ...parsed.error, line: parsed.error.line - 1 },
|
|
29
|
+
ast: parsed.ast && toPrevLine(parsed.ast.nodes[0].block),
|
|
30
|
+
emptyLineEnds: parsed.emptyLineEnds.map(offset => offset - indentedPrefix.length),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
/** Blanks the tags of the `template` around the HTML, and shifts the pieces that map past the prefix. */
|
|
34
|
+
function blankTemplateTag(pieces, prefixedCode, htmlLength) {
|
|
35
|
+
if (htmlLength < startTagLength + endTagLength) {
|
|
36
|
+
const htmlCode = ' '.repeat(startTagLength + endTagLength);
|
|
37
|
+
return { htmlCode, pieces: [htmlCode] };
|
|
38
|
+
}
|
|
39
|
+
const prefixLength = indentedPrefix.length;
|
|
40
|
+
const endTagStart = htmlLength - endTagLength;
|
|
41
|
+
const result = [];
|
|
42
|
+
let offset = 0;
|
|
43
|
+
for (const piece of pieces) {
|
|
44
|
+
const text = pieceText(piece, prefixedCode);
|
|
45
|
+
const start = offset;
|
|
46
|
+
offset += text.length;
|
|
47
|
+
let blanked = text;
|
|
48
|
+
if (start < startTagLength || offset > endTagStart) {
|
|
49
|
+
blanked = '';
|
|
50
|
+
for (let i = 0; i < text.length; i++) {
|
|
51
|
+
const at = start + i;
|
|
52
|
+
blanked += at < startTagLength || at >= endTagStart ? ' ' : text[i];
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (typeof piece === 'string' || piece.start < prefixLength) {
|
|
56
|
+
result.push(blanked);
|
|
57
|
+
}
|
|
58
|
+
else if (piece.text === undefined && blanked === text) {
|
|
59
|
+
result.push({ start: piece.start - prefixLength, end: piece.end - prefixLength });
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
result.push({ start: piece.start - prefixLength, end: piece.end - prefixLength, text: blanked });
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return { htmlCode: toString(result, prefixedCode.slice(prefixLength)), pieces: result };
|
|
66
|
+
}
|
|
67
|
+
function pieceText(piece, pugCode) {
|
|
68
|
+
return typeof piece === 'string' ? piece : piece.text ?? pugCode.slice(piece.start, piece.end);
|
|
69
|
+
}
|
|
70
|
+
function toString(pieces, pugCode) {
|
|
71
|
+
return pieces.map(piece => pieceText(piece, pugCode)).join('');
|
|
72
|
+
}
|
|
73
|
+
function calculateMinIndent(s) {
|
|
74
|
+
const lines = s.split('\n');
|
|
75
|
+
const minIndent = lines.reduce(function (minIndent, line) {
|
|
76
|
+
if (line.trim() === '') {
|
|
77
|
+
return minIndent;
|
|
78
|
+
}
|
|
79
|
+
const indent = line.match(/^\s*/)?.[0]?.length || 0;
|
|
80
|
+
return Math.min(indent, minIndent);
|
|
81
|
+
}, Infinity);
|
|
82
|
+
return minIndent;
|
|
83
|
+
}
|
|
84
|
+
function toPrevLine(node) {
|
|
85
|
+
const visited = new Set();
|
|
86
|
+
visit(node);
|
|
87
|
+
return node;
|
|
88
|
+
function visit(obj) {
|
|
89
|
+
if (visited.has(obj)) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
visited.add(obj);
|
|
93
|
+
if ('line' in obj && typeof obj.line === 'number') {
|
|
94
|
+
obj.line--;
|
|
95
|
+
}
|
|
96
|
+
for (const value of Object.values(obj)) {
|
|
97
|
+
if (value && typeof value === 'object') {
|
|
98
|
+
visit(value);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
function parseCode(pugCode) {
|
|
104
|
+
const fileName = 'file.pug';
|
|
105
|
+
const lines = new lines_1.Lines(pugCode);
|
|
106
|
+
const codes = [];
|
|
107
|
+
let error;
|
|
108
|
+
let emptyLineEnds = [];
|
|
109
|
+
let attrsBlocks;
|
|
110
|
+
let ast;
|
|
111
|
+
try {
|
|
112
|
+
const tokens = (0, pug_lexer_1.default)(pugCode, { filename: fileName });
|
|
113
|
+
emptyLineEnds = collectEmptyLineEnds(tokens);
|
|
114
|
+
attrsBlocks = collectAttrsBlocks(tokens);
|
|
115
|
+
ast = pugParser(tokens, { filename: fileName, src: pugCode });
|
|
116
|
+
visitNode(ast, undefined, undefined);
|
|
117
|
+
codes.push(mapped('', pugCode.trimEnd().length));
|
|
118
|
+
}
|
|
119
|
+
catch (e) {
|
|
120
|
+
const _error = e;
|
|
121
|
+
error = {
|
|
122
|
+
..._error,
|
|
123
|
+
line: _error.line - 1,
|
|
124
|
+
column: _error.column - 1,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
return {
|
|
128
|
+
htmlCode: toString(codes, pugCode),
|
|
129
|
+
pieces: codes,
|
|
130
|
+
error,
|
|
131
|
+
ast,
|
|
132
|
+
emptyLineEnds,
|
|
133
|
+
};
|
|
134
|
+
/** `text` that maps to `sourceLength` chars at `offset`; a copy of them carries no text. */
|
|
135
|
+
function mapped(text, offset, sourceLength = text.length) {
|
|
136
|
+
const end = offset + sourceLength;
|
|
137
|
+
return text === pugCode.slice(offset, end) ? { start: offset, end } : { start: offset, end, text };
|
|
138
|
+
}
|
|
139
|
+
function visitNode(node, next, parent) {
|
|
140
|
+
if (node.type === 'Block') {
|
|
141
|
+
for (let i = 0; i < node.nodes.length; i++) {
|
|
142
|
+
visitNode(node.nodes[i], node.nodes[i + 1], node);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
else if (node.type === 'Mixin') {
|
|
146
|
+
if (node.block) {
|
|
147
|
+
visitNode(node.block, undefined, node);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
else if (node.type === 'Tag') {
|
|
151
|
+
const pugTagRange = getDocRange(node.line, node.column, node.name.length);
|
|
152
|
+
codes.push(mapped('', pugTagRange.start));
|
|
153
|
+
const selfClosing = node.block.nodes.length === 0;
|
|
154
|
+
const startTagEnd = addStartTag(node, selfClosing);
|
|
155
|
+
if (!selfClosing) {
|
|
156
|
+
visitNode(node.block, next, parent);
|
|
157
|
+
addEndTag(node, next, parent);
|
|
158
|
+
}
|
|
159
|
+
codes.push(mapped('', selfClosing ? startTagEnd : pugTagRange.start));
|
|
160
|
+
}
|
|
161
|
+
else if (node.type === 'Text') {
|
|
162
|
+
codes.push(mapped(node.val, getDocOffset(node.line, node.column)));
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
/** Returns the end of the start tag in the pug code. */
|
|
166
|
+
function addStartTag(node, selfClosing) {
|
|
167
|
+
codes.push(mapped('', getDocOffset(node.line, node.column)));
|
|
168
|
+
codes.push('<');
|
|
169
|
+
const tagRange = getDocRange(node.line, node.column, node.name.length);
|
|
170
|
+
let end = tagRange.start;
|
|
171
|
+
if (pugCode.substring(tagRange.start, tagRange.end) === node.name) {
|
|
172
|
+
codes.push(mapped(node.name, tagRange.start));
|
|
173
|
+
end = tagRange.end;
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
codes.push(node.name);
|
|
177
|
+
}
|
|
178
|
+
for (const attr of node.attrs.filter(attr => !attr.mustEscape)) {
|
|
179
|
+
codes.push(' ');
|
|
180
|
+
if (attr.name === 'class' || attr.name === 'id') {
|
|
181
|
+
const offset = getDocOffset(attr.line, attr.column);
|
|
182
|
+
for (const char of attr.name) {
|
|
183
|
+
codes.push(mapped(char, offset));
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
codes.push(attr.name);
|
|
188
|
+
}
|
|
189
|
+
if (typeof attr.val !== 'boolean') {
|
|
190
|
+
codes.push('=');
|
|
191
|
+
const offset = getDocOffset(attr.line, attr.column);
|
|
192
|
+
const literalName = attr.val.slice(1, -1);
|
|
193
|
+
if ((attr.name === 'class' || attr.name === 'id')
|
|
194
|
+
&& pugCode.slice(offset + 1, offset + 1 + literalName.length) === literalName) {
|
|
195
|
+
// `'foo'` of `.foo`: the opening quote is the `.`, and the closing quote has no source
|
|
196
|
+
codes.push(mapped(attr.val.slice(0, -1), offset));
|
|
197
|
+
codes.push(mapped(attr.val.slice(-1), offset + 1 + literalName.length, 0));
|
|
198
|
+
end = Math.max(end, offset + 1 + literalName.length);
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
codes.push(mapped(attr.val, offset));
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
const attrsBlock = attrsBlocks.get(getDocOffset(node.line, node.column)); // support attr auto-complete in spaces
|
|
206
|
+
if (attrsBlock) {
|
|
207
|
+
codes.push(' ');
|
|
208
|
+
codes.push(mapped(attrsBlock.text, attrsBlock.offset));
|
|
209
|
+
end = Math.max(end, attrsBlock.end);
|
|
210
|
+
}
|
|
211
|
+
if (selfClosing) {
|
|
212
|
+
codes.push(' />');
|
|
213
|
+
}
|
|
214
|
+
else {
|
|
215
|
+
codes.push('>');
|
|
216
|
+
}
|
|
217
|
+
return end;
|
|
218
|
+
}
|
|
219
|
+
function addEndTag(node, next, parent) {
|
|
220
|
+
let nextStart;
|
|
221
|
+
if (next) {
|
|
222
|
+
if (next.type === 'Block') {
|
|
223
|
+
nextStart = getDocOffset(next.line, 1);
|
|
224
|
+
}
|
|
225
|
+
else {
|
|
226
|
+
nextStart = getDocOffset(next.line, next.column);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
else if (!parent) {
|
|
230
|
+
nextStart = pugCode.length;
|
|
231
|
+
}
|
|
232
|
+
if (nextStart !== undefined) {
|
|
233
|
+
codes.push(mapped('', nextStart));
|
|
234
|
+
}
|
|
235
|
+
codes.push(`</${node.name}>`);
|
|
236
|
+
}
|
|
237
|
+
function collectEmptyLineEnds(tokens) {
|
|
238
|
+
const ends = [];
|
|
239
|
+
for (const token of tokens) {
|
|
240
|
+
if (token.type === 'newline' || token.type === 'outdent') {
|
|
241
|
+
let currentLine = token.loc.start.line - 2;
|
|
242
|
+
let prevLine = lines.lineText(currentLine);
|
|
243
|
+
while (prevLine.trim() === '') {
|
|
244
|
+
ends.push(lines.offsetAt(currentLine + 1, 0) - 1);
|
|
245
|
+
if (currentLine <= 0) {
|
|
246
|
+
break;
|
|
247
|
+
}
|
|
248
|
+
currentLine--;
|
|
249
|
+
prevLine = lines.lineText(currentLine);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return ends.sort((a, b) => a - b);
|
|
254
|
+
}
|
|
255
|
+
function collectAttrsBlocks(tokens) {
|
|
256
|
+
const blocks = new Map();
|
|
257
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
258
|
+
const token = tokens[i];
|
|
259
|
+
if (token.type === 'start-attributes') {
|
|
260
|
+
let tagStart = token;
|
|
261
|
+
for (let j = i - 1; j >= 0; j--) {
|
|
262
|
+
const prevToken = tokens[j];
|
|
263
|
+
if (prevToken.type === 'newline'
|
|
264
|
+
|| prevToken.type === 'indent'
|
|
265
|
+
|| prevToken.type === 'outdent'
|
|
266
|
+
|| prevToken.type === ':') {
|
|
267
|
+
break;
|
|
268
|
+
}
|
|
269
|
+
tagStart = prevToken;
|
|
270
|
+
if (prevToken.type === 'tag') {
|
|
271
|
+
break;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
let prevToken = token;
|
|
275
|
+
let text = '';
|
|
276
|
+
for (i++; i < tokens.length; i++) {
|
|
277
|
+
const attrToken = tokens[i];
|
|
278
|
+
addPrevSpace(attrToken);
|
|
279
|
+
if (attrToken.type === 'attribute') {
|
|
280
|
+
let attrText = pugCode.substring(getDocOffset(attrToken.loc.start.line, attrToken.loc.start.column), getDocOffset(attrToken.loc.end.line, attrToken.loc.end.column));
|
|
281
|
+
if (typeof attrToken.val === 'string' && attrText.includes('=')) {
|
|
282
|
+
// the lexer reads the code with `\n` line ends, so the value is found in that text and read from the code
|
|
283
|
+
const valStart = toCodeIndex(attrText, attrText.replace(/\r\n?/g, '\n').lastIndexOf(attrToken.val));
|
|
284
|
+
let valText = attrText.slice(valStart);
|
|
285
|
+
if (valText.startsWith('`') && valText.endsWith('`')) {
|
|
286
|
+
const innerContent = valText.slice(1, -1);
|
|
287
|
+
if (!innerContent.includes('"')) {
|
|
288
|
+
valText = `"${innerContent}"`;
|
|
289
|
+
}
|
|
290
|
+
else if (!innerContent.includes("'")) {
|
|
291
|
+
valText = `'${innerContent}'`;
|
|
292
|
+
}
|
|
293
|
+
else {
|
|
294
|
+
// Both quote types present: convert inner single quotes to double quotes
|
|
295
|
+
// This allows using single quotes as the outer delimiter
|
|
296
|
+
// JavaScript accepts both 'str' and "str" for string literals
|
|
297
|
+
valText = `'${innerContent.replace(/'/g, '"')}'`;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
valText = valText.replace(/ \\(?=\r?\n)/g, '//');
|
|
301
|
+
text += attrText.slice(0, valStart) + valText;
|
|
302
|
+
}
|
|
303
|
+
else {
|
|
304
|
+
text += attrText;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
else if (attrToken.type === 'end-attributes') {
|
|
308
|
+
blocks.set(getDocOffset(tagStart.loc.start.line, tagStart.loc.start.column), {
|
|
309
|
+
offset: getDocOffset(token.loc.end.line, token.loc.end.column),
|
|
310
|
+
end: getDocOffset(attrToken.loc.end.line, attrToken.loc.end.column),
|
|
311
|
+
text,
|
|
312
|
+
});
|
|
313
|
+
break;
|
|
314
|
+
}
|
|
315
|
+
prevToken = attrToken;
|
|
316
|
+
}
|
|
317
|
+
function addPrevSpace(currentToken) {
|
|
318
|
+
text += pugCode.substring(getDocOffset(prevToken.loc.end.line, prevToken.loc.end.column), getDocOffset(currentToken.loc.start.line, currentToken.loc.start.column)).replace(/,/g, '\n');
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
return blocks;
|
|
323
|
+
}
|
|
324
|
+
function getDocOffset(pugLine, pugColumn) {
|
|
325
|
+
return lines.offsetAt(pugLine - 1, pugColumn - 1);
|
|
326
|
+
}
|
|
327
|
+
function getDocRange(pugLine, pugColumn, length) {
|
|
328
|
+
const start = getDocOffset(pugLine, pugColumn);
|
|
329
|
+
const end = start + length;
|
|
330
|
+
return {
|
|
331
|
+
start,
|
|
332
|
+
end,
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
/** The index in `code` of `index` in `code` with `\n` line ends. */
|
|
337
|
+
function toCodeIndex(code, index) {
|
|
338
|
+
let codeIndex = 0;
|
|
339
|
+
for (let i = 0; i < index; i++) {
|
|
340
|
+
if (code[codeIndex] === '\r' && code[codeIndex + 1] === '\n') {
|
|
341
|
+
codeIndex++;
|
|
342
|
+
}
|
|
343
|
+
codeIndex++;
|
|
344
|
+
}
|
|
345
|
+
return codeIndex;
|
|
346
|
+
}
|
|
347
|
+
//# sourceMappingURL=parse.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,30 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
"name": "muggle-pug",
|
|
3
|
+
"version": "0.1.0-alpha.0",
|
|
4
|
+
"description": "Pug compiled to HTML as pieces that map to the pug code",
|
|
5
|
+
"main": "out/index.js",
|
|
6
|
+
"types": "out/index.d.ts",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"files": [
|
|
9
|
+
"out/**/*.js",
|
|
10
|
+
"out/**/*.d.ts"
|
|
11
|
+
],
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "https://github.com/johnsoncodehk/muggle-pug.git"
|
|
15
|
+
},
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "tsc",
|
|
18
|
+
"test": "vitest run"
|
|
19
|
+
},
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"pug-lexer": "^5.0.1",
|
|
22
|
+
"pug-parser": "^6.0.0"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@types/node": "^26.0.1",
|
|
26
|
+
"@vue/compiler-dom": "^3.5.0",
|
|
27
|
+
"typescript": "7.1.0-beta",
|
|
28
|
+
"vitest": "latest"
|
|
29
|
+
}
|
|
30
|
+
}
|