forked from pugjs/pug
-
Notifications
You must be signed in to change notification settings - Fork 6
/
index.js
303 lines (282 loc) · 9.93 KB
/
index.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
var jade = require('jade');
var coffee = require('./coffee');
var crypto = require('crypto');
var path = require('path');
var fs = require('fs');
var os = require('os');
//process.env.DEBUG = 'derby-jade';
var debug = require('debug')('derby-jade');
var options = {};
var defaultIndent = 2;
//process.platform = 'win32';
var newLine = '\n';
var regNewLine = '\\n';
function r(pattern, modifiers) {
return new RegExp(pattern, modifiers);
}
module.exports = exports = function (app, opts) {
options = opts || {};
app.viewExtensions.push('.jade');
app.compilers['.jade'] = compiler;
};
exports.compiler = compiler;
function addindent(source, count) {
if (count === undefined) count = defaultIndent;
var indentation = '';
for (var i = 0; i < count; i++) {
indentation += ' ';
}
return indentation + source;
}
function preprocess(source) {
return source
// Replace if, else, each, etc statements to __derby-statement(type="if", value="expression")
// we cheat Jade, because it has it`s own statements
.replace(/^([ \t]+)(if|else(?:[ \t]+if)?|unless|each|with|bound|unbound|on)((?:[ \t]|\().+)?$/gm,
function (statement, indentation, type, expression) {
if (options.coffee) {
expression = ' ' + coffee(expression);
}
return indentation + '__derby-statement(type=\"' + type + '\"' +
(expression ? ' value=\"' + escape(expression) + '\"' : '') + ')';
})
// This is needed for coffee
// find all statements in {{..}}
.replace(/{{([^\/].*?)}}/g, function(statement, expression) {
var block = '';
if (blockCaptures = /^((?:unescaped|if|else if|unless|each|with|bound|unbound|on)\*?) *([^\n]*)/.exec(expression)) {
block = blockCaptures[1] + ' ';
expression = blockCaptures[2];
} else if (expression === 'else') {
block = expression;
expression = '';
}
if (options.coffee) expression = coffee(expression);
return '{{' + block + expression + '}}';
})
// Make Derby attribues unescaped
.replace(/on-(.*?)=(['"])(.*?)\2/gm, function(statement, type, quote, expression) {
if (options.coffee) expression = coffee(expression);
return 'on-' + type + '!=\"' + expression + '\"';
});
}
function postprocess(html, scripts) {
return html
.replace(/\n/g, newLine)
.replace(r('\\s*(<([\\w-:]+))((?:\\b[^>]+)?>)(?:' + regNewLine + ')?([\\s\\S]*?)(?:' + regNewLine + ')?<\\/\\2>', 'g'), function (template, left, name, right, content, offset, string) {
return left + ':' + right + (content ? newLine + content : '')
+ ((offset + template.length === string.length) ? '' : newLine);
})
// Remove underscores
.replace(/<_derby_/g, '<')
.replace(/<\/_derby_/g, '<\/')
// Add scripts
.replace(/<script(\d*)><\/script\1>/g, function(statement, index) {
return scripts[index];
})
.replace(/\r$/g, '')
// Clean redundant Derby statements
//.replace(/[ \t]*<\/__derby-statement>\n?(?=\s+<__derby-statement type="else([ \t]+if)?")/g, '')
.replace(r('[ \\t]*<\\/__derby-statement>' + regNewLine + '?(?=\\s+<__derby-statement type="else([ \\t]+if)?")', 'g'), '')
// Replace Derby statements back
.replace(/<__derby-statement type="([^"]+)"(?: value="([^"]+)")?>/gm, function (statement, type, value) {
if (value === '%20') value = '';
return '{{' + type + (value ? unescape(value) : '') + '}}';
})
// Closing Derby statements
.replace(/<\/__derby-statement>/g, '{{/}}');
}
function compiler(file, fileName, preprocessOnly, jadeOptions) {
var out = [];
var lines = file.replace(/\r\n/g, newLine).split(newLine);
var lastComment = Infinity;
var lastScript = Infinity;
var lastElement = null;
var script = [];
var scripts = [];
var block = [];
var debugString;
jadeOptions = jadeOptions || options.globals || {};
function renderBlock() {
if (block.length) {
debugString += ', block end';
var source = preprocess(block.join(newLine));
block = [];
jadeOptions.filename = fileName;
jadeOptions.pretty = true;
jade.render(source, jadeOptions, function (error, html) {
if (error) throw error;
out.push(postprocess(html, scripts));
});
}
}
function renderPreprocessBlock() {
if (block.length) {
debugString += ', block end';
var source = preprocess(block.join(newLine));
block = [];
out.push(source);
}
}
function closeScript() {
if (script.length) {
var source = script.join(newLine);
if (options.coffee) source = coffee(source);
script = [];
var scriptSource = '<script>';
source.split(newLine).forEach(function (scriptLine) {
scriptLine = scriptLine.replace(/^\s*/g, '');
scriptSource += newLine + addindent(scriptLine, lastScript + defaultIndent);
});
scriptSource += newLine + addindent('</script>', lastScript);
scripts.push(scriptSource);
block.push(addindent('script' + (scripts.length - 1), lastScript));
}
}
for (var i = 0; i < lines.length; i++) {
var line = lines[i];
var oldLine;
var extendMatch, extendFileName, extendFile, extendTempFileName;
var res = /^(\s*)(.*?)$/g.exec(line);
var spaces = res[1];
var statement = res[2];
var indent = spaces.length;
debugString = addindent(statement, indent) + ' | ' + indent;
// Comment
if (lastComment !== Infinity) {
if (indent > lastComment) {
debug(debugString + ', comment');
continue;
} else {
debugString += ', comment end';
lastComment = Infinity;
}
}
if (statement.indexOf('//') === 0) {
lastComment = indent;
debug(debugString + ', comment start');
continue;
}
// Script
if (lastScript !== Infinity) {
if (indent > lastScript) {
script.push(addindent(statement, indent));
debug(debugString + ', script');
continue;
} else {
debugString += ', script end';
closeScript();
lastScript = Infinity;
}
}
if (statement.indexOf('script.') === 0) {
// Script block
lastScript = indent;
debug(debugString + ', script.start');
continue;
}
if (statement.indexOf('script ') === 0) {
// Script line
if (options.coffee) statement = 'script ' + coffee(statement.slice(7));
block.push(addindent(statement, indent));
debug(debugString + ', script line');
continue;
}
// Empty line
if (!statement.length) {
block.push('');
debug(debugString + ', empty');
continue;
}
// Jade's "extends" and "include"
// We have to compile the source file into a temporary one
if ((indent === 0) && (
extendMatch = statement.match(/^(extends|include) (\S+)/))) {
extendFileName = path.resolve(path.dirname(fileName), extendMatch[2]);
extendFileName = extendFileName.replace(/\.jade\s*$/, '') + '.jade';
extendFile = fs.readFileSync(extendFileName, { encoding: 'utf8' });
extendFile = compiler(extendFile, extendFileName, true, jadeOptions);
extendTempFileName = path.join( os.tmpdir(),
crypto.createHash('md5').update(extendFileName).digest('hex')+ '.jade');
fs.writeFileSync(extendTempFileName, extendFile);
block.push( extendMatch[1] + ' '
+ path.relative(fileName, extendTempFileName) );
debug(debugString + ', jade extends');
continue;
}
// Other Jade reserved keywords
// Simply pass any preprocessing of them
if (indent === 0 &&
/^(\+|mixin|block|prepend|append)/.test(statement)) {
block.push(line);
debug(debugString + ', jade reserved');
continue;
}
// BEM-elements
if (indent === 0) {
lastElement = statement.match(/[ ,\(]bem=['"]([^'"]*)['"]/);
lastElement = lastElement || statement.match(/[ ,\(]element=['"]([^'"]*)['"]/);
lastElement && (lastElement = lastElement[1])
}
if (indent === 0) {
// Derby tag
// It means that we are going to start another block,
// so we should render last one first
if (preprocessOnly) {
renderPreprocessBlock();
} else {
renderBlock();
}
// Remove colons after Derby tags
// it makes colons optional
statement = statement.replace(/:([\n\s(])/, function(statement, symbol) {
return symbol;
});
statement = statement.replace(/:$/, '');
// We add underscore to avoid problems when Derby tag name
// is same as non closing tags
statement = '_derby_' + statement;
debugString += ', block start';
block.push(statement);
} else {
debugString += ', block';
// BEM replacement
if (lastElement) {
do {
oldLine = line;
line = line.replace(/(^\s*[\w\.#-]*\.)(&)/, '$1' + lastElement);
} while (line !== oldLine);
}
// Module mode for Jade (for usage with Webpack and custom css-loader)
// Ref: https://github.com/dmapper/style-guide/blob/master/stylus.md
if (( jadeOptions.moduleMode || options.moduleMode) && fileName) {
var _componentName = path.basename(fileName, path.extname(fileName));
if (_componentName === 'index') {
_componentName = path.basename( path.dirname(fileName) );
}
line = line.replace(/(\.)([a-z][\w_-]+)/g, function(match, dot, localName, offset, string){
// Check that it's a class (the dot is before any parenthesis)
if (/^\s*[\w\.#-]*$/.test( string.substr(0, offset) )) {
if (localName === 'root') {
return dot + _componentName;
} else {
return dot + _componentName + '-' + localName;
}
} else {
return match
}
});
}
block.push(line);
}
debug(debugString);
}
// Close script if exist and render block
closeScript();
if (preprocessOnly) {
renderPreprocessBlock();
} else {
renderBlock();
}
return out.join(newLine);
}