-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathemmet-compress.ts
225 lines (217 loc) · 7.25 KB
/
emmet-compress.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
export interface IParser {
write(str: string): void;
end(): void;
}
export interface IParserConstructor {
new (config: Walker): IParser;
}
let parserP: undefined | Promise<IParserConstructor>;
const defParser = () => {
if (parserP) {
return parserP;
}
parserP = import("htmlparser2").then(({ Parser }) => Parser).catch(() => class ParserFromDOM implements IParser {
private _config: Walker;
private _buf: string;
public constructor(config: Walker) {
this._buf = "";
this._config = config;
}
public write(str: string) {
this._buf += str;
}
public end() {
const parser = new DOMParser();
const doc = parser.parseFromString(this._buf, "text/xml");
const errors = doc.getElementsByTagName("parsererror");
if (errors.length != 0) {
const err = Object.assign(new Error("XML Parse Error"), { errors });
throw err;
}
const f = (children: [Node] | NodeListOf<ChildNode>) => {
for (const child of children) {
if (child.nodeType === 3 || child.nodeType === 4) {
const t = child.nodeValue;
t && this._config.ontext(t);
}
if (child.nodeType == 1) {
const elem = child as Element;
const name = elem.nodeName;
const attrs: Record<string, string> = {};
for (const attr of elem.attributes) {
attrs[attr.nodeName] = attr.nodeValue || "";
}
this._config.onopentag(name, attrs);
f(elem.childNodes);
this._config.onclosetag(name);
}
}
};
f([doc.documentElement]);
}
});
// eslint-disable-next-line @typescript-eslint/no-empty-function
parserP.catch(() => {});
return parserP;
};
class Walker {
private _agg: string;
private _text: string;
private _hasChild: boolean;
private _deep: number;
private _up: number;
constructor() {
this._agg = "";
this._text = "";
this._hasChild = false;
this._deep = 0;
this._up = 0;
}
/**
* This fires when a new tag is opened.
*
* If you don't need an aggregated `attributes` object,
* have a look at the `onopentagname` and `onattribute` events.
*/
public onopentag(name: string, attributes: Record<string, string>) {
console.log("onopentag", name, attributes);
if (this._text.trim()) {
throw new Error(`Cannot have mixed text nodes and element nodes: ${JSON.stringify(this._text)}`);
}
if (this._deep && !this._up) {
this._agg += ">";
} else if (this._up == 1) {
this._agg += "+";
this._up = 0;
} else {
while (this._up != 0) {
this._agg += "^";
this._up -= 1;
}
}
this._agg += name;
const attrs = Object.keys(attributes || {});
if (attrs.length) {
this._agg += `[${attrs.map((k) => {
const v = attributes[k];
if (v === "") {
return k;
}
return `${k}=${JSON.stringify(v)}`;
}).join(" ")}]`;
}
this._text = "";
this._hasChild = false;
this._up = 0;
this._deep += 1;
}
/**
* Fires whenever a section of text was processed.
*
* Note that this can fire at any point within text and you might
* have to stitch together multiple pieces.
*/
public ontext(text: string) {
if (this._hasChild) {
if (text.trim()) {
throw new Error(`Cannot have mixed text nodes and element nodes: ${JSON.stringify(this._text + text)}`);
}
return;
}
this._text += text;
if (text.indexOf("}") != -1) {
throw new Error(`Cannot have the character \`}\` in text: ${JSON.stringify(this._text)}`);
}
this._up = 0;
}
/**
* Fires when a tag is closed.
*
* You can rely on this event only firing when you have received an
* equivalent opening tag before. Closing tags without corresponding
* opening tags will be ignored.
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
public onclosetag(_name: string) {
if (!this._hasChild && this._text) {
this._agg += `{${this._text}}`;
}
this._text = "";
this._hasChild = true;
this._up += 1;
this._deep -= 1;
}
public toString() {
return this._agg;
}
}
/**
* Compresses HTML into an Emmet abbreviation.
*
* To use this class, you must do one of the following:
* - pass in a parser to use in the constructor
* - have `htmlparser2` ready to import
* - have `DOMParser` defined in the global scope
*/
export default class EmmetCompress {
private _walker?: Walker;
private _parser?: IParser;
private _parseCons: IParserConstructor | Promise<IParserConstructor>;
/**
* Creates a new compression context for Emmet.
*
* @param {IParserConstructor | Promise<IParserConstructor>} parser when set, this will be the parser used to parse the HTML. If not set, the default parser will be used.
*/
public constructor(parser?: IParserConstructor | Promise<IParserConstructor>) {
this._parseCons = parser || defParser();
}
/**
* Write a chunk of XML data to the parser.
*
* @param {string} source a chunk of XML to parse
* @returns {void | Promise<void>} if the parser was not ready, this will return a promise that resolves when the parser has been written to
*/
public write(source: string): void | Promise<void> {
if (!source) {
return;
}
if (this._parser) {
this._parser.write(source);
} else if (this._parseCons instanceof Promise) {
return this._parseCons.then((ParserCons: IParserConstructor) => {
if (!this._parser) {
this._parseCons = ParserCons;
this._walker = new Walker();
this._parser = new ParserCons(this._walker);
}
this._parser.write(source);
});
} else {
this._walker = new Walker();
this._parser = new this._parseCons(this._walker);
this._parser.write(source);
}
}
/**
*
* @returns {string} the compressed Emmet abbreviation
*/
public end(): string {
if (!this._parser) {
return "";
}
this._parser.end();
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const ret = this._walker!.toString();
this._parser = undefined;
this._walker = undefined;
return ret;
}
/**
*
* @returns {string} the current serialized state of the Emmet abbreviation
*/
public toString() {
return this._walker ? this._walker.toString() : "";
}
}