-
Notifications
You must be signed in to change notification settings - Fork 36
Expand file tree
/
Copy pathwriteCapture2.js
More file actions
422 lines (385 loc) · 10.8 KB
/
writeCapture2.js
File metadata and controls
422 lines (385 loc) · 10.8 KB
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
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
/**
* Totaly rewritten writeCapture, based on element.write.
*
* Usage:
*
* writeCapture(targetElement).
* write(someHtml).
* close(); // must be sure to close()
*
* Script tags in anything written will automatically be captured, such
* that document.write is redirected into the element.
*/
(function(define){
"use strict";
/*global document window setTimeout */
var console = window.console || {log:function(){}};
var log = console.log;
console.log = function() {
if(writerFor.debug) {
log.apply(this,arguments);
}
};
// feature test for how we create script tags
var scriptEval = (function() {
var script = document.createElement("script");
var id = "script" + (new Date()).getTime();
var root = document.documentElement;
script.type = "text/javascript";
try {
script.appendChild( document.createTextNode( "window." + id + "=1;" ) );
} catch(e){}
root.insertBefore( script, root.firstChild );
// Make sure that the execution of code works by injecting a script
// tag with appendChild/createTextNode
// (IE doesn't support this, fails, and uses .text instead)
if ( window[ id ] ) {
delete window[ id ];
return true;
}
return false;
})();
// will be set by define()
var elementWrite;
/**
* This is the public one. Creates a writer for element.
* onDone is called when all writing finishes.
*
* Depends on elementWrite, which gets set by the define call
* at the end.
*/
function writerFor(element,onDone) {
var writer, capturing, script, html,
// XXX IE will throw security errors if we try to manipulate an object or embed
// this falg tells us that no DOM manipulation is allowed, so we will buffer
// and use innerHTML instead
noDOM = 0,
// XXX some stupid scripts will write noscript tags, which <IE9 doesn't like
// so we have to ignore them
noscript,
// XXX a written iframe may have whitespace in it, which we have to ignore
// since we can't actually access the inside of an iframe via the DOM
// writing to iframes is done through its own document.write, which is not
// captured
iframe;
onDone = onDone || function() {};
// add a set of listeners that will handle script tags and
// capture document.write
writer = elementWrite.toElement(element,pausable({
start: function(tag,attrs,unary,state) {
// XXX ignore noscript content since it should not load
if(noscript) return false;
if(tag.toLowerCase() === 'script') {
console.log('WC element:',element,
'start script. attrs:',attrs,this.id);
script = '';
capturing = attrs || {};
return false;
}
// if we encounter an object or embed tag, buffer the HTML
if(noDOM || /^(object|embed)$/i.test(tag)) {
html = (noDOM ? html : '') + '<'+tag;
for ( var i = 0; i < attrs.length; i++ )
html += " " + attrs[i].name + '="' + attrs[i].escaped + '"';
html += ">";
// if it was an unwrapped bodyless tag, we're done
if(unary && !noDOM) {
state.stack.last().innerHTML += html;
} else if(!unary) {
// otherwise, count the open tags
noDOM++;
}
return false;
}
if(tag.toLowerCase() === 'noscript') {
noscript = true;
return false;
}
if(tag.toLowerCase() === 'iframe') {
iframe = true;
}
},
chars: function(text,state) {
if(noDOM) {
html += text;
return false;
}
// XXX ignore noscript tags since they have no effect
if(noscript) return false;
// XXX we can't write characters inside an iframe, so ignore them (probably just whitespace)
if(iframe) return false;
if(capturing) {
console.log('WC element:',element,'chars:',text,this.id);
script += text;
return false;
}
},
end: function(tag,state) {
// might be buffering HTML
if(noDOM) {
html += "</" + tag + ">";
// if this is the last tag we can't manipulate, append it to the parent
if(!--noDOM) {
state.stack.last().innerHTML += html;
}
return false;
}
// if we're inside a noscript tag, ignore everything until we
// hit a closing noscript tag
if(noscript) {
noscript = tag.toLowerCase() !== 'noscript';
return false;
}
// clear the iframe flag once we're out of it
if(iframe) {
iframe = tag.toLowerCase() !== 'iframe';
return false;
}
if(capturing) {
var attrs = capturing;
capturing = false;
captureScript(script,attrs,state.stack.last(),writer);
return false;
}
},
comment: function(text,state) {
return false;
},
// special pausable handler - fires when queue is exhausted
done: onDone
}));
return writer;
}
/**
* Creates a script tag from script (the code) & attrs and appends
* it to parent. The writer is paused while the script loads and
* runs, then resumed after it has finished.
*/
function captureScript(script,attrs,parent,writer) {
// Create a new writer for the script to use so we
// can pause the current writer.
var newWriter = writerFor(parent,doResume), restore;
console.log('WC captureScript attrs:',attrs,'body:',script,
'in parent:',parent);
// if the script is in fact an inline script, it should
// finish before any other writer actions queue, so it will
// be like we never paused
writer.handle('pause');
// We have to let any current script finish (and queue up writes)
// before we can redirect and run the next script.
// This could be a problem for scripts that expect the new script
// tag to block script execution (impossible for us)
setTimeout(function() {
restore = redirect(getDoc(parent),newWriter);
// when the script has loaded, queue up the close/done
// (script may have paused its writer)
exec(script,attrs,parent,function() {
newWriter.close();
});
},25);
// when the writter is closed and fully written out,
// restore doc.write and resume writing from this
// writer's queue
function doResume() {
restore();
writer.handle('resume');
}
}
/**
* Executes scripts by creating a script tag and inserting it into
* parent.
* @param script the script body (code).
* @param attrs the script attributes. May include src.
* @param parent the element to append the script to.
* @param cb optional callback to call when script is loaded.
*/
function exec(script,attrs,parent,cb) {
var doc = getDoc(parent),
el = doc.createElement('script'),
name, value;
for ( var i = 0; i < attrs.length; i++ ) {
var attr = attrs[i];
name = attr.name;
value = attr.value;
if(writerFor.fixUrls && name === 'src') {
value = writerFor.fixUrls(value);
}
el.setAttribute( name, value );
}
if(script) {
if ( scriptEval ) {
el.appendChild( doc.createTextNode( script ) );
} else {
el.text = script;
}
}
if(cb && el.src) {
el.onload = el.onreadystatechange = function( _, isAbort ) {
if ( isAbort || !el.readyState || /loaded|complete/.test( el.readyState ) ) {
// Handle memory leak in IE
el.onload = el.onreadystatechange = null;
// Dereference the script
el = undefined;
// Callback if not abort
if ( !isAbort ) {
cb();
}
}
};
}
parent.appendChild(el);
// if it was an inline script, it's done now
if(cb && !el.src) {
cb();
}
}
/**
* Redirects the document's document.write/writeln
* to writer.
*
* @return a function to restore the original functions.
*/
function redirect(document,writer) {
var original = {
write: document.write,
writeln: document.writeln
};
document.write = function(s) {
// if the writer is paused, this queues the write
writer.handle('write',[s]);
};
document.writeln = function(s) {
document.write(s+'\n');
};
return function() {
document.write = original.write;
document.writeln = original.writeln;
};
}
var ids = 0;
// adds pause and resume methods to listeners and returns a new set of
// listeners that will queue their events when paused
function pausable(listeners) {
var queue = [], paused, id = ids++;
return {
pause: function() {
console.log('WC PAUSE',id);
paused = true;
},
resume: function() {
console.log('WC RESUME',id,queue.slice(0));
paused = false;
while(!paused && queue.length) {
var next = queue.shift();
this.writer.handle(next[0],next[1]);
}
},
start: function(tag,attrs,unary,state) {
console.log('WC start',paused,'args',tag,attrs,unary,state,id);
if(paused) {
queue.push(['start',[tag,attrs,unary]]);
return false;
} else {
return listeners.start(tag,attrs,unary,state);
}
},
chars: function(text,state) {
console.log('WC chars',paused,'args',text,state,id);
if(paused) {
queue.push(['chars',[text]]);
return false;
} else {
return listeners.chars(text,state);
}
},
end: function(tag,state) {
console.log('WC end',paused,'args',tag,state,id);
if(paused) {
queue.push(['end',[tag]]);
return false;
} else {
return listeners.end(tag,state);
}
},
comment: function(text,state) {
if(paused) {
queue.push(['comment',[text]]);
return false;
} else {
return listeners.comment(text,state);
}
},
write: function(s) {
console.log('WC queue.write',paused,id);
if(paused) {
queue.push(['write',[s]]);
return false;
} else {
this.writer.write(s);
return false;
}
},
close: function() {
console.log('WC close',paused,id);
if(paused) {
queue.push(['close',[]]);
return false;
} else if(listeners.done) {
return listeners.done();
}
}
};
}
function getDoc(element) {
return element.ownerDocument ||
element.getOwnerDocument && element.getOwnerDocument();
}
function object(obj) {
function F() {}
F.prototype = obj;
return new F();
}
var queue = [], writing;
function nextWrite() {
if(!writing) {
var w = queue.shift();
// End Of Queue
if(typeof w === 'undefined' ) {
return;
} else if(typeof w.el === 'function') {
w.el();
return;
}
writing = true;
writerFor(w.el,function() {
writing = false;
nextWrite();
}).close(w.html);
}
}
/**
* Simplified API. Just call this function with the target element
* and the HTML as many times as you like. It will ensure that writes
* don't overlap.
*
* Pass a function and it will be called after the previous writes finish.
*/
writerFor.write = function(el,html) {
queue.push({el:el,html:html});
nextWrite();
};
// export writerFor
define(['element.write'],function(elWrite) {
elementWrite = writerFor.elementWrite = elWrite;
writerFor.fixUrls = function(src) {
return src.replace(/&/g,'&');
};
return writerFor;
});
})(typeof define == 'function' ? define : function(deps,factory) {
if (typeof exports === 'object') {
module.exports = factory(require(deps[0]));
} else {
window.writeCapture = factory(window.elementWrite);
}
});