@markuplint/pug-parser 4.0.0-dev.28 → 4.0.0-rc.1
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 +1 -1
- package/lib/index.d.ts +1 -2
- package/lib/index.js +1 -2
- package/lib/parser.d.ts +25 -0
- package/lib/parser.js +269 -0
- package/lib/pug-parser/index.d.ts +16 -4
- package/lib/pug-parser/index.js +40 -7
- package/package.json +5 -5
- package/lib/attr-tokenizer.d.ts +0 -3
- package/lib/attr-tokenizer.js +0 -137
- package/lib/parse.d.ts +0 -2
- package/lib/parse.js +0 -255
package/LICENSE
CHANGED
package/lib/index.d.ts
CHANGED
|
@@ -1,2 +1 @@
|
|
|
1
|
-
export {
|
|
2
|
-
export declare const endTag = "never";
|
|
1
|
+
export { parser } from './parser.js';
|
package/lib/index.js
CHANGED
|
@@ -1,2 +1 @@
|
|
|
1
|
-
export {
|
|
2
|
-
export const endTag = 'never';
|
|
1
|
+
export { parser } from './parser.js';
|
package/lib/parser.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { ASTNode } from './pug-parser/index.js';
|
|
2
|
+
import type { MLASTAttr, MLASTNodeTreeItem, MLASTParentNode } from '@markuplint/ml-ast';
|
|
3
|
+
import type { ChildToken, ParseOptions, Token } from '@markuplint/parser-utils';
|
|
4
|
+
import { ParserError, Parser } from '@markuplint/parser-utils';
|
|
5
|
+
declare class PugParser extends Parser<ASTNode> {
|
|
6
|
+
constructor();
|
|
7
|
+
tokenize(options?: ParseOptions): {
|
|
8
|
+
ast: ASTNode[];
|
|
9
|
+
isFragment: boolean;
|
|
10
|
+
};
|
|
11
|
+
parseError(error: any): ParserError;
|
|
12
|
+
nodeize(originNode: ASTNode, parentNode: MLASTParentNode | null, depth: number): readonly MLASTNodeTreeItem[];
|
|
13
|
+
afterFlattenNodes(nodeList: readonly MLASTNodeTreeItem[]): readonly MLASTNodeTreeItem[];
|
|
14
|
+
visitElement(token: ChildToken & {
|
|
15
|
+
readonly nodeName: string;
|
|
16
|
+
readonly namespace: string;
|
|
17
|
+
}, childNodes: readonly ASTNode[], options: {
|
|
18
|
+
readonly overwriteProps: {
|
|
19
|
+
readonly attributes: readonly MLASTAttr[];
|
|
20
|
+
};
|
|
21
|
+
}): MLASTNodeTreeItem[];
|
|
22
|
+
visitAttr(token: Token): MLASTAttr;
|
|
23
|
+
}
|
|
24
|
+
export declare const parser: PugParser;
|
|
25
|
+
export {};
|
package/lib/parser.js
ADDED
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import { HtmlParser, getNamespace } from '@markuplint/html-parser';
|
|
2
|
+
import { ParserError, Parser, AttrState, scriptParser } from '@markuplint/parser-utils';
|
|
3
|
+
import { pugParse } from './pug-parser/index.js';
|
|
4
|
+
class HtmlInPugParser extends HtmlParser {
|
|
5
|
+
constructor() {
|
|
6
|
+
super({
|
|
7
|
+
ignoreTags: [
|
|
8
|
+
/**
|
|
9
|
+
* Tag Interpolation
|
|
10
|
+
*
|
|
11
|
+
* @see https://pugjs.org/language/interpolation.html#tag-interpolation
|
|
12
|
+
*/
|
|
13
|
+
{
|
|
14
|
+
type: 'tag-interpolation',
|
|
15
|
+
start: '#[',
|
|
16
|
+
end: ']',
|
|
17
|
+
},
|
|
18
|
+
],
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
class PugParser extends Parser {
|
|
23
|
+
constructor() {
|
|
24
|
+
super({
|
|
25
|
+
endTagType: 'never',
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
tokenize(options) {
|
|
29
|
+
const offsetOffset = options?.offsetOffset ?? 0;
|
|
30
|
+
return {
|
|
31
|
+
ast: pugParse(this.rawCode, offsetOffset >= 1).nodes,
|
|
32
|
+
isFragment: true,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
parseError(error) {
|
|
36
|
+
if (error instanceof Error && 'msg' in error && 'line' in error && 'column' in error && 'src' in error) {
|
|
37
|
+
return new ParserError(error.msg, {
|
|
38
|
+
line: error.line,
|
|
39
|
+
col: error.column,
|
|
40
|
+
raw: error.src,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
return super.parseError(error);
|
|
44
|
+
}
|
|
45
|
+
nodeize(
|
|
46
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
47
|
+
originNode, parentNode, depth) {
|
|
48
|
+
const parentNamespace = parentNode && 'namespace' in parentNode ? parentNode.namespace : 'http://www.w3.org/1999/xhtml';
|
|
49
|
+
const token = this.sliceFragment(originNode.offset, originNode.endOffset);
|
|
50
|
+
switch (originNode.type) {
|
|
51
|
+
case 'Doctype': {
|
|
52
|
+
return this.visitDoctype({
|
|
53
|
+
...token,
|
|
54
|
+
depth,
|
|
55
|
+
parentNode,
|
|
56
|
+
name: originNode.val ?? '',
|
|
57
|
+
publicId: '',
|
|
58
|
+
systemId: '',
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
case 'Text': {
|
|
62
|
+
if (originNode.raw.trim() === '') {
|
|
63
|
+
return [];
|
|
64
|
+
}
|
|
65
|
+
const htmlDoc = new HtmlInPugParser().parse(originNode.raw, {
|
|
66
|
+
offsetOffset: originNode.offset,
|
|
67
|
+
offsetLine: originNode.line,
|
|
68
|
+
offsetColumn: originNode.column,
|
|
69
|
+
depth,
|
|
70
|
+
});
|
|
71
|
+
const newNodeList = [];
|
|
72
|
+
for (const node of htmlDoc.nodeList) {
|
|
73
|
+
if (node.nodeName === '#ps:tag-interpolation') {
|
|
74
|
+
// Remove `#[` and `]`
|
|
75
|
+
const raw = node.raw.slice(2, -1);
|
|
76
|
+
const innerNodes = new PugParser().parse(raw, {
|
|
77
|
+
offsetOffset: node.startOffset + 2,
|
|
78
|
+
offsetLine: node.startLine,
|
|
79
|
+
offsetColumn: node.startCol + 2,
|
|
80
|
+
depth: node.depth,
|
|
81
|
+
});
|
|
82
|
+
newNodeList.push(...innerNodes.nodeList);
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
newNodeList.push(node);
|
|
86
|
+
}
|
|
87
|
+
return newNodeList;
|
|
88
|
+
}
|
|
89
|
+
case 'Comment': {
|
|
90
|
+
return this.visitComment({
|
|
91
|
+
...token,
|
|
92
|
+
depth,
|
|
93
|
+
parentNode,
|
|
94
|
+
}, {
|
|
95
|
+
isBogus: false,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
case 'Tag': {
|
|
99
|
+
const namespace = getNamespace(originNode.name, parentNamespace);
|
|
100
|
+
const attrs = originNode.attrs.map(attr => {
|
|
101
|
+
// eslint-disable-next-line prefer-const
|
|
102
|
+
let { offset, endOffset } = this.getOffsetsFromCode(attr.line, attr.column, attr.endLine, attr.endColumn);
|
|
103
|
+
if ((attr.name === 'id' || attr.name === 'class') &&
|
|
104
|
+
attr.offset === attr.endOffset &&
|
|
105
|
+
typeof attr.val === 'string') {
|
|
106
|
+
/**
|
|
107
|
+
* #value =>
|
|
108
|
+
* {
|
|
109
|
+
* name: 'id',
|
|
110
|
+
* val: "'value'",
|
|
111
|
+
* }
|
|
112
|
+
* Remove single quotes and add (#|.) prefix
|
|
113
|
+
*/
|
|
114
|
+
endOffset = attr.offset + attr.val.length - 1;
|
|
115
|
+
}
|
|
116
|
+
const token = this.sliceFragment(offset, endOffset);
|
|
117
|
+
return this.visitAttr(token);
|
|
118
|
+
});
|
|
119
|
+
// &attributes(syntax)
|
|
120
|
+
const andAttr = originNode.attributeBlocks.map(block => {
|
|
121
|
+
const blockLength = '&attributes('.length;
|
|
122
|
+
const { offset, endOffset } = this.getOffsetsFromCode(block.line, block.column + blockLength, block.line, block.column + blockLength + block.val.length);
|
|
123
|
+
const token = this.sliceFragment(offset, endOffset);
|
|
124
|
+
const node = this.createToken(token.raw, token.startOffset, token.startLine, token.startCol);
|
|
125
|
+
return {
|
|
126
|
+
...node,
|
|
127
|
+
type: 'spread',
|
|
128
|
+
nodeName: '#spread',
|
|
129
|
+
};
|
|
130
|
+
});
|
|
131
|
+
return this.visitElement({
|
|
132
|
+
...token,
|
|
133
|
+
depth,
|
|
134
|
+
parentNode,
|
|
135
|
+
nodeName: originNode.name,
|
|
136
|
+
namespace,
|
|
137
|
+
}, originNode.block.nodes, {
|
|
138
|
+
overwriteProps: {
|
|
139
|
+
attributes: [...attrs, ...andAttr],
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
default: {
|
|
144
|
+
return this.visitPsBlock({
|
|
145
|
+
...token,
|
|
146
|
+
depth,
|
|
147
|
+
parentNode,
|
|
148
|
+
nodeName: originNode.type,
|
|
149
|
+
}, 'block' in originNode && originNode.block ? originNode.block.nodes : []);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
afterFlattenNodes(nodeList) {
|
|
154
|
+
return super.afterFlattenNodes(nodeList, {
|
|
155
|
+
exposeInvalidNode: false,
|
|
156
|
+
exposeWhiteSpace: false,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
visitElement(token,
|
|
160
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
161
|
+
childNodes, options) {
|
|
162
|
+
const startTag = {
|
|
163
|
+
...token,
|
|
164
|
+
...this.createToken(token),
|
|
165
|
+
...options.overwriteProps,
|
|
166
|
+
type: 'starttag',
|
|
167
|
+
elementType: this.detectElementType(token.nodeName),
|
|
168
|
+
childNodes: [],
|
|
169
|
+
pairNode: null,
|
|
170
|
+
tagOpenChar: '',
|
|
171
|
+
tagCloseChar: '',
|
|
172
|
+
isGhost: false,
|
|
173
|
+
};
|
|
174
|
+
const siblings = this.visitChildren(childNodes, startTag);
|
|
175
|
+
return [startTag, ...siblings];
|
|
176
|
+
}
|
|
177
|
+
visitAttr(token) {
|
|
178
|
+
if (token.raw[0] === '#' || token.raw[0] === '.') {
|
|
179
|
+
const attr = super.visitAttr(token, {
|
|
180
|
+
startState: AttrState.BeforeValue,
|
|
181
|
+
quoteSet: [],
|
|
182
|
+
quoteInValueChars: [],
|
|
183
|
+
endOfUnquotedValueChars: [],
|
|
184
|
+
});
|
|
185
|
+
if (attr.type === 'spread') {
|
|
186
|
+
return attr;
|
|
187
|
+
}
|
|
188
|
+
const potentialName = token.raw[0] === '#' ? 'id' : 'class';
|
|
189
|
+
this.updateAttr(attr, {
|
|
190
|
+
potentialName,
|
|
191
|
+
potentialValue: attr.raw.slice(1),
|
|
192
|
+
isDuplicatable: potentialName === 'class',
|
|
193
|
+
});
|
|
194
|
+
return attr;
|
|
195
|
+
}
|
|
196
|
+
const attr = super.visitAttr(token, {
|
|
197
|
+
quoteSet: [],
|
|
198
|
+
quoteInValueChars: [],
|
|
199
|
+
endOfUnquotedValueChars: [],
|
|
200
|
+
});
|
|
201
|
+
if (attr.type === 'spread') {
|
|
202
|
+
return attr;
|
|
203
|
+
}
|
|
204
|
+
if (attr.name.raw.toLowerCase() === 'class') {
|
|
205
|
+
this.updateAttr(attr, { isDuplicatable: true });
|
|
206
|
+
}
|
|
207
|
+
if (attr.name.raw.startsWith("'") && attr.name.raw.endsWith("'")) {
|
|
208
|
+
this.updateAttr(attr, {
|
|
209
|
+
potentialName: attr.name.raw.slice(1, -1),
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
if (attr.name.raw.endsWith('!')) {
|
|
213
|
+
this.updateAttr(attr, {
|
|
214
|
+
potentialName: attr.name.raw.slice(0, -1),
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
const valueCodeTokens = scriptParser(attr.value.raw.trim());
|
|
218
|
+
if (valueCodeTokens.length === 1) {
|
|
219
|
+
const token = valueCodeTokens[0];
|
|
220
|
+
switch (token.type) {
|
|
221
|
+
case 'Numeric': {
|
|
222
|
+
return {
|
|
223
|
+
...attr,
|
|
224
|
+
valueType: 'number',
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
case 'Boolean': {
|
|
228
|
+
return {
|
|
229
|
+
...attr,
|
|
230
|
+
valueType: 'boolean',
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
case 'String':
|
|
234
|
+
case 'Template': {
|
|
235
|
+
const value = super.visitAttr(attr.value, {
|
|
236
|
+
startState: AttrState.BeforeValue,
|
|
237
|
+
quoteSet: [
|
|
238
|
+
{ start: '"', end: '"' },
|
|
239
|
+
{ start: "'", end: "'" },
|
|
240
|
+
{ start: '`', end: '`' },
|
|
241
|
+
],
|
|
242
|
+
quoteInValueChars: [
|
|
243
|
+
{ start: '"', end: '"' },
|
|
244
|
+
{ start: "'", end: "'" },
|
|
245
|
+
{ start: '`', end: '`' },
|
|
246
|
+
{ start: '${', end: '}' },
|
|
247
|
+
],
|
|
248
|
+
});
|
|
249
|
+
if (value.type === 'spread') {
|
|
250
|
+
throw new ParserError('Unexpected attribute value', value);
|
|
251
|
+
}
|
|
252
|
+
return {
|
|
253
|
+
...attr,
|
|
254
|
+
startQuote: value.startQuote,
|
|
255
|
+
value: value.value,
|
|
256
|
+
endQuote: value.endQuote,
|
|
257
|
+
...(attr.name.raw.endsWith('!') ? { valueType: 'code' } : {}),
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return {
|
|
263
|
+
...attr,
|
|
264
|
+
isDynamicValue: true,
|
|
265
|
+
valueType: 'code',
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
export const parser = new PugParser();
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
export declare function pugParse(pug: string): ASTBlock;
|
|
1
|
+
export declare function pugParse(pug: string, useOffset?: boolean): ASTBlock;
|
|
2
2
|
export type ASTBlock = PugAST<ASTNode>;
|
|
3
|
-
export type ASTNode = ASTTagNode | ASTTextNode | ASTCodeNode | ASTComment | ASTDoctype | ASTIncludeNode | ASTMixinNode | ASTMixinSlotNode | ASTFilterNode | ASTEachNode | ASTConditionalNode | ASTCaseNode | ASTCaseWhenNode;
|
|
4
|
-
export type ASTTagNode = Omit<PugASTTagNode<ASTAttr, ASTBlock>, '
|
|
3
|
+
export type ASTNode = ASTTagNode | ASTTextNode | ASTEmptyPipeNode | ASTCodeNode | ASTComment | ASTDoctype | ASTIncludeNode | ASTMixinNode | ASTMixinSlotNode | ASTFilterNode | ASTEachNode | ASTConditionalNode | ASTCaseNode | ASTCaseWhenNode;
|
|
4
|
+
export type ASTTagNode = Omit<PugASTTagNode<ASTAttr, ASTBlock>, 'selfClosing' | 'isInline'> & AdditionalASTData;
|
|
5
5
|
export type ASTTextNode = Omit<PugASTTextNode, 'val'> & AdditionalASTData;
|
|
6
|
+
export type ASTEmptyPipeNode = PugASTEmptyPipeNode & AdditionalASTData;
|
|
6
7
|
export type ASTCodeNode = PugASTCodeNode & AdditionalASTData;
|
|
7
8
|
export type ASTComment = PugASTCommentNode & AdditionalASTData;
|
|
8
9
|
export type ASTDoctype = PugASTDoctypeNode & AdditionalASTData;
|
|
@@ -34,7 +35,12 @@ type PugASTTagNode<A, B> = {
|
|
|
34
35
|
name: string;
|
|
35
36
|
selfClosing: boolean;
|
|
36
37
|
attrs: A[];
|
|
37
|
-
attributeBlocks:
|
|
38
|
+
attributeBlocks: {
|
|
39
|
+
type: 'AttributeBlock';
|
|
40
|
+
val: string;
|
|
41
|
+
line: number;
|
|
42
|
+
column: number;
|
|
43
|
+
}[];
|
|
38
44
|
isInline: boolean;
|
|
39
45
|
line: number;
|
|
40
46
|
column: number;
|
|
@@ -47,6 +53,12 @@ type PugASTTextNode = {
|
|
|
47
53
|
line: number;
|
|
48
54
|
column: number;
|
|
49
55
|
};
|
|
56
|
+
type PugASTEmptyPipeNode = {
|
|
57
|
+
type: 'EmptyPipe';
|
|
58
|
+
val: string;
|
|
59
|
+
line: number;
|
|
60
|
+
column: number;
|
|
61
|
+
};
|
|
50
62
|
type PugASTCodeNode = {
|
|
51
63
|
type: 'Code';
|
|
52
64
|
val: string;
|
package/lib/pug-parser/index.js
CHANGED
|
@@ -1,19 +1,29 @@
|
|
|
1
|
-
// @ts-ignore
|
|
2
1
|
import lexer from 'pug-lexer';
|
|
3
2
|
// @ts-ignore
|
|
4
3
|
import parser from 'pug-parser';
|
|
5
4
|
import { getOffsetFromLineAndCol } from '../utils/get-offset-from-line-and-col.js';
|
|
6
|
-
export function pugParse(pug) {
|
|
7
|
-
|
|
5
|
+
export function pugParse(pug, useOffset = false) {
|
|
6
|
+
let lexOrigin = lexer(pug);
|
|
7
|
+
/**
|
|
8
|
+
* Exclude indent and outdent tokens when offset is received to avoid indentation errors
|
|
9
|
+
*/
|
|
10
|
+
if (useOffset) {
|
|
11
|
+
const newLexOrigin = [];
|
|
12
|
+
for (const token of lexOrigin) {
|
|
13
|
+
if (token.type === 'indent' || token.type === 'outdent') {
|
|
14
|
+
continue;
|
|
15
|
+
}
|
|
16
|
+
newLexOrigin.push(token);
|
|
17
|
+
}
|
|
18
|
+
lexOrigin = newLexOrigin;
|
|
19
|
+
}
|
|
8
20
|
const lex = JSON.parse(JSON.stringify(lexOrigin));
|
|
9
21
|
const originAst = parser(lexOrigin);
|
|
10
|
-
// console.log(lex);
|
|
11
|
-
// console.log(JSON.stringify(originAst, null, 2));
|
|
12
22
|
const ast = optimizeAST(originAst, lex, pug);
|
|
13
23
|
return ast;
|
|
14
24
|
}
|
|
15
25
|
function getOffsetsFromLines(pug) {
|
|
16
|
-
const lines = pug.split(/\n/
|
|
26
|
+
const lines = pug.split(/\n/);
|
|
17
27
|
let chars = 0;
|
|
18
28
|
const result = lines.map(line => {
|
|
19
29
|
chars += line.length + 1;
|
|
@@ -76,6 +86,7 @@ tokens, pug) {
|
|
|
76
86
|
endColumn,
|
|
77
87
|
block,
|
|
78
88
|
attrs,
|
|
89
|
+
attributeBlocks: node.attributeBlocks,
|
|
79
90
|
};
|
|
80
91
|
nodes.push(tagNode);
|
|
81
92
|
continue;
|
|
@@ -224,6 +235,27 @@ tokens, pug) {
|
|
|
224
235
|
const { endOffset, endLine, endColumn } = getRawTextAndLocationEnd(node.val, offset, line, column, tokens, offsets, pug);
|
|
225
236
|
const raw = pug.slice(offset, endOffset);
|
|
226
237
|
// console.log({ v: node.val, r: raw });
|
|
238
|
+
/**
|
|
239
|
+
*
|
|
240
|
+
* Empty piped line
|
|
241
|
+
*
|
|
242
|
+
* @see https://pugjs.org/language/plain-text.html#recommended-solutions
|
|
243
|
+
*/
|
|
244
|
+
if (raw === '|') {
|
|
245
|
+
const newNode = {
|
|
246
|
+
type: 'EmptyPipe',
|
|
247
|
+
raw,
|
|
248
|
+
val: node.val,
|
|
249
|
+
offset,
|
|
250
|
+
endOffset,
|
|
251
|
+
line,
|
|
252
|
+
endLine,
|
|
253
|
+
column,
|
|
254
|
+
endColumn,
|
|
255
|
+
};
|
|
256
|
+
nodes.push(newNode);
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
227
259
|
const textNode = {
|
|
228
260
|
type: node.type,
|
|
229
261
|
raw,
|
|
@@ -292,8 +324,9 @@ tokens, pug) {
|
|
|
292
324
|
continue;
|
|
293
325
|
}
|
|
294
326
|
default: {
|
|
327
|
+
throw new Error(`Unsupported syntax: The "${
|
|
295
328
|
// @ts-ignore
|
|
296
|
-
|
|
329
|
+
node.type}" node\n${JSON.stringify(node, null, 2)}`);
|
|
297
330
|
}
|
|
298
331
|
}
|
|
299
332
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@markuplint/pug-parser",
|
|
3
|
-
"version": "4.0.0-
|
|
3
|
+
"version": "4.0.0-rc.1",
|
|
4
4
|
"description": "Pug parser for markuplint",
|
|
5
5
|
"repository": "git@github.com:markuplint/markuplint.git",
|
|
6
6
|
"author": "Yusuke Hirao <yusukehirao@me.com>",
|
|
@@ -21,11 +21,11 @@
|
|
|
21
21
|
"clean": "tsc --build --clean"
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@markuplint/html-parser": "4.0.0-
|
|
25
|
-
"@markuplint/ml-ast": "4.0.0-
|
|
26
|
-
"@markuplint/parser-utils": "4.0.0-
|
|
24
|
+
"@markuplint/html-parser": "4.0.0-rc.1",
|
|
25
|
+
"@markuplint/ml-ast": "4.0.0-rc.1",
|
|
26
|
+
"@markuplint/parser-utils": "4.0.0-rc.1",
|
|
27
27
|
"pug-lexer": "^5.0.1",
|
|
28
28
|
"pug-parser": "^6.0.0"
|
|
29
29
|
},
|
|
30
|
-
"gitHead": "
|
|
30
|
+
"gitHead": "3a9dbbf4c3c05de66d402802919ee94a46a5eb67"
|
|
31
31
|
}
|
package/lib/attr-tokenizer.d.ts
DELETED
package/lib/attr-tokenizer.js
DELETED
|
@@ -1,137 +0,0 @@
|
|
|
1
|
-
import { tokenizer, uuid } from '@markuplint/parser-utils';
|
|
2
|
-
export default function attrTokenizer(
|
|
3
|
-
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
4
|
-
attr) {
|
|
5
|
-
if (attr.raw[0] === '#' || attr.raw[0] === '.') {
|
|
6
|
-
let value = '';
|
|
7
|
-
if (typeof attr.val === 'string') {
|
|
8
|
-
value = attr.val.match(/(["'`]?)([^\1]+)(\1)/)?.[2] ?? '';
|
|
9
|
-
}
|
|
10
|
-
else {
|
|
11
|
-
value = `${attr.val}`;
|
|
12
|
-
}
|
|
13
|
-
return {
|
|
14
|
-
type: 'ps-attr',
|
|
15
|
-
uuid: uuid(),
|
|
16
|
-
raw: attr.raw,
|
|
17
|
-
startOffset: attr.offset,
|
|
18
|
-
endOffset: attr.endOffset,
|
|
19
|
-
startLine: attr.line,
|
|
20
|
-
endLine: attr.endLine,
|
|
21
|
-
startCol: attr.column,
|
|
22
|
-
endCol: attr.endColumn,
|
|
23
|
-
potentialName: attr.name,
|
|
24
|
-
potentialValue: value ?? '',
|
|
25
|
-
valueType: 'string',
|
|
26
|
-
isDuplicatable: attr.raw[0] === '.',
|
|
27
|
-
nodeName: '#pug-special-attr',
|
|
28
|
-
parentNode: null,
|
|
29
|
-
nextNode: null,
|
|
30
|
-
prevNode: null,
|
|
31
|
-
isFragment: false,
|
|
32
|
-
isGhost: false,
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
|
-
const spacesBeforeAttrString = '';
|
|
36
|
-
const nameChars = attr.name;
|
|
37
|
-
let spacesBeforeEqualChars;
|
|
38
|
-
let equalChars;
|
|
39
|
-
let spacesAfterEqualChars;
|
|
40
|
-
let quoteChars;
|
|
41
|
-
let valueChars;
|
|
42
|
-
let isDynamicValue = undefined;
|
|
43
|
-
if (attr.val === true) {
|
|
44
|
-
spacesBeforeEqualChars = '';
|
|
45
|
-
equalChars = '';
|
|
46
|
-
spacesAfterEqualChars = '';
|
|
47
|
-
quoteChars = '';
|
|
48
|
-
valueChars = '';
|
|
49
|
-
}
|
|
50
|
-
else {
|
|
51
|
-
const withoutName = attr.raw.slice(attr.name.length);
|
|
52
|
-
const valueOffset = withoutName.indexOf(attr.val);
|
|
53
|
-
const equalAndBeforeSpaceAfterSpace = withoutName.slice(0, valueOffset);
|
|
54
|
-
const [, before, equal, after] = equalAndBeforeSpaceAfterSpace.match(/^(\s*)(=)(\s*)$/) ?? ['', '', '', ''];
|
|
55
|
-
const [, quote, coreValue] = attr.val.match(/(["'`]?)([^\1]+)(\1)/) ?? ['', '', ''];
|
|
56
|
-
spacesBeforeEqualChars = before ?? '';
|
|
57
|
-
equalChars = equal ?? '';
|
|
58
|
-
spacesAfterEqualChars = after ?? '';
|
|
59
|
-
quoteChars = quote ?? '';
|
|
60
|
-
valueChars = coreValue ?? '';
|
|
61
|
-
if (quote === '`' || quote === '') {
|
|
62
|
-
isDynamicValue = true;
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
const invalid = !!(valueChars && quoteChars === null && /["'<=>`]/.test(valueChars)) ||
|
|
66
|
-
!!(equalChars && quoteChars === null && valueChars === null);
|
|
67
|
-
if (invalid) {
|
|
68
|
-
throw new Error('Parse error: It has invalid attribute');
|
|
69
|
-
}
|
|
70
|
-
let offset = attr.offset;
|
|
71
|
-
let line = attr.line;
|
|
72
|
-
let col = attr.column;
|
|
73
|
-
const attrToken = tokenizer(attr.raw, line, col, offset);
|
|
74
|
-
const spacesBeforeName = tokenizer(spacesBeforeAttrString, line, col, offset);
|
|
75
|
-
line = spacesBeforeName.endLine;
|
|
76
|
-
col = spacesBeforeName.endCol;
|
|
77
|
-
offset = spacesBeforeName.endOffset;
|
|
78
|
-
const name = tokenizer(nameChars, line, col, offset);
|
|
79
|
-
line = name.endLine;
|
|
80
|
-
col = name.endCol;
|
|
81
|
-
offset = name.endOffset;
|
|
82
|
-
const spacesBeforeEqual = tokenizer(spacesBeforeEqualChars, line, col, offset);
|
|
83
|
-
line = spacesBeforeEqual.endLine;
|
|
84
|
-
col = spacesBeforeEqual.endCol;
|
|
85
|
-
offset = spacesBeforeEqual.endOffset;
|
|
86
|
-
const equal = tokenizer(equalChars, line, col, offset);
|
|
87
|
-
line = equal.endLine;
|
|
88
|
-
col = equal.endCol;
|
|
89
|
-
offset = equal.endOffset;
|
|
90
|
-
const spacesAfterEqual = tokenizer(spacesAfterEqualChars, line, col, offset);
|
|
91
|
-
line = spacesAfterEqual.endLine;
|
|
92
|
-
col = spacesAfterEqual.endCol;
|
|
93
|
-
offset = spacesAfterEqual.endOffset;
|
|
94
|
-
const startQuote = tokenizer(quoteChars, line, col, offset);
|
|
95
|
-
line = startQuote.endLine;
|
|
96
|
-
col = startQuote.endCol;
|
|
97
|
-
offset = startQuote.endOffset;
|
|
98
|
-
const value = tokenizer(valueChars, line, col, offset);
|
|
99
|
-
line = value.endLine;
|
|
100
|
-
col = value.endCol;
|
|
101
|
-
offset = value.endOffset;
|
|
102
|
-
const endQuote = tokenizer(quoteChars, line, col, offset);
|
|
103
|
-
line = endQuote.endLine;
|
|
104
|
-
col = endQuote.endCol;
|
|
105
|
-
offset = endQuote.endOffset;
|
|
106
|
-
let isDuplicatable = false;
|
|
107
|
-
if (name.raw.toLowerCase() === 'class') {
|
|
108
|
-
isDuplicatable = true;
|
|
109
|
-
}
|
|
110
|
-
return {
|
|
111
|
-
type: 'html-attr',
|
|
112
|
-
uuid: uuid(),
|
|
113
|
-
raw: attrToken.raw,
|
|
114
|
-
startOffset: attrToken.startOffset,
|
|
115
|
-
endOffset: attrToken.endOffset,
|
|
116
|
-
startLine: attrToken.startLine,
|
|
117
|
-
endLine: attrToken.endLine,
|
|
118
|
-
startCol: attrToken.startCol,
|
|
119
|
-
endCol: attrToken.endCol,
|
|
120
|
-
spacesBeforeName,
|
|
121
|
-
name,
|
|
122
|
-
spacesBeforeEqual,
|
|
123
|
-
equal,
|
|
124
|
-
spacesAfterEqual,
|
|
125
|
-
startQuote,
|
|
126
|
-
value,
|
|
127
|
-
endQuote,
|
|
128
|
-
isDynamicValue,
|
|
129
|
-
isDuplicatable,
|
|
130
|
-
nodeName: name.raw,
|
|
131
|
-
parentNode: null,
|
|
132
|
-
nextNode: null,
|
|
133
|
-
prevNode: null,
|
|
134
|
-
isFragment: false,
|
|
135
|
-
isGhost: false,
|
|
136
|
-
};
|
|
137
|
-
}
|
package/lib/parse.d.ts
DELETED
package/lib/parse.js
DELETED
|
@@ -1,255 +0,0 @@
|
|
|
1
|
-
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
|
|
2
|
-
if (kind === "m") throw new TypeError("Private method is not writable");
|
|
3
|
-
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
|
|
4
|
-
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
|
|
5
|
-
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
|
|
6
|
-
};
|
|
7
|
-
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
|
|
8
|
-
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
|
|
9
|
-
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
|
|
10
|
-
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
|
|
11
|
-
};
|
|
12
|
-
var _Parser_ast;
|
|
13
|
-
import { getNamespace, parse as htmlParse, isDocumentFragment } from '@markuplint/html-parser';
|
|
14
|
-
import { detectElementType, ignoreFrontMatter, ParserError, tokenizer, uuid, walk, removeDeprecatedNode, } from '@markuplint/parser-utils';
|
|
15
|
-
import attrTokenizer from './attr-tokenizer.js';
|
|
16
|
-
import { pugParse } from './pug-parser/index.js';
|
|
17
|
-
export const parse = (rawCode, options) => {
|
|
18
|
-
let unknownParseError;
|
|
19
|
-
let nodeList;
|
|
20
|
-
if (options?.ignoreFrontMatter) {
|
|
21
|
-
rawCode = ignoreFrontMatter(rawCode);
|
|
22
|
-
}
|
|
23
|
-
try {
|
|
24
|
-
const parser = new Parser(rawCode);
|
|
25
|
-
nodeList = parser.getNodeList();
|
|
26
|
-
}
|
|
27
|
-
catch (error) {
|
|
28
|
-
nodeList = [];
|
|
29
|
-
if (error instanceof Error && 'msg' in error && 'line' in error && 'column' in error && 'src' in error) {
|
|
30
|
-
throw new ParserError(
|
|
31
|
-
// @ts-ignore
|
|
32
|
-
error.msg, {
|
|
33
|
-
// @ts-ignore
|
|
34
|
-
line: error.line,
|
|
35
|
-
// @ts-ignore
|
|
36
|
-
col: error.column,
|
|
37
|
-
// @ts-ignore
|
|
38
|
-
raw: error.src,
|
|
39
|
-
});
|
|
40
|
-
}
|
|
41
|
-
unknownParseError = error instanceof Error ? error.message : new Error(`${error}`).message;
|
|
42
|
-
}
|
|
43
|
-
return {
|
|
44
|
-
nodeList,
|
|
45
|
-
isFragment: isDocumentFragment(rawCode),
|
|
46
|
-
unknownParseError: unknownParseError,
|
|
47
|
-
};
|
|
48
|
-
};
|
|
49
|
-
class Parser {
|
|
50
|
-
constructor(raw) {
|
|
51
|
-
_Parser_ast.set(this, void 0);
|
|
52
|
-
__classPrivateFieldSet(this, _Parser_ast, pugParse(raw), "f");
|
|
53
|
-
// console.log(JSON.stringify(this.#ast, null, 2));
|
|
54
|
-
}
|
|
55
|
-
flattenNodes(
|
|
56
|
-
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
57
|
-
nodeTree) {
|
|
58
|
-
const nodeOrders = [];
|
|
59
|
-
walk(nodeTree, node => {
|
|
60
|
-
nodeOrders.push(node);
|
|
61
|
-
});
|
|
62
|
-
removeDeprecatedNode(nodeOrders);
|
|
63
|
-
return nodeOrders;
|
|
64
|
-
}
|
|
65
|
-
getNodeList() {
|
|
66
|
-
const nodeTree = this.traverse(__classPrivateFieldGet(this, _Parser_ast, "f").nodes, null);
|
|
67
|
-
return this.flattenNodes(nodeTree);
|
|
68
|
-
}
|
|
69
|
-
nodeize(
|
|
70
|
-
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
71
|
-
originNode,
|
|
72
|
-
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
73
|
-
prevNode,
|
|
74
|
-
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
75
|
-
parentNode) {
|
|
76
|
-
const nextNode = null;
|
|
77
|
-
const startOffset = originNode.offset;
|
|
78
|
-
const endOffset = originNode.endOffset;
|
|
79
|
-
const startLine = originNode.line;
|
|
80
|
-
const endLine = originNode.endLine;
|
|
81
|
-
const startCol = originNode.column;
|
|
82
|
-
const endCol = originNode.endColumn;
|
|
83
|
-
const parentNamespace = parentNode && 'namespace' in parentNode ? parentNode.namespace : 'http://www.w3.org/1999/xhtml';
|
|
84
|
-
switch (originNode.type) {
|
|
85
|
-
case 'Doctype': {
|
|
86
|
-
return {
|
|
87
|
-
uuid: uuid(),
|
|
88
|
-
raw: originNode.raw,
|
|
89
|
-
name: originNode.val ?? '',
|
|
90
|
-
// TODO:
|
|
91
|
-
publicId: '',
|
|
92
|
-
// TODO:
|
|
93
|
-
systemId: '',
|
|
94
|
-
startOffset,
|
|
95
|
-
endOffset,
|
|
96
|
-
startLine,
|
|
97
|
-
endLine,
|
|
98
|
-
startCol,
|
|
99
|
-
endCol,
|
|
100
|
-
nodeName: '#doctype',
|
|
101
|
-
type: 'doctype',
|
|
102
|
-
parentNode,
|
|
103
|
-
prevNode,
|
|
104
|
-
_addPrevNode: 102,
|
|
105
|
-
nextNode,
|
|
106
|
-
isFragment: false,
|
|
107
|
-
isGhost: false,
|
|
108
|
-
};
|
|
109
|
-
}
|
|
110
|
-
case 'Text': {
|
|
111
|
-
if (parentNode && /^script$|^style$/i.test(parentNode.nodeName)) {
|
|
112
|
-
return {
|
|
113
|
-
uuid: uuid(),
|
|
114
|
-
raw: originNode.raw,
|
|
115
|
-
startOffset,
|
|
116
|
-
endOffset,
|
|
117
|
-
startLine,
|
|
118
|
-
endLine,
|
|
119
|
-
startCol,
|
|
120
|
-
endCol,
|
|
121
|
-
nodeName: '#text',
|
|
122
|
-
type: 'text',
|
|
123
|
-
parentNode,
|
|
124
|
-
prevNode,
|
|
125
|
-
nextNode,
|
|
126
|
-
isFragment: false,
|
|
127
|
-
isGhost: false,
|
|
128
|
-
};
|
|
129
|
-
}
|
|
130
|
-
const htmlDoc = htmlParse(originNode.raw, {
|
|
131
|
-
offsetOffset: originNode.offset,
|
|
132
|
-
offsetLine: originNode.line - 1,
|
|
133
|
-
offsetColumn: originNode.column - 1,
|
|
134
|
-
});
|
|
135
|
-
const nodes = htmlDoc.nodeList.filter(node => {
|
|
136
|
-
return node.parentNode == null && node.type !== 'endtag';
|
|
137
|
-
});
|
|
138
|
-
return nodes;
|
|
139
|
-
}
|
|
140
|
-
case 'Comment': {
|
|
141
|
-
return {
|
|
142
|
-
uuid: uuid(),
|
|
143
|
-
raw: originNode.raw,
|
|
144
|
-
startOffset,
|
|
145
|
-
endOffset,
|
|
146
|
-
startLine,
|
|
147
|
-
endLine,
|
|
148
|
-
startCol,
|
|
149
|
-
endCol,
|
|
150
|
-
nodeName: '#comment',
|
|
151
|
-
type: 'comment',
|
|
152
|
-
parentNode,
|
|
153
|
-
prevNode,
|
|
154
|
-
nextNode,
|
|
155
|
-
isFragment: false,
|
|
156
|
-
isGhost: false,
|
|
157
|
-
};
|
|
158
|
-
}
|
|
159
|
-
case 'Tag': {
|
|
160
|
-
const namespace = getNamespace(originNode.name, parentNamespace);
|
|
161
|
-
const tag = {
|
|
162
|
-
uuid: uuid(),
|
|
163
|
-
raw: originNode.raw,
|
|
164
|
-
startOffset,
|
|
165
|
-
endOffset,
|
|
166
|
-
startLine,
|
|
167
|
-
endLine,
|
|
168
|
-
startCol,
|
|
169
|
-
endCol,
|
|
170
|
-
nodeName: originNode.name,
|
|
171
|
-
type: 'starttag',
|
|
172
|
-
namespace,
|
|
173
|
-
elementType: detectElementType(originNode.name),
|
|
174
|
-
attributes: originNode.attrs.map(attr => attrTokenizer(attr)),
|
|
175
|
-
hasSpreadAttr: false,
|
|
176
|
-
parentNode,
|
|
177
|
-
prevNode,
|
|
178
|
-
nextNode,
|
|
179
|
-
pearNode: null,
|
|
180
|
-
selfClosingSolidus: tokenizer('', originNode.line, originNode.column, originNode.offset),
|
|
181
|
-
endSpace: tokenizer('', originNode.line, originNode.column, originNode.offset),
|
|
182
|
-
isFragment: false,
|
|
183
|
-
isGhost: false,
|
|
184
|
-
tagOpenChar: '',
|
|
185
|
-
tagCloseChar: '',
|
|
186
|
-
};
|
|
187
|
-
if (originNode.block.nodes.length > 0) {
|
|
188
|
-
tag.childNodes = this.traverse(originNode.block.nodes, tag);
|
|
189
|
-
}
|
|
190
|
-
return tag;
|
|
191
|
-
}
|
|
192
|
-
default: {
|
|
193
|
-
const tag = {
|
|
194
|
-
uuid: uuid(),
|
|
195
|
-
raw: originNode.raw,
|
|
196
|
-
startOffset,
|
|
197
|
-
endOffset,
|
|
198
|
-
startLine,
|
|
199
|
-
endLine,
|
|
200
|
-
startCol,
|
|
201
|
-
endCol,
|
|
202
|
-
type: 'psblock',
|
|
203
|
-
nodeName: originNode.type,
|
|
204
|
-
parentNode,
|
|
205
|
-
prevNode,
|
|
206
|
-
nextNode,
|
|
207
|
-
isFragment: true,
|
|
208
|
-
isGhost: false,
|
|
209
|
-
};
|
|
210
|
-
if ('block' in originNode && originNode.block && originNode.block.nodes.length > 0) {
|
|
211
|
-
tag.childNodes = this.traverse(originNode.block.nodes, tag);
|
|
212
|
-
}
|
|
213
|
-
return tag;
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
traverse(
|
|
218
|
-
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
219
|
-
astNodes, parentNode = null) {
|
|
220
|
-
const nodeList = [];
|
|
221
|
-
let prevNode = null;
|
|
222
|
-
for (const astNode of astNodes) {
|
|
223
|
-
const nodes = this.nodeize(astNode, prevNode, parentNode);
|
|
224
|
-
if (!nodes || (Array.isArray(nodes) && nodes.length === 0)) {
|
|
225
|
-
continue;
|
|
226
|
-
}
|
|
227
|
-
let node;
|
|
228
|
-
if (Array.isArray(nodes)) {
|
|
229
|
-
const lastNode = nodes.at(-1);
|
|
230
|
-
if (!lastNode) {
|
|
231
|
-
continue;
|
|
232
|
-
}
|
|
233
|
-
node = lastNode;
|
|
234
|
-
}
|
|
235
|
-
else {
|
|
236
|
-
node = nodes;
|
|
237
|
-
}
|
|
238
|
-
if (prevNode) {
|
|
239
|
-
if (node.type !== 'endtag') {
|
|
240
|
-
prevNode.nextNode = node;
|
|
241
|
-
}
|
|
242
|
-
node.prevNode = prevNode;
|
|
243
|
-
}
|
|
244
|
-
prevNode = node;
|
|
245
|
-
if (Array.isArray(nodes)) {
|
|
246
|
-
nodeList.push(...nodes);
|
|
247
|
-
}
|
|
248
|
-
else {
|
|
249
|
-
nodeList.push(nodes);
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
return nodeList;
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
_Parser_ast = new WeakMap();
|