This repository has been archived by the owner on Dec 12, 2023. It is now read-only.
-
-
Notifications
You must be signed in to change notification settings - Fork 0
/
tabStops.js
368 lines (320 loc) · 10.7 KB
/
tabStops.js
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
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
/**
* Utility module for handling tabstops tokens generated by Emmet's
* "Expand Abbreviation" action. The main <code>extract</code> method will take
* raw text (for example: <i>${0} some ${1:text}</i>), find all tabstops
* occurrences, replace them with tokens suitable for your editor of choice and
* return object with processed text and list of found tabstops and their ranges.
* For sake of portability (Objective-C/Java) the tabstops list is a plain
* sorted array with plain objects.
*
* Placeholders with the same are meant to be <i>linked</i> in your editor.
*/
if (typeof module === 'object' && typeof define !== 'function') {
var define = function (factory) {
module.exports = factory(require, exports, module);
};
}
define(function(require, exports, module) {
var utils = require('../utils/common');
var stringStream = require('./stringStream');
var resources = require('./resources');
/**
* Global placeholder value, automatically incremented by
* <code>variablesResolver()</code> function
*/
var startPlaceholderNum = 100;
var tabstopIndex = 0;
var defaultOptions = {
replaceCarets: false,
escape: function(ch) {
return '\\' + ch;
},
tabstop: function(data) {
return data.token;
},
variable: function(data) {
return data.token;
}
};
return {
/**
* Main function that looks for a tabstops in provided <code>text</code>
* and returns a processed version of <code>text</code> with expanded
* placeholders and list of tabstops found.
* @param {String} text Text to process
* @param {Object} options List of processor options:<br>
*
* <b>replaceCarets</b> : <code>Boolean</code> — replace all default
* caret placeholders (like <i>{%::emmet-caret::%}</i>) with <i>${0:caret}</i><br>
*
* <b>escape</b> : <code>Function</code> — function that handle escaped
* characters (mostly '$'). By default, it returns the character itself
* to be displayed as is in output, but sometimes you will use
* <code>extract</code> method as intermediate solution for further
* processing and want to keep character escaped. Thus, you should override
* <code>escape</code> method to return escaped symbol (e.g. '\\$')<br>
*
* <b>tabstop</b> : <code>Function</code> – a tabstop handler. Receives
* a single argument – an object describing token: its position, number
* group, placeholder and token itself. Should return a replacement
* string that will appear in final output
*
* <b>variable</b> : <code>Function</code> – variable handler. Receives
* a single argument – an object describing token: its position, name
* and original token itself. Should return a replacement
* string that will appear in final output
*
* @returns {Object} Object with processed <code>text</code> property
* and array of <code>tabstops</code> found
* @memberOf tabStops
*/
extract: function(text, options) {
// prepare defaults
var placeholders = {carets: ''};
var marks = [];
options = utils.extend({}, defaultOptions, options, {
tabstop: function(data) {
var token = data.token;
var ret = '';
if (data.placeholder == 'cursor') {
marks.push({
start: data.start,
end: data.start + token.length,
group: 'carets',
value: ''
});
} else {
// unify placeholder value for single group
if ('placeholder' in data)
placeholders[data.group] = data.placeholder;
if (data.group in placeholders)
ret = placeholders[data.group];
marks.push({
start: data.start,
end: data.start + token.length,
group: data.group,
value: ret
});
}
return token;
}
});
if (options.replaceCarets) {
text = text.replace(new RegExp( utils.escapeForRegexp( utils.getCaretPlaceholder() ), 'g'), '${0:cursor}');
}
// locate tabstops and unify group's placeholders
text = this.processText(text, options);
// now, replace all tabstops with placeholders
var buf = '', lastIx = 0;
var tabStops = marks.map(function(mark) {
buf += text.substring(lastIx, mark.start);
var pos = buf.length;
var ph = placeholders[mark.group] || '';
buf += ph;
lastIx = mark.end;
return {
group: mark.group,
start: pos,
end: pos + ph.length
};
});
buf += text.substring(lastIx);
return {
text: buf,
tabstops: tabStops.sort(function(a, b) {
return a.start - b.start;
})
};
},
/**
* Text processing routine. Locates escaped characters and tabstops and
* replaces them with values returned by handlers defined in
* <code>options</code>
* @param {String} text
* @param {Object} options See <code>extract</code> method options
* description
* @returns {String}
*/
processText: function(text, options) {
options = utils.extend({}, defaultOptions, options);
var buf = '';
/** @type StringStream */
var stream = stringStream.create(text);
var ch, m, a;
while ((ch = stream.next())) {
if (ch == '\\' && !stream.eol()) {
// handle escaped character
buf += options.escape(stream.next());
continue;
}
a = ch;
if (ch == '$') {
// looks like a tabstop
stream.start = stream.pos - 1;
if ((m = stream.match(/^[0-9]+/))) {
// it's $N
a = options.tabstop({
start: buf.length,
group: stream.current().substr(1),
token: stream.current()
});
} else if ((m = stream.match(/^\{([a-z_\-][\w\-]*)\}/))) {
// ${variable}
a = options.variable({
start: buf.length,
name: m[1],
token: stream.current()
});
} else if ((m = stream.match(/^\{([0-9]+)(:.+?)?\}/, false))) {
// ${N:value} or ${N} placeholder
// parse placeholder, including nested ones
stream.skipToPair('{', '}');
var obj = {
start: buf.length,
group: m[1],
token: stream.current()
};
var placeholder = obj.token.substring(obj.group.length + 2, obj.token.length - 1);
if (placeholder) {
obj.placeholder = placeholder.substr(1);
}
a = options.tabstop(obj);
}
}
buf += a;
}
return buf;
},
/**
* Upgrades tabstops in output node in order to prevent naming conflicts
* @param {AbbreviationNode} node
* @param {Number} offset Tab index offset
* @returns {Number} Maximum tabstop index in element
*/
upgrade: function(node, offset) {
var maxNum = 0;
var options = {
tabstop: function(data) {
var group = parseInt(data.group, 10);
if (group > maxNum) maxNum = group;
if (data.placeholder)
return '${' + (group + offset) + ':' + data.placeholder + '}';
else
return '${' + (group + offset) + '}';
}
};
['start', 'end', 'content'].forEach(function(p) {
node[p] = this.processText(node[p], options);
}, this);
return maxNum;
},
/**
* Helper function that produces a callback function for
* <code>replaceVariables()</code> method from {@link utils}
* module. This callback will replace variable definitions (like
* ${var_name}) with their value defined in <i>resource</i> module,
* or outputs tabstop with variable name otherwise.
* @param {AbbreviationNode} node Context node
* @returns {Function}
*/
variablesResolver: function(node) {
var placeholderMemo = {};
return function(str, varName) {
// do not mark `child` variable as placeholder – it‘s a reserved
// variable name
if (varName == 'child') {
return str;
}
if (varName == 'cursor') {
return utils.getCaretPlaceholder();
}
var attr = node.attribute(varName);
if (typeof attr !== 'undefined' && attr !== str) {
return attr;
}
var varValue = resources.getVariable(varName);
if (varValue) {
return varValue;
}
// output as placeholder
if (!placeholderMemo[varName]) {
placeholderMemo[varName] = startPlaceholderNum++;
}
return '${' + placeholderMemo[varName] + ':' + varName + '}';
};
},
/**
* Replace variables like ${var} in string
* @param {String} str
* @param {Object} vars Variable set (defaults to variables defined in
* <code>snippets.json</code>) or variable resolver (<code>Function</code>)
* @return {String}
*/
replaceVariables: function(str, vars) {
vars = vars || {};
var resolver = typeof vars === 'function' ? vars : function(str, p1) {
return p1 in vars ? vars[p1] : null;
};
return this.processText(str, {
variable: function(data) {
var newValue = resolver(data.token, data.name, data);
if (newValue === null) {
// try to find variable in resources
newValue = resources.getVariable(data.name);
}
if (newValue === null || typeof newValue === 'undefined')
// nothing found, return token itself
newValue = data.token;
return newValue;
}
});
},
/**
* Resets global tabstop index. When parsed tree is converted to output
* string (<code>AbbreviationNode.toString()</code>), all tabstops
* defined in snippets and elements are upgraded in order to prevent
* naming conflicts of nested. For example, <code>${1}</code> of a node
* should not be linked with the same placehilder of the child node.
* By default, <code>AbbreviationNode.toString()</code> automatically
* upgrades tabstops of the same index for each node and writes maximum
* tabstop index into the <code>tabstopIndex</code> variable. To keep
* this variable at reasonable value, it is recommended to call
* <code>resetTabstopIndex()</code> method each time you expand variable
* @returns
*/
resetTabstopIndex: function() {
tabstopIndex = 0;
startPlaceholderNum = 100;
},
/**
* Output processor for abbreviation parser that will upgrade tabstops
* of parsed node in order to prevent tabstop index conflicts
*/
abbrOutputProcessor: function(text, node, type) {
var maxNum = 0;
var that = this;
var tsOptions = {
tabstop: function(data) {
var group = parseInt(data.group, 10);
if (group === 0)
return '${0}';
if (group > maxNum) maxNum = group;
if (data.placeholder) {
// respect nested placeholders
var ix = group + tabstopIndex;
var placeholder = that.processText(data.placeholder, tsOptions);
return '${' + ix + ':' + placeholder + '}';
} else {
return '${' + (group + tabstopIndex) + '}';
}
}
};
// upgrade tabstops
text = this.processText(text, tsOptions);
// resolve variables
text = this.replaceVariables(text, this.variablesResolver(node));
tabstopIndex += maxNum + 1;
return text;
}
};
});