OLD | NEW |
---|---|
(Empty) | |
1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file | |
2 // for details. All rights reserved. Use of this source code is governed by a | |
3 // BSD-style license that can be found in the LICENSE file. | |
4 | |
5 library trydart.main; | |
6 | |
7 import 'dart:async'; | |
8 import 'dart:html'; | |
9 import 'dart:isolate'; | |
10 import 'dart:uri'; | |
11 | |
12 import '../sdk/lib/_internal/compiler/implementation/scanner/scannerlib.dart' sh ow StringScanner, EOF_TOKEN; | |
kasperl
2014/01/07 07:18:43
Long lines.
ahe
2014/01/07 14:06:23
Done.
| |
13 import '../sdk/lib/_internal/compiler/implementation/scanner/scannerlib.dart' as scanner; | |
14 | |
15 import 'decoration.dart'; | |
16 import 'themes.dart'; | |
17 | |
18 @lazy import 'compiler_isolate.dart'; | |
19 | |
20 const lazy = const DeferredLibrary('compiler_isolate'); | |
21 | |
22 var inputPre; | |
23 var outputDiv; | |
24 var hackDiv; | |
25 var outputFrame; | |
26 var compilerTimer; | |
27 var compilerPort; | |
28 var observer; | |
29 var cacheStatusElement; | |
30 bool alwaysRunInWorker = window.localStorage['alwaysRunInWorker'] == 'true'; | |
31 bool verboseCompiler = window.localStorage['verboseCompiler'] == 'true'; | |
32 bool minified = window.localStorage['minified'] == 'true'; | |
33 bool onlyAnalyze = window.localStorage['onlyAnalyze'] == 'true'; | |
34 String codeFont = ((x) => x == null ? '' : x)(window.localStorage['codeFont']); | |
kasperl
2014/01/07 07:18:43
Maybe just add an extra rawCodeFont variable? This
ahe
2014/01/07 14:06:23
Done.
| |
35 String currentSample = window.localStorage['currentSample']; | |
36 Theme currentTheme = Theme.named(window.localStorage['theme']); | |
37 bool applyingSettings = false; | |
38 | |
39 const String INDENT = '\u{a0}\u{a0}'; | |
40 | |
41 onKeyUp(KeyboardEvent e) { | |
42 if (e.keyCode == 13) { | |
43 e.preventDefault(); | |
44 DomSelection selection = window.getSelection(); | |
45 if (selection.isCollapsed && selection.anchorNode is Text) { | |
46 Text text = selection.anchorNode; | |
47 int offset = selection.anchorOffset; | |
48 text.insertData(offset, '\n'); | |
49 selection.collapse(text, offset + 1); | |
50 } | |
51 } | |
52 // This is a hack to get Safari to send mutation events on contenteditable. | |
53 var newDiv = new DivElement(); | |
54 hackDiv.replaceWith(newDiv); | |
55 hackDiv = newDiv; | |
56 } | |
57 | |
58 bool isMalformedInput = false; | |
59 String currentSource = ""; | |
60 | |
61 onMutation(List<MutationRecord> mutations, MutationObserver observer) { | |
kasperl
2014/01/07 07:18:43
This method is very long. Maybe break it into a fe
ahe
2014/01/07 14:06:23
Totally agree. I'll add a TODO for now.
| |
62 scheduleCompilation(); | |
63 | |
64 for (Element element in inputPre.queryAll('a[class="diagnostic"]>span')) { | |
65 element.remove(); | |
66 } | |
67 // Discard clean-up mutations. | |
68 observer.takeRecords(); | |
69 | |
70 DomSelection selection = window.getSelection(); | |
71 | |
72 while (!mutations.isEmpty) { | |
73 for (MutationRecord record in mutations) { | |
74 String type = record.type; | |
75 switch (type) { | |
76 | |
77 case 'characterData': | |
kasperl
2014/01/07 07:18:43
Indent cases.
ahe
2014/01/07 14:06:23
OK. That was hard:
(setq my-dart-style
'((c
| |
78 | |
79 bool hasSelection = false; | |
80 int offset = selection.anchorOffset; | |
81 if (selection.isCollapsed && selection.anchorNode == record.target) { | |
82 hasSelection = true; | |
83 } | |
84 var parent = record.target.parentNode; | |
85 if (parent != inputPre) { | |
86 inlineChildren(parent); | |
87 } | |
88 if (hasSelection) { | |
89 selection.collapse(record.target, offset); | |
90 } | |
91 break; | |
92 | |
93 default: | |
94 if (!record.addedNodes.isEmpty) { | |
95 for (var node in record.addedNodes) { | |
96 | |
97 if (node.nodeType != Node.ELEMENT_NODE) continue; | |
98 | |
99 if (node is BRElement) { | |
100 if (selection.anchorNode != node) { | |
101 node.replaceWith(new Text('\n')); | |
102 } | |
103 } else { | |
104 var parent = node.parentNode; | |
105 if (parent == null) continue; | |
106 var nodes = new List.from(node.nodes); | |
107 var style = node.getComputedStyle(); | |
108 if (style.display != 'inline') { | |
109 var previous = node.previousNode; | |
110 if (previous is Text) { | |
111 previous.appendData('\n'); | |
112 } else { | |
113 parent.insertBefore(new Text('\n'), node); | |
114 } | |
115 } | |
116 for (Node child in nodes) { | |
117 child.remove(); | |
118 parent.insertBefore(child, node); | |
119 } | |
120 node.remove(); | |
121 } | |
122 } | |
123 } | |
124 } | |
125 } | |
126 mutations = observer.takeRecords(); | |
127 } | |
128 | |
129 if (!inputPre.nodes.isEmpty && inputPre.nodes.last is Text) { | |
130 Text text = inputPre.nodes.last; | |
131 if (!text.text.endsWith('\n')) { | |
132 text.appendData('\n'); | |
133 } | |
134 } | |
135 | |
136 int offset = 0; | |
137 int anchorOffset = 0; | |
138 bool hasSelection = false; | |
139 Node anchorNode = selection.anchorNode; | |
140 void walk4(Node node) { | |
141 // TODO(ahe): Use TreeWalker when that is exposed. | |
142 // function textNodesUnder(root){ | |
143 // var n, a=[], walk=document.createTreeWalker(root,NodeFilter.SHOW_TEXT,n ull,false); | |
kasperl
2014/01/07 07:18:43
Long line.
ahe
2014/01/07 14:06:23
Done.
| |
144 // while(n=walk.nextNode()) a.push(n); | |
145 // return a; | |
146 // } | |
147 int type = node.nodeType; | |
148 if (type == Node.TEXT_NODE || type == Node.CDATA_SECTION_NODE) { | |
149 if (anchorNode == node) { | |
150 hasSelection = true; | |
151 anchorOffset = selection.anchorOffset + offset; | |
152 return; | |
153 } | |
154 offset += node.length; | |
155 } | |
156 | |
157 var child = node.$dom_firstChild; | |
158 while(child != null) { | |
kasperl
2014/01/07 07:18:43
DANGER. DANGER. You know the drill!
ahe
2014/01/07 14:06:23
Done.
| |
159 walk4(child); | |
160 if (hasSelection) return; | |
161 child = child.nextNode; | |
162 } | |
163 } | |
164 if (selection.isCollapsed) { | |
165 walk4(inputPre); | |
166 } | |
167 | |
168 currentSource = inputPre.text; | |
169 inputPre.nodes.clear(); | |
170 inputPre.appendText(currentSource); | |
171 if (hasSelection) { | |
172 selection.collapse(inputPre.$dom_firstChild, anchorOffset); | |
173 } | |
174 | |
175 isMalformedInput = false; | |
176 for (Node node in new List.from(inputPre.nodes)) { | |
177 if (node is! Text) continue; | |
178 String text = node.text; | |
179 | |
180 var token = new StringScanner(text, includeComments: true).tokenize(); | |
181 int offset = 0; | |
182 for (;token.kind != EOF_TOKEN; token = token.next) { | |
183 Decoration decoration = getDecoration(token); | |
184 if (decoration == null) continue; | |
185 bool hasSelection = false; | |
186 int selectionOffset = selection.anchorOffset; | |
187 | |
188 if (selection.isCollapsed && selection.anchorNode == node) { | |
189 hasSelection = true; | |
190 selectionOffset = selection.anchorOffset; | |
191 } | |
192 int splitPoint = token.charOffset - offset; | |
193 Text str = node.splitText(splitPoint); | |
194 Text after = str.splitText(token.slowCharCount); | |
195 offset += splitPoint + token.slowCharCount; | |
196 inputPre.insertBefore(after, node.nextNode); | |
197 inputPre.insertBefore(decoration.applyTo(str), after); | |
198 | |
199 if (hasSelection && selectionOffset > node.length) { | |
200 selectionOffset -= node.length; | |
201 if (selectionOffset > str.length) { | |
202 selectionOffset -= str.length; | |
203 selection.collapse(after, selectionOffset); | |
204 } else { | |
205 selection.collapse(str, selectionOffset); | |
206 } | |
207 } | |
208 node = after; | |
209 } | |
210 } | |
211 | |
212 window.localStorage['currentSource'] = currentSource; | |
213 | |
214 // Discard highlighting mutations. | |
215 observer.takeRecords(); | |
216 } | |
217 | |
218 addDiagnostic(String kind, String message, int begin, int end) { | |
219 observer.disconnect(); | |
220 DomSelection selection = window.getSelection(); | |
221 int offset = 0; | |
222 int anchorOffset = 0; | |
223 bool hasSelection = false; | |
224 Node anchorNode = selection.anchorNode; | |
225 bool foundNode = false; | |
226 void walk4(Node node) { | |
kasperl
2014/01/07 07:18:43
Could this be refactored somehow? You have walk4 i
ahe
2014/01/07 14:06:23
This might be fixed by using TreeWalker.
| |
227 // TODO(ahe): Use TreeWalker when that is exposed. | |
228 int type = node.nodeType; | |
229 if (type == Node.TEXT_NODE || type == Node.CDATA_SECTION_NODE) { | |
230 // print('walking: ${node.data}'); | |
231 if (anchorNode == node) { | |
232 hasSelection = true; | |
233 anchorOffset = selection.anchorOffset + offset; | |
234 } | |
235 int newOffset = offset + node.length; | |
236 if (offset <= begin && begin < newOffset) { | |
237 hasSelection = node == anchorNode; | |
238 anchorOffset = selection.anchorOffset; | |
239 Node marker = new Text(""); | |
240 node.replaceWith(marker); | |
241 // TODO(ahe): Don't highlight everything in the node. Find | |
242 // the relevant token. | |
243 if (kind == 'error') { | |
244 marker.replaceWith(diagnostic(node, error(message))); | |
245 } else if (kind == 'warning') { | |
246 marker.replaceWith(diagnostic(node, warning(message))); | |
247 } else { | |
248 marker.replaceWith(diagnostic(node, info(message))); | |
249 } | |
250 if (hasSelection) { | |
251 selection.collapse(node, anchorOffset); | |
252 } | |
253 foundNode = true; | |
254 return; | |
255 } | |
256 offset = newOffset; | |
257 } else if (type == Node.ELEMENT_NODE) { | |
258 if (node.classes.contains('alert')) return; | |
259 } | |
260 | |
261 var child = node.$dom_firstChild; | |
262 while(child != null && !foundNode) { | |
263 walk4(child); | |
264 child = child.nextNode; | |
265 } | |
266 } | |
267 walk4(inputPre); | |
268 | |
269 if (!foundNode) { | |
270 outputDiv.appendText('$message\n'); | |
271 } | |
272 | |
273 observer.takeRecords(); | |
274 observer.observe(inputPre, childList: true, characterData: true, subtree: true ); | |
kasperl
2014/01/07 07:18:43
Long line.
ahe
2014/01/07 14:06:23
Done.
| |
275 } | |
276 | |
277 void inlineChildren(Element element) { | |
278 if (element == null) return; | |
279 var parent = element.parentNode; | |
280 if (parent == null) return; | |
281 for (Node child in new List.from(element.nodes)) { | |
282 child.remove(); | |
283 parent.insertBefore(child, element); | |
284 } | |
285 element.remove(); | |
286 } | |
287 | |
288 int count = 0; | |
289 | |
290 void scheduleCompilation() { | |
291 if (applyingSettings) return; | |
292 if (compilerTimer != null) { | |
293 compilerTimer.cancel(); | |
294 compilerTimer = null; | |
295 } | |
296 compilerTimer = | |
297 new Timer(const Duration(milliseconds: 500), startCompilation); | |
298 } | |
299 | |
300 void startCompilation() { | |
301 if (compilerTimer != null) { | |
302 compilerTimer.cancel(); | |
303 compilerTimer = null; | |
304 } | |
305 | |
306 new CompilationProcess(currentSource, outputDiv).start(); | |
307 } | |
308 | |
309 class CompilationProcess { | |
310 final String source; | |
311 final Element console; | |
312 final ReceivePort receivePort = new ReceivePort(); | |
313 bool isCleared = false; | |
314 bool isDone = false; | |
315 bool usesDartHtml = false; | |
316 Worker worker; | |
317 List<String> objectUrls = <String>[]; | |
318 | |
319 static CompilationProcess current; | |
320 | |
321 CompilationProcess(this.source, this.console); | |
322 | |
323 static bool shouldStartCompilation() { | |
324 if (compilerPort == null) return false; | |
325 if (isMalformedInput) return false; | |
326 if (current != null) return current.isDone; | |
327 return true; | |
328 } | |
329 | |
330 void clear() { | |
331 if (verboseCompiler) return; | |
332 if (!isCleared) console.nodes.clear(); | |
333 isCleared = true; | |
334 } | |
335 | |
336 void start() { | |
337 if (!shouldStartCompilation()) { | |
338 receivePort.close(); | |
339 if (!isMalformedInput) scheduleCompilation(); | |
340 return; | |
341 } | |
342 if (current != null) current.dispose(); | |
343 current = this; | |
344 console.nodes.clear(); | |
345 var options = []; | |
346 if (verboseCompiler) options.add('--verbose'); | |
347 if (minified) options.add('--minify'); | |
348 if (onlyAnalyze) options.add('--analyze-only'); | |
349 compilerPort.send(['options', options], receivePort.toSendPort()); | |
350 console.appendHtml('<i class="icon-spinner icon-spin"></i>'); | |
351 console.appendText(' Compiling Dart program...\n'); | |
352 outputFrame.style.display = 'none'; | |
353 receivePort.receive(onMessage); | |
354 compilerPort.send(source, receivePort.toSendPort()); | |
355 } | |
356 | |
357 void dispose() { | |
358 if (worker != null) worker.terminate(); | |
359 objectUrls.forEach(Url.revokeObjectUrl); | |
360 } | |
361 | |
362 onMessage(message, _) { | |
363 String kind = message is String ? message : message[0]; | |
364 var data = (message is List && message.length == 2) ? message[1] : null; | |
365 switch (kind) { | |
366 case 'done': return onDone(data); | |
kasperl
2014/01/07 07:18:43
I'd indent all the cases.
ahe
2014/01/07 14:06:23
Done.
| |
367 case 'url': return onUrl(data); | |
368 case 'code': return onCode(data); | |
369 case 'diagnostic': return onDiagnostic(data); | |
370 case 'crash': return onCrash(data); | |
371 case 'failed': return onFail(data); | |
372 case 'dart:html': return onDartHtml(data); | |
373 default: | |
374 throw ['Unknown message kind', message]; | |
375 } | |
376 } | |
377 | |
378 onDartHtml(_) { | |
379 usesDartHtml = true; | |
380 } | |
381 | |
382 onFail(_) { | |
383 clear(); | |
384 consolePrint('Compilation failed'); | |
385 } | |
386 | |
387 onDone(_) { | |
388 isDone = true; | |
389 receivePort.close(); | |
390 } | |
391 | |
392 // This is called in browsers that support creating Object URLs in a | |
393 // web worker. For example, Chrome and Firefox 21. | |
394 onUrl(String url) { | |
395 objectUrls.add(url); | |
396 clear(); | |
397 String wrapper = | |
398 'function dartPrint(msg) { self.postMessage(msg); };' | |
399 'self.importScripts("$url");'; | |
400 var wrapperUrl = | |
401 Url.createObjectUrl(new Blob([wrapper], 'application/javascript')); | |
402 objectUrls.add(wrapperUrl); | |
403 void retryInIframe(_) { | |
404 var frame = makeOutputFrame(url); | |
405 outputFrame.replaceWith(frame); | |
406 outputFrame = frame; | |
407 } | |
408 void onError(String errorMessage) { | |
409 console.appendText(errorMessage); | |
410 console.appendText(' '); | |
411 console.append(buildButton('Try in iframe', retryInIframe)); | |
412 console.appendText('\n'); | |
413 } | |
414 if (usesDartHtml && !alwaysRunInWorker) { | |
415 retryInIframe(null); | |
416 } else { | |
417 runInWorker(wrapperUrl, onError); | |
418 } | |
419 } | |
420 | |
421 // This is called in browsers that do not support creating Object | |
422 // URLs in a web worker. For example, Safari and Firefox < 21. | |
423 onCode(String code) { | |
424 clear(); | |
425 | |
426 void retryInIframe(_) { | |
427 // The obvious thing would be to call [makeOutputFrame], but | |
428 // Safari doesn't support access to Object URLs in an iframe. | |
429 | |
430 var frame = new IFrameElement() | |
431 ..src = 'iframe.html' | |
432 ..style.width = '100%' | |
433 ..style.height = '0px' | |
434 ..seamless = false; | |
435 frame.onLoad.listen((_) { | |
436 frame.contentWindow.postMessage(['source', code], '*'); | |
437 }); | |
438 outputFrame.replaceWith(frame); | |
439 outputFrame = frame; | |
440 } | |
441 | |
442 void onError(String errorMessage) { | |
443 console.appendText(errorMessage); | |
444 console.appendText(' '); | |
445 console.append(buildButton('Try in iframe', retryInIframe)); | |
446 console.appendText('\n'); | |
447 } | |
448 | |
449 String codeWithPrint = | |
450 '$code\n' | |
451 'function dartPrint(msg) { postMessage(msg); }\n'; | |
452 var url = | |
453 Url.createObjectUrl( | |
454 new Blob([codeWithPrint], 'application/javascript')); | |
455 objectUrls.add(url); | |
456 | |
457 if (usesDartHtml && !alwaysRunInWorker) { | |
458 retryInIframe(null); | |
459 } else { | |
460 runInWorker(url, onError); | |
461 } | |
462 } | |
463 | |
464 void runInWorker(String url, void onError(String errorMessage)) { | |
465 worker = new Worker(url) | |
466 ..onMessage.listen((MessageEvent event) { | |
467 consolePrint(event.data); | |
468 }) | |
469 ..onError.listen((ErrorEvent event) { | |
470 worker.terminate(); | |
471 worker = null; | |
472 onError(event.message); | |
473 }); | |
474 } | |
475 | |
476 onDiagnostic(Map<String, dynamic> diagnostic) { | |
477 String kind = diagnostic['kind']; | |
478 String message = diagnostic['message']; | |
479 if (kind == 'verbose info') { | |
480 if (verboseCompiler) { | |
481 consolePrint(message); | |
482 } | |
483 return; | |
484 } | |
485 String uri = diagnostic['uri']; | |
486 if (uri == null) { | |
487 clear(); | |
488 consolePrint(message); | |
489 return; | |
490 } | |
491 if (uri != 'memory:/main.dart') return; | |
492 if (currentSource != source) return; | |
493 int begin = diagnostic['begin']; | |
494 int end = diagnostic['end']; | |
495 if (begin == null) return; | |
496 addDiagnostic(kind, message, begin, end); | |
497 } | |
498 | |
499 onCrash(data) { | |
500 consolePrint(data); | |
501 } | |
502 | |
503 void consolePrint(message) { | |
504 console.appendText('$message\n'); | |
505 } | |
506 } | |
507 | |
508 Decoration getDecoration(scanner.Token token) { | |
509 String tokenValue = token.slowToString(); | |
510 String tokenInfo = token.info.value.slowToString(); | |
511 if (tokenInfo == 'string') return currentTheme.string; | |
512 // if (tokenInfo == 'identifier') return identifier; | |
513 if (tokenInfo == 'keyword') return currentTheme.keyword; | |
514 if (tokenInfo == 'comment') return currentTheme.singleLineComment; | |
515 if (tokenInfo == 'malformed input') { | |
516 isMalformedInput = true; | |
517 return new DiagnosticDecoration('error', tokenValue); | |
518 } | |
519 return null; | |
520 } | |
521 | |
522 diagnostic(text, tip) { | |
523 if (text is String) { | |
524 text = new Text(text); | |
525 } | |
526 return new AnchorElement() | |
527 ..classes.add('diagnostic') | |
528 ..append(text) | |
529 ..append(tip); | |
530 } | |
531 | |
532 img(src, width, height, alt) { | |
533 return new ImageElement(src: src, width: width, height: height)..alt = alt; | |
534 } | |
535 | |
536 makeOutputFrame(String scriptUrl) { | |
537 final String outputHtml = ''' | |
538 <!DOCTYPE html> | |
539 <html lang="en"> | |
540 <head> | |
541 <title>JavaScript output</title> | |
542 <meta http-equiv="Content-type" content="text/html;charset=UTF-8"> | |
543 </head> | |
544 <body> | |
545 <script type="application/javascript" src="$outputHelper"></script> | |
546 <script type="application/javascript" src="$scriptUrl"></script> | |
547 </body> | |
548 </html> | |
549 '''; | |
550 | |
551 return new IFrameElement() | |
552 ..src = Url.createObjectUrl(new Blob([outputHtml], "text/html")) | |
553 ..style.width = '100%' | |
554 ..style.height = '0px' | |
555 ..seamless = false; | |
556 } | |
557 | |
558 const String HAS_NON_DOM_HTTP_REQUEST = 'spawnFunction supports HttpRequest'; | |
559 const String NO_NON_DOM_HTTP_REQUEST = | |
560 'spawnFunction does not support HttpRequest'; | |
561 | |
562 | |
563 checkHttpRequest() { | |
564 port.receive((String uri, SendPort replyTo) { | |
565 try { | |
566 new HttpRequest(); | |
567 replyTo.send(HAS_NON_DOM_HTTP_REQUEST); | |
568 } catch (e, trace) { | |
569 replyTo.send(NO_NON_DOM_HTTP_REQUEST); | |
570 } | |
571 port.close(); | |
572 }); | |
573 } | |
574 | |
575 main() { | |
576 if (window.localStorage['currentSource'] == null) { | |
577 window.localStorage['currentSource'] = EXAMPLE_HELLO; | |
578 } | |
579 | |
580 buildUI(); | |
581 spawnFunction(checkHttpRequest).call('').then((reply) { | |
582 var compilerFuture; | |
583 if (reply == HAS_NON_DOM_HTTP_REQUEST) { | |
584 compilerFuture = spawnFunction(compilerIsolate); | |
585 } else { | |
586 compilerFuture = spawnDomFunction(compilerIsolate); | |
587 } | |
588 if (compilerFuture is! Future) { | |
589 compilerFuture = new Future.value(compilerFuture); | |
590 } | |
591 compilerFuture.then((port) { | |
592 String sdk = query('link[rel="dart-sdk"]').href; | |
593 print('Using Dart SDK: $sdk'); | |
594 port.call(sdk).then((_) { | |
595 compilerPort = port; | |
596 onMutation([], observer); | |
597 }); | |
598 }); | |
599 }); | |
600 } | |
601 | |
602 buildButton(message, action) { | |
603 if (message is String) { | |
604 message = new Text(message); | |
605 } | |
606 return new ButtonElement() | |
607 ..onClick.listen(action) | |
608 ..append(message); | |
609 } | |
610 | |
611 buildTab(message, id, action) { | |
612 if (message is String) { | |
613 message = new Text(message); | |
614 } | |
615 | |
616 onClick(MouseEvent event) { | |
617 event.preventDefault(); | |
618 Element e = event.target; | |
619 LIElement parent = e.parent; | |
620 parent.parent.query('li[class="active"]').classes.remove('active'); | |
621 parent.classes.add('active'); | |
622 action(event); | |
623 } | |
624 | |
625 inspirationCallbacks[id] = action; | |
626 | |
627 return new OptionElement()..append(message)..id = id; | |
628 } | |
629 | |
630 Map<String, Function> inspirationCallbacks = new Map<String, Function>(); | |
631 | |
632 void onInspirationChange(Event event) { | |
633 SelectElement select = event.target; | |
634 String id = select.queryAll('option')[select.selectedIndex].id; | |
635 Function action = inspirationCallbacks[id]; | |
636 if (action != null) action(event); | |
637 outputFrame.style.display = 'none'; | |
638 } | |
639 | |
640 buildUI() { | |
641 window.localStorage['currentSample'] = '$currentSample'; | |
642 | |
643 var inspirationTabs = document.getElementById('inspiration'); | |
644 var htmlGroup = new OptGroupElement()..label = 'HTML'; | |
645 var benchmarkGroup = new OptGroupElement()..label = 'Benchmarks'; | |
646 inspirationTabs.append(new OptionElement()..appendText('Pick an example')); | |
647 inspirationTabs.onChange.listen(onInspirationChange); | |
648 // inspirationTabs.classes.addAll(['nav', 'nav-tabs']); | |
649 inspirationTabs.append(buildTab('Hello, World!', 'EXAMPLE_HELLO', (_) { | |
650 inputPre | |
651 ..nodes.clear() | |
652 ..appendText(EXAMPLE_HELLO); | |
653 })); | |
654 inspirationTabs.append(buildTab('Fibonacci', 'EXAMPLE_FIBONACCI', (_) { | |
655 inputPre | |
656 ..nodes.clear() | |
657 ..appendText(EXAMPLE_FIBONACCI); | |
658 })); | |
659 inspirationTabs.append(htmlGroup); | |
660 inspirationTabs.append(benchmarkGroup); | |
661 | |
662 htmlGroup.append( | |
663 buildTab('Hello, World!', 'EXAMPLE_HELLO_HTML', (_) { | |
664 inputPre | |
665 ..nodes.clear() | |
666 ..appendText(EXAMPLE_HELLO_HTML); | |
667 })); | |
668 htmlGroup.append( | |
669 buildTab('Fibonacci', 'EXAMPLE_FIBONACCI_HTML', (_) { | |
670 inputPre | |
671 ..nodes.clear() | |
672 ..appendText(EXAMPLE_FIBONACCI_HTML); | |
673 })); | |
674 htmlGroup.append(buildTab('Sunflower', 'EXAMPLE_SUNFLOWER', (_) { | |
675 inputPre | |
676 ..nodes.clear() | |
677 ..appendText(EXAMPLE_SUNFLOWER); | |
678 })); | |
679 | |
680 benchmarkGroup.append(buildTab('DeltaBlue', 'BENCHMARK_DELTA_BLUE', (_) { | |
681 inputPre.contentEditable = 'false'; | |
682 String deltaBlueUri = query('link[rel="benchmark-DeltaBlue"]').href; | |
683 String benchmarkBaseUri = query('link[rel="benchmark-base"]').href; | |
684 HttpRequest.getString(benchmarkBaseUri).then((String benchmarkBase) { | |
685 HttpRequest.getString(deltaBlueUri).then((String deltaBlue) { | |
686 benchmarkBase = benchmarkBase.replaceFirst( | |
687 'part of benchmark_harness;', '// part of benchmark_harness;'); | |
688 deltaBlue = deltaBlue.replaceFirst( | |
689 "import 'package:benchmark_harness/benchmark_harness.dart';", | |
690 benchmarkBase); | |
691 inputPre | |
692 ..nodes.clear() | |
693 ..appendText(deltaBlue) | |
694 ..contentEditable = 'true'; | |
695 }); | |
696 }); | |
697 })); | |
698 | |
699 benchmarkGroup.append(buildTab('Richards', 'BENCHMARK_RICHARDS', (_) { | |
700 inputPre.contentEditable = 'false'; | |
701 String richardsUri = query('link[rel="benchmark-Richards"]').href; | |
702 String benchmarkBaseUri = query('link[rel="benchmark-base"]').href; | |
703 HttpRequest.getString(benchmarkBaseUri).then((String benchmarkBase) { | |
704 HttpRequest.getString(richardsUri).then((String richards) { | |
705 benchmarkBase = benchmarkBase.replaceFirst( | |
706 'part of benchmark_harness;', '// part of benchmark_harness;'); | |
707 richards = richards.replaceFirst( | |
708 "import 'package:benchmark_harness/benchmark_harness.dart';", | |
709 benchmarkBase); | |
710 inputPre | |
711 ..nodes.clear() | |
712 ..appendText(richards) | |
713 ..contentEditable = 'true'; | |
714 }); | |
715 }); | |
716 })); | |
717 | |
718 // TODO(ahe): Update currentSample. Or try switching to a drop-down menu. | |
719 var active = inspirationTabs.query('[id="$currentSample"]'); | |
720 if (active == null) { | |
721 // inspirationTabs.query('li').classes.add('active'); | |
722 } | |
723 | |
724 (inputPre = new DivElement()) | |
725 ..classes.add('well') | |
726 ..style.backgroundColor = currentTheme.background.color | |
727 ..style.color = currentTheme.foreground.color | |
728 ..style.overflow = 'auto' | |
729 ..style.whiteSpace = 'pre' | |
730 ..style.font = codeFont | |
731 ..spellcheck = false; | |
732 | |
733 inputPre.contentEditable = 'true'; | |
734 inputPre.onKeyDown.listen(onKeyUp); | |
735 | |
736 var inputWrapper = new DivElement() | |
737 ..append(inputPre) | |
738 ..style.position = 'relative'; | |
739 | |
740 var inputHeader = new DivElement()..appendText('Code'); | |
741 | |
742 inputHeader.style | |
743 ..right = '3px' | |
744 ..top = '0px' | |
745 ..position = 'absolute'; | |
746 inputWrapper.append(inputHeader); | |
747 | |
748 outputFrame = | |
749 makeOutputFrame( | |
750 Url.createObjectUrl(new Blob([''], 'application/javascript'))); | |
751 | |
752 outputDiv = new PreElement(); | |
753 outputDiv.style | |
754 ..backgroundColor = currentTheme.background.color | |
755 ..color = currentTheme.foreground.color | |
756 ..overflow = 'auto' | |
757 ..padding = '1em' | |
758 ..minHeight = '10em' | |
759 ..whiteSpace = 'pre-wrap'; | |
760 | |
761 var outputWrapper = new DivElement() | |
762 ..append(outputDiv) | |
763 ..style.position = 'relative'; | |
764 | |
765 var consoleHeader = new DivElement()..appendText('Console'); | |
766 | |
767 consoleHeader.style | |
768 ..right = '3px' | |
769 ..top = '0px' | |
770 ..position = 'absolute'; | |
771 outputWrapper.append(consoleHeader); | |
772 | |
773 hackDiv = new DivElement(); | |
774 | |
775 var saveButton = new ButtonElement() | |
776 ..onClick.listen((_) { | |
777 var blobUrl = | |
778 Url.createObjectUrl(new Blob([inputPre.text], 'text/plain')); | |
779 var save = new AnchorElement(href: blobUrl); | |
780 save.target = '_blank'; | |
781 save.download = 'untitled.dart'; | |
782 save.dispatchEvent(new Event.eventType('Event', 'click')); | |
783 }) | |
784 ..style.position = 'absolute' | |
785 ..style.right = '0px' | |
786 ..appendText('Save'); | |
787 | |
788 cacheStatusElement = document.getElementById('appcache-status'); | |
789 updateCacheStatus(); | |
790 | |
791 // TODO(ahe): Switch to two column layout so the console is on the right. | |
792 var section = document.query('article[class="homepage"]>section'); | |
793 | |
794 DivElement tryColumn = document.getElementById('try-dart-column'); | |
795 DivElement runColumn = document.getElementById('run-dart-column'); | |
796 | |
797 tryColumn.append(inputWrapper); | |
798 outputFrame.style.display = 'none'; | |
799 runColumn.append(outputFrame); | |
800 runColumn.append(outputWrapper); | |
801 runColumn.append(hackDiv); | |
802 | |
803 var settingsElement = document.getElementById('settings'); | |
804 settingsElement.onClick.listen(openSettings); | |
805 | |
806 window.onMessage.listen((MessageEvent event) { | |
807 if (event.data is List) { | |
808 List message = event.data; | |
809 if (message.length > 0) { | |
810 switch (message[0]) { | |
811 case 'error': | |
812 Map diagnostics = message[1]; | |
813 String url = diagnostics['url']; | |
814 outputDiv.appendText('${diagnostics["message"]}\n'); | |
815 return; | |
816 case 'scrollHeight': | |
817 int scrollHeight = message[1]; | |
818 if (scrollHeight > 0) { | |
819 outputFrame.style.height = '${scrollHeight}px'; | |
820 } | |
821 return; | |
822 } | |
823 } | |
824 } | |
825 outputDiv.appendText('${event.data}\n'); | |
826 }); | |
827 | |
828 observer = new MutationObserver(onMutation) | |
829 ..observe(inputPre, childList: true, characterData: true, subtree: true); | |
830 | |
831 window.setImmediate(() { | |
832 inputPre.appendText(window.localStorage['currentSource']); | |
833 }); | |
834 | |
835 // You cannot install event handlers on window.applicationCache | |
836 // until the window has loaded. In dartium, that's later than this | |
837 // method is called. | |
838 window.onLoad.listen(onLoad); | |
839 | |
840 // However, in dart2js, the window has already loaded, and onLoad is | |
841 // never called. | |
842 onLoad(null); | |
843 } | |
844 | |
845 void openSettings(MouseEvent event) { | |
846 event.preventDefault(); | |
847 | |
848 var backdrop = new DivElement()..classes.add('modal-backdrop'); | |
849 document.body.append(backdrop); | |
850 | |
851 void updateCodeFont(Event e) { | |
852 codeFont = e.target.value; | |
853 inputPre.style.font = codeFont; | |
854 backdrop.style.opacity = '0.0'; | |
855 } | |
856 | |
857 void updateTheme(Event e) { | |
858 var select = e.target; | |
859 String theme = select.queryAll('option')[select.selectedIndex].text; | |
860 window.localStorage['theme'] = theme; | |
861 currentTheme = Theme.named(theme); | |
862 | |
863 inputPre.style | |
864 ..backgroundColor = currentTheme.background.color | |
865 ..color = currentTheme.foreground.color; | |
866 | |
867 outputDiv.style | |
868 ..backgroundColor = currentTheme.background.color | |
869 ..color = currentTheme.foreground.color; | |
870 | |
871 backdrop.style.opacity = '0.0'; | |
872 | |
873 applyingSettings = true; | |
874 onMutation([], observer); | |
875 applyingSettings = false; | |
876 } | |
877 | |
878 | |
879 var body = document.getElementById('settings-body'); | |
880 | |
881 body.nodes.clear(); | |
882 | |
883 var form = new FormElement(); | |
884 var fieldSet = new FieldSetElement(); | |
885 body.append(form); | |
886 form.append(fieldSet); | |
887 | |
888 buildCheckBox(String text, bool defaultValue, void action(Event e)) { | |
889 var checkBox = new CheckboxInputElement() | |
890 ..defaultChecked = defaultValue | |
891 ..onChange.listen(action); | |
892 return new LabelElement() | |
893 ..classes.add('checkbox') | |
894 ..append(checkBox) | |
895 ..appendText(' $text'); | |
896 } | |
897 | |
898 fieldSet.append( | |
kasperl
2014/01/07 07:18:43
Maybe it would make sense to have an abstraction o
ahe
2014/01/07 14:06:23
Added TODO.
| |
899 buildCheckBox( | |
900 'Always run in Worker thread.', alwaysRunInWorker, | |
901 (Event e) { alwaysRunInWorker = e.target.checked; })); | |
902 | |
903 fieldSet.append( | |
904 buildCheckBox( | |
905 'Verbose compiler output.', verboseCompiler, | |
906 (Event e) { verboseCompiler = e.target.checked; })); | |
907 | |
908 fieldSet.append( | |
909 buildCheckBox( | |
910 'Generate compact (minified) JavaScript.', minified, | |
911 (Event e) { minified = e.target.checked; })); | |
912 | |
913 fieldSet.append( | |
914 buildCheckBox( | |
915 'Only analyze program.', onlyAnalyze, | |
916 (Event e) { onlyAnalyze = e.target.checked; })); | |
917 | |
918 fieldSet.append(new LabelElement()..appendText('Code font:')); | |
919 var textInput = new TextInputElement(); | |
920 textInput.classes.add('input-block-level'); | |
921 if (codeFont != null && codeFont != '') { | |
922 textInput.value = codeFont; | |
923 } | |
924 textInput.placeholder = 'Enter a size and font, for example, 11pt monospace'; | |
925 textInput.onChange.listen(updateCodeFont); | |
926 fieldSet.append(textInput); | |
927 | |
928 fieldSet.append(new LabelElement()..appendText('Theme:')); | |
929 var themeSelector = new SelectElement(); | |
930 themeSelector.classes.add('input-block-level'); | |
931 for (Theme theme in THEMES) { | |
932 OptionElement option = new OptionElement()..appendText(theme.name); | |
933 if (theme == currentTheme) option.selected = true; | |
934 themeSelector.append(option); | |
935 } | |
936 themeSelector.onChange.listen(updateTheme); | |
937 fieldSet.append(themeSelector); | |
938 | |
939 var dialog = document.getElementById('settings-dialog'); | |
940 | |
941 dialog.style.display = 'block'; | |
942 dialog.classes.add('in'); | |
943 | |
944 onSubmit(Event event) { | |
945 event.preventDefault(); | |
946 | |
947 window.localStorage['alwaysRunInWorker'] = '$alwaysRunInWorker'; | |
948 window.localStorage['verboseCompiler'] = '$verboseCompiler'; | |
949 window.localStorage['minified'] = '$minified'; | |
950 window.localStorage['onlyAnalyze'] = '$onlyAnalyze'; | |
951 window.localStorage['codeFont'] = '$codeFont'; | |
952 | |
953 dialog.style.display = 'none'; | |
954 dialog.classes.remove('in'); | |
955 backdrop.remove(); | |
956 } | |
957 form.onSubmit.listen(onSubmit); | |
958 | |
959 var doneButton = document.getElementById('settings-done'); | |
960 doneButton.onClick.listen(onSubmit); | |
961 } | |
962 | |
963 /// Called when the window has finished loading. | |
964 void onLoad(Event event) { | |
965 window.applicationCache.onUpdateReady.listen((_) => updateCacheStatus()); | |
966 window.applicationCache.onCached.listen((_) => updateCacheStatus()); | |
967 window.applicationCache.onChecking.listen((_) => updateCacheStatus()); | |
968 window.applicationCache.onDownloading.listen((_) => updateCacheStatus()); | |
969 window.applicationCache.onError.listen((_) => updateCacheStatus()); | |
970 window.applicationCache.onNoUpdate.listen((_) => updateCacheStatus()); | |
971 window.applicationCache.onObsolete.listen((_) => updateCacheStatus()); | |
972 window.applicationCache.onProgress.listen(onCacheProgress); | |
973 } | |
974 | |
975 onCacheProgress(ProgressEvent event) { | |
976 if (!event.lengthComputable) { | |
977 updateCacheStatus(); | |
978 return; | |
979 } | |
980 cacheStatusElement.nodes.clear(); | |
981 cacheStatusElement.appendText('Downloading SDK '); | |
982 var progress = '${event.loaded} of ${event.total}'; | |
983 if (MeterElement.supported) { | |
984 cacheStatusElement.append( | |
985 new MeterElement() | |
986 ..appendText(progress) | |
987 ..min = 0 | |
988 ..max = event.total | |
989 ..value = event.loaded); | |
990 } else { | |
991 cacheStatusElement.appendText(progress); | |
992 } | |
993 } | |
994 | |
995 String cacheStatus() { | |
996 if (!ApplicationCache.supported) return 'offline not supported'; | |
997 int status = window.applicationCache.status; | |
998 if (status == ApplicationCache.CHECKING) return 'Checking for updates'; | |
999 if (status == ApplicationCache.DOWNLOADING) return 'Downloading SDK'; | |
1000 if (status == ApplicationCache.IDLE) return 'Try Dart! works offline'; | |
1001 if (status == ApplicationCache.OBSOLETE) return 'OBSOLETE'; | |
1002 if (status == ApplicationCache.UNCACHED) return 'offline not available'; | |
1003 if (status == ApplicationCache.UPDATEREADY) return 'SDK downloaded'; | |
1004 return '?'; | |
1005 } | |
1006 | |
1007 void updateCacheStatus() { | |
1008 cacheStatusElement.nodes.clear(); | |
1009 String status = window.applicationCache.status; | |
1010 if (status == ApplicationCache.UPDATEREADY) { | |
1011 cacheStatusElement.appendText('New version of Try Dart! ready: '); | |
1012 cacheStatusElement.append( | |
1013 new AnchorElement(href: '#') | |
1014 ..appendText('Load') | |
1015 ..onClick.listen((event) { | |
1016 event.preventDefault(); | |
1017 window.applicationCache.swapCache(); | |
1018 window.location.reload(); | |
1019 })); | |
1020 } else if (status == ApplicationCache.IDLE) { | |
1021 cacheStatusElement.appendText(cacheStatus()); | |
1022 cacheStatusElement.classes.add('offlineyay'); | |
1023 new Timer(const Duration(seconds: 10), () { | |
1024 cacheStatusElement.style.display = 'none'; | |
1025 }); | |
1026 } else { | |
1027 cacheStatusElement.appendText(cacheStatus()); | |
1028 } | |
1029 } | |
1030 | |
1031 void compilerIsolate() { | |
1032 lazy.load().then((_) => port.receive(compile)); | |
1033 } | |
1034 | |
1035 final String outputHelper = | |
1036 Url.createObjectUrl(new Blob([OUTPUT_HELPER], 'application/javascript')); | |
1037 | |
1038 const String EXAMPLE_HELLO = r''' | |
1039 // Go ahead and modify this example. | |
1040 | |
1041 var greeting = "Hello, World!"; | |
1042 | |
1043 // Prints a greeting. | |
1044 void main() { | |
1045 // The [print] function displays a message in the "Console" box. | |
1046 // Try modifying the greeting above and watch the "Console" box change. | |
1047 print(greeting); | |
1048 } | |
1049 '''; | |
1050 | |
1051 const String EXAMPLE_HELLO_HTML = r''' | |
1052 // Go ahead and modify this example. | |
1053 | |
1054 import "dart:html"; | |
1055 | |
1056 var greeting = "Hello, World!"; | |
1057 | |
1058 // Displays a greeting. | |
1059 void main() { | |
1060 // This example uses HTML to display the greeting and it will appear | |
1061 // in a nested HTML frame (an iframe). | |
1062 document.body.append(new HeadingElement.h1()..appendText(greeting)); | |
1063 } | |
1064 '''; | |
1065 | |
1066 const String EXAMPLE_FIBONACCI = r''' | |
1067 // Go ahead and modify this example. | |
1068 | |
1069 // Computes the nth Fibonacci number. | |
1070 int fibonacci(int n) { | |
1071 if (n < 2) return n; | |
1072 return fibonacci(n - 1) + fibonacci(n - 2); | |
1073 } | |
1074 | |
1075 // Prints a Fibonacci number. | |
1076 void main() { | |
1077 int i = 20; | |
1078 String message = "fibonacci($i) = ${fibonacci(i)}"; | |
1079 // Print the result in the "Console" box. | |
1080 print(message); | |
1081 } | |
1082 '''; | |
1083 | |
1084 const String EXAMPLE_FIBONACCI_HTML = r''' | |
1085 // Go ahead and modify this example. | |
1086 | |
1087 import "dart:html"; | |
1088 | |
1089 // Computes the nth Fibonacci number. | |
1090 int fibonacci(int n) { | |
1091 if (n < 2) return n; | |
1092 return fibonacci(n - 1) + fibonacci(n - 2); | |
1093 } | |
1094 | |
1095 // Displays a Fibonacci number. | |
1096 void main() { | |
1097 int i = 20; | |
1098 String message = "fibonacci($i) = ${fibonacci(i)}"; | |
1099 | |
1100 // This example uses HTML to display the result and it will appear | |
1101 // in a nested HTML frame (an iframe). | |
1102 document.body.append(new HeadingElement.h1()..appendText(message)); | |
1103 } | |
1104 '''; | |
1105 | |
1106 const String OUTPUT_HELPER = r''' | |
1107 function dartPrint(msg) { | |
1108 window.parent.postMessage(String(msg), "*"); | |
1109 } | |
1110 | |
1111 function dartMainRunner(main) { | |
1112 main(); | |
1113 } | |
1114 | |
1115 window.onerror = function (message, url, lineNumber) { | |
1116 window.parent.postMessage( | |
1117 ["error", {message: message, url: url, lineNumber: lineNumber}], "*"); | |
1118 }; | |
1119 | |
1120 (function () { | |
1121 | |
1122 function postScrollHeight() { | |
1123 window.parent.postMessage(["scrollHeight", document.documentElement.scrollHeig ht], "*"); | |
1124 } | |
1125 | |
1126 var observer = new (window.MutationObserver||window.WebKitMutationObserver||wind ow.MozMutationObserver)(function(mutations) { | |
1127 postScrollHeight() | |
1128 window.setTimeout(postScrollHeight, 500); | |
1129 }); | |
1130 | |
1131 observer.observe( | |
1132 document.body, | |
1133 { attributes: true, | |
1134 childList: true, | |
1135 characterData: true, | |
1136 subtree: true }); | |
1137 })(); | |
1138 '''; | |
1139 | |
1140 const String EXAMPLE_SUNFLOWER = ''' | |
1141 // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file | |
1142 // for details. All rights reserved. Use of this source code is governed by a | |
1143 // BSD-style license that can be found in the LICENSE file. | |
1144 | |
1145 library sunflower; | |
1146 | |
1147 import "dart:html"; | |
1148 import "dart:math"; | |
1149 | |
1150 const String ORANGE = "orange"; | |
1151 const int SEED_RADIUS = 2; | |
1152 const int SCALE_FACTOR = 4; | |
1153 const num TAU = PI * 2; | |
1154 const int MAX_D = 300; | |
1155 const num centerX = MAX_D / 2; | |
1156 const num centerY = centerX; | |
1157 | |
1158 final InputElement slider = query("#slider"); | |
1159 final Element notes = query("#notes"); | |
1160 final num PHI = (sqrt(5) + 1) / 2; | |
1161 int seeds = 0; | |
1162 final CanvasRenderingContext2D context = | |
1163 (query("#canvas") as CanvasElement).context2D; | |
1164 | |
1165 void main() { | |
1166 document.head.append(new StyleElement()..appendText(STYLE)); | |
1167 document.body.innerHtml = BODY; | |
1168 slider.onChange.listen((e) => draw()); | |
1169 draw(); | |
1170 } | |
1171 | |
1172 /// Draw the complete figure for the current number of seeds. | |
1173 void draw() { | |
1174 seeds = int.parse(slider.value); | |
1175 context.clearRect(0, 0, MAX_D, MAX_D); | |
1176 for (var i = 0; i < seeds; i++) { | |
1177 final num theta = i * TAU / PHI; | |
1178 final num r = sqrt(i) * SCALE_FACTOR; | |
1179 drawSeed(centerX + r * cos(theta), centerY - r * sin(theta)); | |
1180 } | |
1181 notes.text = "\${seeds} seeds"; | |
1182 } | |
1183 | |
1184 /// Draw a small circle representing a seed centered at (x,y). | |
1185 void drawSeed(num x, num y) { | |
1186 context..beginPath() | |
1187 ..lineWidth = 2 | |
1188 ..fillStyle = ORANGE | |
1189 ..strokeStyle = ORANGE | |
1190 ..arc(x, y, SEED_RADIUS, 0, TAU, false) | |
1191 ..fill() | |
1192 ..closePath() | |
1193 ..stroke(); | |
1194 } | |
1195 | |
1196 const String MATH_PNG = | |
1197 "https://dart.googlecode.com/svn/trunk/dart/samples/sunflower/web/math.png"; | |
1198 const String BODY = """ | |
1199 <h1>drfibonacci\'s Sunflower Spectacular</h1> | |
1200 | |
1201 <p>A canvas 2D demo.</p> | |
1202 | |
1203 <div id="container"> | |
1204 <canvas id="canvas" width="300" height="300" class="center"></canvas> | |
1205 <form class="center"> | |
1206 <input id="slider" type="range" max="1000" value="500"/> | |
1207 </form> | |
1208 <br/> | |
1209 <img src="\$MATH_PNG" width="350px" height="42px" class="center"> | |
1210 </div> | |
1211 | |
1212 <footer> | |
1213 <p id="summary"> </p> | |
1214 <p id="notes"> </p> | |
1215 </footer> | |
1216 """; | |
1217 | |
1218 const String STYLE = r""" | |
1219 body { | |
1220 background-color: #F8F8F8; | |
1221 font-family: 'Open Sans', sans-serif; | |
1222 font-size: 14px; | |
1223 font-weight: normal; | |
1224 line-height: 1.2em; | |
1225 margin: 15px; | |
1226 } | |
1227 | |
1228 p { | |
1229 color: #333; | |
1230 } | |
1231 | |
1232 #container { | |
1233 width: 100%; | |
1234 height: 400px; | |
1235 position: relative; | |
1236 border: 1px solid #ccc; | |
1237 background-color: #fff; | |
1238 } | |
1239 | |
1240 #summary { | |
1241 float: left; | |
1242 } | |
1243 | |
1244 #notes { | |
1245 float: right; | |
1246 width: 120px; | |
1247 text-align: right; | |
1248 } | |
1249 | |
1250 .error { | |
1251 font-style: italic; | |
1252 color: red; | |
1253 } | |
1254 | |
1255 img { | |
1256 border: 1px solid #ccc; | |
1257 margin: auto; | |
1258 } | |
1259 | |
1260 .center { | |
1261 display: block; | |
1262 margin: 0px auto; | |
1263 text-align: center; | |
1264 } | |
1265 """; | |
1266 | |
1267 '''; | |
OLD | NEW |