| OLD | NEW |
| (Empty) |
| 1 // Copyright (c) 2011, 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 class CGBlock { | |
| 6 int _blockType; // Code type of this block | |
| 7 int _indent; // Number of spaces to prefix for each statement | |
| 8 bool _inEach; // This block or any currently active blocks is a | |
| 9 // #each. If so then any element marked with a | |
| 10 // var attribute is repeated therefore the var | |
| 11 // is a List type instead of an Element type. | |
| 12 String _localName; // optional local name for #each or #with | |
| 13 List<CGStatement> _stmts; | |
| 14 int localIndex; // Local variable index (e.g., e0, e1, etc.) | |
| 15 | |
| 16 // Block Types: | |
| 17 static const int CONSTRUCTOR = 0; | |
| 18 static const int EACH = 1; | |
| 19 static const int WITH = 2; | |
| 20 | |
| 21 CGBlock([this._indent = 4, | |
| 22 this._blockType = CGBlock.CONSTRUCTOR, | |
| 23 this._inEach = false, | |
| 24 this._localName = null]) : | |
| 25 _stmts = new List<CGStatement>(), localIndex = 0 { | |
| 26 assert(_blockType >= CGBlock.CONSTRUCTOR && _blockType <= CGBlock.WITH); | |
| 27 } | |
| 28 | |
| 29 bool get anyStatements => !_stmts.isEmpty; | |
| 30 bool get isConstructor => _blockType == CGBlock.CONSTRUCTOR; | |
| 31 bool get isEach => _blockType == CGBlock.EACH; | |
| 32 bool get isWith => _blockType == CGBlock.WITH; | |
| 33 | |
| 34 bool get hasLocalName => _localName != null; | |
| 35 String get localName => _localName; | |
| 36 | |
| 37 CGStatement push(var elem, var parentName, [bool exact = false]) { | |
| 38 var varName; | |
| 39 if (elem is TemplateElement && elem.hasVar) { | |
| 40 varName = elem.varName; | |
| 41 } else { | |
| 42 varName = localIndex++; | |
| 43 } | |
| 44 | |
| 45 CGStatement stmt = new CGStatement(elem, _indent, parentName, varName, | |
| 46 exact, _inEach); | |
| 47 _stmts.add(stmt); | |
| 48 | |
| 49 return stmt; | |
| 50 } | |
| 51 | |
| 52 void pop() { | |
| 53 _stmts.removeLast(); | |
| 54 } | |
| 55 | |
| 56 void add(String value) { | |
| 57 if (_stmts.last != null) { | |
| 58 _stmts.last.add(value); | |
| 59 } | |
| 60 } | |
| 61 | |
| 62 CGStatement get last => _stmts.length > 0 ? _stmts.last : null; | |
| 63 | |
| 64 /** | |
| 65 * Returns mixed list of elements marked with the var attribute. If the | |
| 66 * element is inside of a #each the name exposed is: | |
| 67 * | |
| 68 * List varName; | |
| 69 * | |
| 70 * otherwise it's: | |
| 71 * | |
| 72 * var varName; | |
| 73 * | |
| 74 * TODO(terry): For scalars var varName should be Element tag type e.g., | |
| 75 * | |
| 76 * DivElement varName; | |
| 77 */ | |
| 78 String get globalDeclarations { | |
| 79 StringBuffer buff = new StringBuffer(); | |
| 80 for (final CGStatement stmt in _stmts) { | |
| 81 buff.write(stmt.globalDeclaration()); | |
| 82 } | |
| 83 | |
| 84 return buff.toString(); | |
| 85 } | |
| 86 | |
| 87 /** | |
| 88 * List of statement constructors for each var inside a #each. | |
| 89 * | |
| 90 * ${#each products} | |
| 91 * <div var=myVar>...</div> | |
| 92 * ${/each} | |
| 93 * | |
| 94 * returns: | |
| 95 * | |
| 96 * myVar = []; | |
| 97 */ | |
| 98 String get globalInitializers { | |
| 99 StringBuffer buff = new StringBuffer(); | |
| 100 for (final CGStatement stmt in _stmts) { | |
| 101 buff.write(stmt.globalInitializers()); | |
| 102 } | |
| 103 | |
| 104 return buff.toString(); | |
| 105 } | |
| 106 | |
| 107 String get codeBody { | |
| 108 StringBuffer buff = new StringBuffer(); | |
| 109 | |
| 110 for (final CGStatement stmt in _stmts) { | |
| 111 buff.write(stmt.emitDartStatement()); | |
| 112 } | |
| 113 | |
| 114 return buff.toString(); | |
| 115 } | |
| 116 } | |
| 117 | |
| 118 class CGStatement { | |
| 119 bool _exact; // If True not HTML construct instead exact stmt | |
| 120 bool _repeating; // Stmt in a #each this block or nested block. | |
| 121 StringBuffer _buff; | |
| 122 var _elem; | |
| 123 int _indent; | |
| 124 var parentName; | |
| 125 String varName; | |
| 126 bool _globalVariable; | |
| 127 bool _closed; | |
| 128 | |
| 129 CGStatement(this._elem, this._indent, this.parentName, var varNameOrIndex, | |
| 130 [this._exact = false, this._repeating = false]) : | |
| 131 _buff = new StringBuffer(), _closed = false { | |
| 132 | |
| 133 if (varNameOrIndex is String) { | |
| 134 // We have the global variable name | |
| 135 varName = varNameOrIndex; | |
| 136 _globalVariable = true; | |
| 137 } else { | |
| 138 // local index generate local variable name. | |
| 139 varName = "e${varNameOrIndex}"; | |
| 140 _globalVariable = false; | |
| 141 } | |
| 142 } | |
| 143 | |
| 144 bool get hasGlobalVariable => _globalVariable; | |
| 145 String get variableName => varName; | |
| 146 | |
| 147 String globalDeclaration() { | |
| 148 if (hasGlobalVariable) { | |
| 149 String spaces = Codegen.spaces(_indent); | |
| 150 return (_repeating) ? | |
| 151 " List ${varName}; // Repeated elements.\n" : " var ${varName};\n"; | |
| 152 } | |
| 153 | |
| 154 return ""; | |
| 155 } | |
| 156 | |
| 157 String globalInitializers() { | |
| 158 if (hasGlobalVariable && _repeating) { | |
| 159 return " ${varName} = [];\n"; | |
| 160 } | |
| 161 | |
| 162 return ""; | |
| 163 } | |
| 164 | |
| 165 void add(String value) { | |
| 166 _buff.write(value); | |
| 167 } | |
| 168 | |
| 169 bool get isClosed => _closed; | |
| 170 | |
| 171 void close() { | |
| 172 if (_elem is TemplateElement && _elem.scoped) { | |
| 173 add("</${_elem.tagName}>"); | |
| 174 } | |
| 175 _closed = true; | |
| 176 } | |
| 177 | |
| 178 String emitDartStatement() { | |
| 179 StringBuffer statement = new StringBuffer(); | |
| 180 | |
| 181 String spaces = Codegen.spaces(_indent); | |
| 182 | |
| 183 if (_exact) { | |
| 184 statement.add("${spaces}${_buff.toString()};\n"); | |
| 185 } else { | |
| 186 String localVar = ""; | |
| 187 String tmpRepeat; | |
| 188 if (hasGlobalVariable) { | |
| 189 if (_repeating) { | |
| 190 tmpRepeat = "tmp_${varName}"; | |
| 191 localVar = "var "; | |
| 192 } | |
| 193 } else { | |
| 194 localVar = "var "; | |
| 195 } | |
| 196 | |
| 197 /* Emiting the following code fragment where varName is the attribute | |
| 198 value for var= | |
| 199 | |
| 200 varName = new Element.html('HTML GOES HERE'); | |
| 201 parent.elements.add(varName); | |
| 202 | |
| 203 for repeating elements in a #each: | |
| 204 | |
| 205 var tmp_nnn = new Element.html('HTML GOES HERE'); | |
| 206 varName.add(tmp_nnn); | |
| 207 parent.elements.add(tmp_nnn); | |
| 208 | |
| 209 for elements w/o var attribute set: | |
| 210 | |
| 211 var eNNN = new Element.html('HTML GOES HERE'); | |
| 212 parent.elements.add(eNNN); | |
| 213 */ | |
| 214 if (_elem is TemplateCall) { | |
| 215 // Call template NameEntry2 | |
| 216 String cls = _elem.toCall; | |
| 217 String params = _elem.params; | |
| 218 statement.add("\n${spaces}// Call template ${cls}.\n"); | |
| 219 statement.add( | |
| 220 "${spaces}${localVar}${varName} = new ${cls}${params};\n"); | |
| 221 statement.add( | |
| 222 "${spaces}${parentName}.elements.add(${varName}.root);\n"); | |
| 223 } else { | |
| 224 bool isTextNode = _elem is TemplateText; | |
| 225 String createType = isTextNode ? "Text" : "Element.html"; | |
| 226 if (tmpRepeat == null) { | |
| 227 statement.add("${spaces}${localVar}${varName} = new ${createType}('"); | |
| 228 } else { | |
| 229 statement.add( | |
| 230 "${spaces}${localVar}${tmpRepeat} = new ${createType}('"); | |
| 231 } | |
| 232 statement.add(isTextNode ? _buff.toString().trim() : _buff.toString()); | |
| 233 | |
| 234 if (tmpRepeat == null) { | |
| 235 statement.add( | |
| 236 "');\n${spaces}${parentName}.elements.add(${varName});\n"); | |
| 237 } else { | |
| 238 statement.add( | |
| 239 "');\n${spaces}${parentName}.elements.add(${tmpRepeat});\n"); | |
| 240 statement.add("${spaces}${varName}.add(${tmpRepeat});\n"); | |
| 241 } | |
| 242 } | |
| 243 } | |
| 244 | |
| 245 return statement.toString(); | |
| 246 } | |
| 247 } | |
| 248 | |
| 249 class Codegen { | |
| 250 static const String SPACES = " "; | |
| 251 static String spaces(int numSpaces) { | |
| 252 return SPACES.substring(0, numSpaces); | |
| 253 } | |
| 254 | |
| 255 // TODO(terry): Before generating Dart class need a validate phase that | |
| 256 // checks mangles all class names to be prefix with the | |
| 257 // template name to avoid any class name collisions. Also, | |
| 258 // investigate possible runtime check mode to insure that only | |
| 259 // valid CSS class names are used (in case someone uses strings | |
| 260 // and not the generated getters to the CSS class selector. This | |
| 261 // mode would be slower would require that every class name set | |
| 262 // (maybe jQuery too) is for a particular view (requires walking | |
| 263 // the HTML tree looking for a parent template prefix that | |
| 264 // matches the CSS prefix. (more thinking needed). | |
| 265 static String generate(List<Template> templates, String filename) { | |
| 266 List<String> fileParts = filename.split('.'); | |
| 267 assert(fileParts.length == 2); | |
| 268 filename = fileParts[0]; | |
| 269 | |
| 270 StringBuffer buff = new StringBuffer(); | |
| 271 int injectId = 0; // Inject function id | |
| 272 | |
| 273 buff.write("// Generated Dart class from HTML template.\n"); | |
| 274 buff.write("// DO NOT EDIT.\n\n"); | |
| 275 | |
| 276 String addStylesheetFuncName = "add_${filename}_templatesStyles"; | |
| 277 | |
| 278 for (final template in templates) { | |
| 279 // Emit the template class. | |
| 280 TemplateSignature sig = template.signature; | |
| 281 buff.write(_emitClass(sig.name, sig.params, template.content, | |
| 282 addStylesheetFuncName)); | |
| 283 } | |
| 284 | |
| 285 // TODO(terry): Stylesheet aggregator should not be global needs to be | |
| 286 // bound to this template file not global to the app. | |
| 287 | |
| 288 // Emit the stylesheet aggregator. | |
| 289 buff.write("\n\n// Inject all templates stylesheet once into the head.\n"); | |
| 290 buff.write("bool ${filename}_stylesheet_added = false;\n"); | |
| 291 buff.write("void ${addStylesheetFuncName}() {\n"); | |
| 292 buff.write(" if (!${filename}_stylesheet_added) {\n"); | |
| 293 buff.write(" StringBuffer styles = new StringBuffer();\n\n"); | |
| 294 | |
| 295 buff.write(" // All templates stylesheet.\n"); | |
| 296 | |
| 297 for (final template in templates) { | |
| 298 TemplateSignature sig = template.signature; | |
| 299 buff.write(" styles.add(${sig.name}.stylesheet);\n"); | |
| 300 } | |
| 301 | |
| 302 buff.write("\n ${filename}_stylesheet_added = true;\n"); | |
| 303 | |
| 304 buff.write(" document.head.elements.add(new Element.html('<style>" | |
| 305 "\${styles.toString()}</style>'));\n"); | |
| 306 buff.write(" }\n"); | |
| 307 buff.write("}\n"); | |
| 308 | |
| 309 return buff.toString(); | |
| 310 } | |
| 311 | |
| 312 static String _emitCSSSelectors(css.Stylesheet stylesheet) { | |
| 313 if (stylesheet == null) { | |
| 314 return ""; | |
| 315 } | |
| 316 | |
| 317 List<String> classes = []; | |
| 318 | |
| 319 for (final production in stylesheet.topLevels) { | |
| 320 if (production is css.IncludeDirective) { | |
| 321 for (final topLevel in production.styleSheet.topLevels) { | |
| 322 if (topLevel is css.RuleSet) { | |
| 323 classes = css.Generate.computeClassSelectors(topLevel, classes); | |
| 324 } | |
| 325 } | |
| 326 } else if (production is css.RuleSet) { | |
| 327 classes = css.Generate.computeClassSelectors(production, classes); | |
| 328 } | |
| 329 } | |
| 330 | |
| 331 List<String> dartNames = []; | |
| 332 | |
| 333 for (final String knownClass in classes) { | |
| 334 StringBuffer dartName = new StringBuffer(); | |
| 335 List<String> splits = knownClass.split('-'); | |
| 336 if (splits.length > 0) { | |
| 337 dartName.add(splits[0]); | |
| 338 for (int idx = 1; idx < splits.length; idx++) { | |
| 339 String part = splits[idx]; | |
| 340 // Character between 'a'..'z' mapped to 'A'..'Z' | |
| 341 dartName.add("${part[0].toUpperCase()}${part.substring(1)}"); | |
| 342 } | |
| 343 dartNames.add(dartName.toString()); | |
| 344 } | |
| 345 } | |
| 346 | |
| 347 StringBuffer buff = new StringBuffer(); | |
| 348 if (classes.length > 0) { | |
| 349 assert(classes.length == dartNames.length); | |
| 350 buff.write("\n // CSS class selectors for this template.\n"); | |
| 351 for (int i = 0; i < classes.length; i++) { | |
| 352 buff.write( | |
| 353 " static String get ${dartNames[i]} => \"${classes[i]}\";\n"); | |
| 354 } | |
| 355 } | |
| 356 | |
| 357 return buff.toString(); | |
| 358 } | |
| 359 | |
| 360 static String _emitClass(String className, | |
| 361 List<Map<Identifier, Identifier>> params, | |
| 362 TemplateContent content, | |
| 363 String addStylesheetFuncName) { | |
| 364 StringBuffer buff = new StringBuffer(); | |
| 365 | |
| 366 // Emit the template class. | |
| 367 buff.write("class ${className} {\n"); | |
| 368 | |
| 369 buff.write(" Map<String, Object> _scopes;\n"); | |
| 370 buff.write(" Element _fragment;\n\n"); | |
| 371 | |
| 372 bool anyParams = false; | |
| 373 for (final param in params) { | |
| 374 buff.write(" ${param['type']} ${param['name']};\n"); | |
| 375 anyParams = true; | |
| 376 } | |
| 377 if (anyParams) buff.write("\n"); | |
| 378 | |
| 379 ElemCG ecg = new ElemCG(); | |
| 380 | |
| 381 if (!ecg.pushBlock()) { | |
| 382 world.error("Error at ${content}"); | |
| 383 } | |
| 384 | |
| 385 var root = content.html.children.length > 0 ? content.html.children[0] : | |
| 386 content.html; | |
| 387 bool firstTime = true; | |
| 388 for (var child in root.children) { | |
| 389 if (child is TemplateText) { | |
| 390 if (!firstTime) { | |
| 391 ecg.closeStatement(); | |
| 392 } | |
| 393 | |
| 394 String textNodeValue = child.value.trim(); | |
| 395 if (textNodeValue.length > 0) { | |
| 396 CGStatement stmt = ecg.pushStatement(child, "_fragment"); | |
| 397 } | |
| 398 } | |
| 399 ecg.emitConstructHtml(child, "", "_fragment"); | |
| 400 firstTime = false; | |
| 401 } | |
| 402 | |
| 403 // Create all element names marked with var. | |
| 404 String decls = ecg.globalDeclarations; | |
| 405 if (decls.length > 0) { | |
| 406 buff.write("\n // Elements bound to a variable:\n"); | |
| 407 buff.write("${decls}\n"); | |
| 408 } | |
| 409 | |
| 410 // Create the constructor. | |
| 411 buff.write(" ${className}("); | |
| 412 bool firstParam = true; | |
| 413 for (final param in params) { | |
| 414 if (!firstParam) { | |
| 415 buff.write(", "); | |
| 416 } | |
| 417 buff.write("this.${param['name']}"); | |
| 418 firstParam = false; | |
| 419 } | |
| 420 buff.write(") : _scopes = new Map<String, Object>() {\n"); | |
| 421 | |
| 422 String initializers = ecg.globalInitializers; | |
| 423 if (initializers.length > 0) { | |
| 424 buff.write(" //Global initializers.\n"); | |
| 425 buff.write("${initializers}\n"); | |
| 426 } | |
| 427 | |
| 428 buff.write(" // Insure stylesheet for template exist in the document.\n")
; | |
| 429 buff.write(" ${addStylesheetFuncName}();\n\n"); | |
| 430 | |
| 431 buff.write(" _fragment = new DocumentFragment();\n"); | |
| 432 | |
| 433 buff.write(ecg.codeBody); // HTML for constructor to build. | |
| 434 | |
| 435 buff.write(" }\n\n"); // End constructor | |
| 436 | |
| 437 buff.write(emitGetters(content.getters)); | |
| 438 | |
| 439 buff.write(" Element get root => _fragment;\n"); | |
| 440 | |
| 441 // Emit all CSS class selectors: | |
| 442 buff.write(_emitCSSSelectors(content.css)); | |
| 443 | |
| 444 // Emit the injection functions. | |
| 445 buff.write("\n // Injection functions:"); | |
| 446 for (final expr in ecg.expressions) { | |
| 447 buff.write("${expr}"); | |
| 448 } | |
| 449 | |
| 450 buff.write("\n // Each functions:\n"); | |
| 451 for (var eachFunc in ecg.eachs) { | |
| 452 buff.write("${eachFunc}\n"); | |
| 453 } | |
| 454 | |
| 455 buff.write("\n // With functions:\n"); | |
| 456 for (var withFunc in ecg.withs) { | |
| 457 buff.write("${withFunc}\n"); | |
| 458 } | |
| 459 | |
| 460 buff.write("\n // CSS for this template.\n"); | |
| 461 buff.write(" static const String stylesheet = "); | |
| 462 | |
| 463 if (content.css != null) { | |
| 464 buff.write("\'\'\'\n ${content.css.toString()}\n"); | |
| 465 buff.write(" \'\'\';\n\n"); | |
| 466 | |
| 467 // TODO(terry): Emit all known selectors for this template. | |
| 468 buff.write(" // Stylesheet class selectors:\n"); | |
| 469 } else { | |
| 470 buff.write("\"\";\n"); | |
| 471 } | |
| 472 | |
| 473 buff.write(" String safeHTML(String html) {\n"); | |
| 474 buff.write(" // TODO(terry): Escaping for XSS vulnerabilities TBD.\n"); | |
| 475 buff.write(" return html;\n"); | |
| 476 buff.write(" }\n"); | |
| 477 | |
| 478 buff.write("}\n"); // End class | |
| 479 | |
| 480 return buff.toString(); | |
| 481 } | |
| 482 | |
| 483 // TODO(terry): Need to generate function to inject any TemplateExpressions | |
| 484 // to call SafeHTML wrapper. | |
| 485 static String emitGetters(List<TemplateGetter> getters) { | |
| 486 StringBuffer buff = new StringBuffer(); | |
| 487 for (final TemplateGetter getter in getters) { | |
| 488 buff.write(' String ${getter.getterSignatureAsString()} {\n'); | |
| 489 buff.write(' return \'\'\''); | |
| 490 var docFrag = getter.docFrag.children[0]; | |
| 491 for (final child in docFrag.children) { | |
| 492 buff.write(child.toString().trim()); | |
| 493 } | |
| 494 buff.write('\'\'\';\n'); | |
| 495 buff.write(' }\n\n'); | |
| 496 } | |
| 497 | |
| 498 return buff.toString(); | |
| 499 } | |
| 500 } | |
| 501 | |
| 502 class ElemCG { | |
| 503 // List of identifiers and quoted strings (single and double quoted). | |
| 504 var identRe = new RegExp( | |
| 505 "\s*('\"\\'\\\"[^'\"\\'\\\"]+'\"\\'\\\"|[_A-Za-z][_A-Za-z0-9]*)"); | |
| 506 | |
| 507 List<CGBlock> _cgBlocks; | |
| 508 StringBuffer _globalDecls; // Global var declarations for all blocks. | |
| 509 StringBuffer _globalInits; // Global List var initializtion for all | |
| 510 // blocks in a #each. | |
| 511 String funcCall; // Func call after element creation? | |
| 512 List<String> expressions; // List of injection function declarations. | |
| 513 List<String> eachs; // List of each function declarations. | |
| 514 List<String> withs; // List of with function declarations. | |
| 515 | |
| 516 ElemCG() : | |
| 517 expressions = [], | |
| 518 eachs = [], | |
| 519 withs = [], | |
| 520 _cgBlocks = [], | |
| 521 _globalDecls = new StringBuffer(), | |
| 522 _globalInits = new StringBuffer(); | |
| 523 | |
| 524 bool get isLastBlockConstructor { | |
| 525 CGBlock block = _cgBlocks.last; | |
| 526 return block.isConstructor; | |
| 527 } | |
| 528 | |
| 529 List<String> activeBlocksLocalNames() { | |
| 530 List<String> result = []; | |
| 531 | |
| 532 for (final CGBlock block in _cgBlocks) { | |
| 533 if (block.isEach || block.isWith) { | |
| 534 if (block.hasLocalName) { | |
| 535 result.add(block.localName); | |
| 536 } | |
| 537 } | |
| 538 } | |
| 539 | |
| 540 return result; | |
| 541 } | |
| 542 | |
| 543 /** | |
| 544 * Active block with this localName. | |
| 545 */ | |
| 546 bool matchBlocksLocalName(String name) { | |
| 547 for (final CGBlock block in _cgBlocks) { | |
| 548 if (block.isEach || block.isWith) { | |
| 549 if (block.hasLocalName && block.localName == name) { | |
| 550 return true; | |
| 551 } | |
| 552 } | |
| 553 } | |
| 554 | |
| 555 return false; | |
| 556 } | |
| 557 | |
| 558 /** | |
| 559 * Any active blocks? | |
| 560 */ | |
| 561 bool isNestedBlock() { | |
| 562 for (final CGBlock block in _cgBlocks) { | |
| 563 if (block.isEach || block.isWith) { | |
| 564 return true; | |
| 565 } | |
| 566 } | |
| 567 | |
| 568 return false; | |
| 569 } | |
| 570 | |
| 571 /** | |
| 572 * Any active blocks with localName? | |
| 573 */ | |
| 574 bool isNestedNamedBlock() { | |
| 575 for (final CGBlock block in _cgBlocks) { | |
| 576 if ((block.isEach || block.isWith) && block.hasLocalName) { | |
| 577 return true; | |
| 578 } | |
| 579 } | |
| 580 | |
| 581 return false; | |
| 582 } | |
| 583 | |
| 584 // Any current active #each blocks. | |
| 585 bool anyEachBlocks(int blockToCreateType) { | |
| 586 bool result = blockToCreateType == CGBlock.EACH; | |
| 587 | |
| 588 for (final CGBlock block in _cgBlocks) { | |
| 589 if (block.isEach) { | |
| 590 result = result || true; | |
| 591 } | |
| 592 } | |
| 593 | |
| 594 return result; | |
| 595 } | |
| 596 | |
| 597 bool pushBlock([int indent = 4, int blockType = CGBlock.CONSTRUCTOR, | |
| 598 String itemName = null]) { | |
| 599 if (itemName != null && matchBlocksLocalName(itemName)) { | |
| 600 world.error("Active block already exist with local name: ${itemName}."); | |
| 601 return false; | |
| 602 } else if (itemName == null && this.isNestedBlock()) { | |
| 603 world.error(''' | |
| 604 Nested #each or #with must have a localName; | |
| 605 \n #each list [localName]\n #with object [localName]'''); | |
| 606 return false; | |
| 607 } | |
| 608 _cgBlocks.add( | |
| 609 new CGBlock(indent, blockType, anyEachBlocks(blockType), itemName)); | |
| 610 | |
| 611 return true; | |
| 612 } | |
| 613 | |
| 614 void popBlock() { | |
| 615 _globalDecls.add(lastBlock.globalDeclarations); | |
| 616 _globalInits.add(lastBlock.globalInitializers); | |
| 617 _cgBlocks.removeLast(); | |
| 618 } | |
| 619 | |
| 620 CGStatement pushStatement(var elem, var parentName) { | |
| 621 return lastBlock.push(elem, parentName, false); | |
| 622 } | |
| 623 | |
| 624 CGStatement pushExactStatement(var elem, var parentName) { | |
| 625 return lastBlock.push(elem, parentName, true); | |
| 626 } | |
| 627 | |
| 628 bool get isClosedStatement { | |
| 629 return (lastBlock != null && lastBlock.last != null) ? | |
| 630 lastBlock.last.isClosed : false; | |
| 631 } | |
| 632 | |
| 633 void closeStatement() { | |
| 634 if (lastBlock != null && lastBlock.last != null && | |
| 635 !lastBlock.last.isClosed) { | |
| 636 lastBlock.last.close(); | |
| 637 } | |
| 638 } | |
| 639 | |
| 640 String get lastVariableName { | |
| 641 if (lastBlock != null && lastBlock.last != null) { | |
| 642 return lastBlock.last.variableName; | |
| 643 } | |
| 644 } | |
| 645 | |
| 646 String get lastParentName { | |
| 647 if (lastBlock != null && lastBlock.last != null) { | |
| 648 return lastBlock.last.parentName; | |
| 649 } | |
| 650 } | |
| 651 | |
| 652 CGBlock get lastBlock => _cgBlocks.length > 0 ? _cgBlocks.last : null; | |
| 653 | |
| 654 void add(String str) { | |
| 655 _cgBlocks.last.add(str); | |
| 656 } | |
| 657 | |
| 658 String get globalDeclarations { | |
| 659 assert(_cgBlocks.length == 1); // Only constructor body should be left. | |
| 660 _globalDecls.add(lastBlock.globalDeclarations); | |
| 661 return _globalDecls.toString(); | |
| 662 } | |
| 663 | |
| 664 String get globalInitializers { | |
| 665 assert(_cgBlocks.length == 1); // Only constructor body should be left. | |
| 666 _globalInits.add(lastBlock.globalInitializers); | |
| 667 return _globalInits.toString(); | |
| 668 } | |
| 669 | |
| 670 String get codeBody { | |
| 671 closeStatement(); | |
| 672 return _cgBlocks.last.codeBody; | |
| 673 } | |
| 674 | |
| 675 /* scopeName for expression | |
| 676 * parentVarOrIndex if # it's a local variable if string it's an exposed | |
| 677 * name (specified by the var attribute) for this element. | |
| 678 * | |
| 679 */ | |
| 680 emitElement(var elem, | |
| 681 [String scopeName = "", | |
| 682 var parentVarOrIdx = 0, | |
| 683 bool immediateNestedEach = false]) { | |
| 684 if (elem is TemplateElement) { | |
| 685 if (!elem.isFragment) { | |
| 686 add("<${elem.tagName}${elem.attributesToString()}>"); | |
| 687 } | |
| 688 String prevParent = lastVariableName; | |
| 689 for (var childElem in elem.children) { | |
| 690 if (childElem is TemplateElement) { | |
| 691 closeStatement(); | |
| 692 if (childElem.hasVar) { | |
| 693 emitConstructHtml(childElem, scopeName, prevParent, | |
| 694 childElem.varName); | |
| 695 } else { | |
| 696 emitConstructHtml(childElem, scopeName, prevParent); | |
| 697 } | |
| 698 closeStatement(); | |
| 699 } else { | |
| 700 emitElement(childElem, scopeName, parentVarOrIdx); | |
| 701 } | |
| 702 } | |
| 703 | |
| 704 // Close this tag. | |
| 705 closeStatement(); | |
| 706 } else if (elem is TemplateText) { | |
| 707 String outputValue = elem.value.trim(); | |
| 708 if (outputValue.length > 0) { | |
| 709 bool emitTextNode = false; | |
| 710 if (isClosedStatement) { | |
| 711 String prevParent = lastParentName; | |
| 712 CGStatement stmt = pushStatement(elem, prevParent); | |
| 713 emitTextNode = true; | |
| 714 } | |
| 715 | |
| 716 // TODO(terry): Need to interpolate following: | |
| 717 // {sp} → space | |
| 718 // {nil} → empty string | |
| 719 // {\r} → carriage return | |
| 720 // {\n} → new line (line feed) | |
| 721 // {\t} → tab | |
| 722 // {lb} → left brace | |
| 723 // {rb} → right brace | |
| 724 | |
| 725 add("${outputValue}"); // remove leading/trailing whitespace. | |
| 726 | |
| 727 if (emitTextNode) { | |
| 728 closeStatement(); | |
| 729 } | |
| 730 } | |
| 731 } else if (elem is TemplateExpression) { | |
| 732 emitExpressions(elem, scopeName); | |
| 733 } else if (elem is TemplateEachCommand) { | |
| 734 // Signal to caller new block coming in, returns "each_" prefix | |
| 735 emitEach(elem, "List", elem.listName.name, "parent", immediateNestedEach, | |
| 736 elem.hasLoopItem ? elem.loopItem.name : null); | |
| 737 } else if (elem is TemplateWithCommand) { | |
| 738 // Signal to caller new block coming in, returns "each_" prefix | |
| 739 emitWith(elem, "var", elem.objectName.name, "parent", | |
| 740 elem.hasBlockItem ? elem.blockItem.name : null); | |
| 741 } else if (elem is TemplateCall) { | |
| 742 emitCall(elem, parentVarOrIdx); | |
| 743 } | |
| 744 } | |
| 745 | |
| 746 // TODO(terry): Hack prefixing all names with "${scopeName}." but don't touch | |
| 747 // quoted strings. | |
| 748 String _resolveNames(String expr, String prefixPart) { | |
| 749 StringBuffer newExpr = new StringBuffer(); | |
| 750 Iterable<Match> matches = identRe.allMatches(expr); | |
| 751 | |
| 752 int lastIdx = 0; | |
| 753 for (Match m in matches) { | |
| 754 if (m.start > lastIdx) { | |
| 755 newExpr.add(expr.substring(lastIdx, m.start)); | |
| 756 } | |
| 757 | |
| 758 bool identifier = true; | |
| 759 if (m.start > 0) { | |
| 760 int charCode = expr.codeUnitAt(m.start - 1); | |
| 761 // Starts with ' or " then it's not an identifier. | |
| 762 identifier = charCode != 34 /* " */ && charCode != 39 /* ' */; | |
| 763 } | |
| 764 | |
| 765 String strMatch = expr.substring(m.start, m.end); | |
| 766 if (identifier) { | |
| 767 newExpr.add("${prefixPart}.${strMatch}"); | |
| 768 } else { | |
| 769 // Quoted string don't touch. | |
| 770 newExpr.add("${strMatch}"); | |
| 771 } | |
| 772 lastIdx = m.end; | |
| 773 } | |
| 774 | |
| 775 if (expr.length > lastIdx) { | |
| 776 newExpr.add(expr.substring(lastIdx)); | |
| 777 } | |
| 778 | |
| 779 return newExpr.toString(); | |
| 780 } | |
| 781 | |
| 782 /** | |
| 783 * Construct the HTML each top-level node get's it's own variable. | |
| 784 * | |
| 785 * TODO(terry): Might want to optimize if the other top-level nodes have no | |
| 786 * control structures (with, each, if, etc.). We could | |
| 787 * synthesize a root node and create all the top-level nodes | |
| 788 * under the root node with one innerHTML. | |
| 789 */ | |
| 790 void emitConstructHtml(var elem, | |
| 791 [String scopeName = "", | |
| 792 String parentName = "parent", | |
| 793 var varIndex = 0, | |
| 794 bool immediateNestedEach = false]) { | |
| 795 if (elem is TemplateElement) { | |
| 796 CGStatement stmt = pushStatement(elem, parentName); | |
| 797 emitElement(elem, scopeName, stmt.hasGlobalVariable ? | |
| 798 stmt.variableName : varIndex); | |
| 799 } else { | |
| 800 emitElement(elem, scopeName, varIndex, immediateNestedEach); | |
| 801 } | |
| 802 } | |
| 803 | |
| 804 /* Any references to products.sales needs to be remaped to item.sales | |
| 805 * for now it's a hack look for first dot and replace with item. | |
| 806 */ | |
| 807 String eachIterNameToItem(String iterName) { | |
| 808 String newName = iterName; | |
| 809 var dotForIter = iterName.indexOf('.'); | |
| 810 if (dotForIter >= 0) { | |
| 811 newName = "_item${iterName.substring(dotForIter)}"; | |
| 812 } | |
| 813 | |
| 814 return newName; | |
| 815 } | |
| 816 | |
| 817 emitExpressions(TemplateExpression elem, String scopeName) { | |
| 818 StringBuffer func = new StringBuffer(); | |
| 819 | |
| 820 String newExpr = elem.expression; | |
| 821 bool anyNesting = isNestedNamedBlock(); | |
| 822 if (scopeName.length > 0 && !anyNesting) { | |
| 823 // In a block #command need the scope passed in. | |
| 824 add("\$\{inject_${expressions.length}(_item)\}"); | |
| 825 func.add("\n String inject_${expressions.length}(var _item) {\n"); | |
| 826 // Escape all single-quotes, this expression is embedded as a string | |
| 827 // parameter for the call to safeHTML. | |
| 828 newExpr = _resolveNames(newExpr.replaceAll("'", "\\'"), "_item"); | |
| 829 } else { | |
| 830 // Not in a block #command item isn't passed in. | |
| 831 add("\$\{inject_${expressions.length}()\}"); | |
| 832 func.add("\n String inject_${expressions.length}() {\n"); | |
| 833 | |
| 834 if (anyNesting) { | |
| 835 func.add(defineScopes()); | |
| 836 } | |
| 837 } | |
| 838 | |
| 839 // Construct the active scope names for name resolution. | |
| 840 | |
| 841 func.add(" return safeHTML('\$\{${newExpr}\}');\n"); | |
| 842 func.add(" }\n"); | |
| 843 | |
| 844 expressions.add(func.toString()); | |
| 845 } | |
| 846 | |
| 847 emitCall(TemplateCall elem, String scopeName) { | |
| 848 pushStatement(elem, scopeName); | |
| 849 } | |
| 850 | |
| 851 emitEach(TemplateEachCommand elem, String iterType, String iterName, | |
| 852 var parentVarOrIdx, bool nestedImmediateEach, [String itemName = null]) { | |
| 853 TemplateDocument docFrag = elem.documentFragment; | |
| 854 | |
| 855 int eachIndex = eachs.length; | |
| 856 eachs.add(""); | |
| 857 | |
| 858 StringBuffer funcBuff = new StringBuffer(); | |
| 859 // Prepare function call "each_N(iterName," parent param computed later. | |
| 860 String funcName = "each_${eachIndex}"; | |
| 861 | |
| 862 funcBuff.add(" ${funcName}(${iterType} items, Element parent) {\n"); | |
| 863 | |
| 864 String paramName = injectParamName(itemName); | |
| 865 if (paramName == null) { | |
| 866 world.error("Use a different local name; ${itemName} is reserved."); | |
| 867 } | |
| 868 funcBuff.add(" for (var ${paramName} in items) {\n"); | |
| 869 | |
| 870 if (!pushBlock(6, CGBlock.EACH, itemName)) { | |
| 871 world.error("Error at ${elem}"); | |
| 872 } | |
| 873 | |
| 874 addScope(6, funcBuff, itemName); | |
| 875 | |
| 876 TemplateElement docFragChild = docFrag.children[0]; | |
| 877 var children = docFragChild.isFragment ? | |
| 878 docFragChild.children : docFrag.children; | |
| 879 for (var child in children) { | |
| 880 // If any immediate children of the parent #each is an #each then | |
| 881 // so we need to pass the outer #each parent not the last statement's | |
| 882 // variableName when calling the nested #each. | |
| 883 bool eachChild = (child is TemplateEachCommand); | |
| 884 emitConstructHtml(child, iterName, parentVarOrIdx, 0, eachChild); | |
| 885 } | |
| 886 | |
| 887 funcBuff.add(codeBody); | |
| 888 | |
| 889 removeScope(6, funcBuff, itemName); | |
| 890 | |
| 891 popBlock(); | |
| 892 | |
| 893 funcBuff.add(" }\n"); | |
| 894 funcBuff.add(" }\n"); | |
| 895 | |
| 896 eachs[eachIndex] = funcBuff.toString(); | |
| 897 | |
| 898 // If nested each then we want to pass the parent otherwise we'll use the | |
| 899 // varName. | |
| 900 var varName = nestedImmediateEach ? "parent" : lastBlockVarName; | |
| 901 | |
| 902 pushExactStatement(elem, parentVarOrIdx); | |
| 903 | |
| 904 // Setup call to each func as "each_n(xxxxx, " the parent param is filled | |
| 905 // in later when we known the parent variable. | |
| 906 String eachParam = | |
| 907 (itemName == null) ? eachIterNameToItem(iterName) : iterName; | |
| 908 add("${funcName}(${eachParam}, ${varName})"); | |
| 909 } | |
| 910 | |
| 911 emitWith(TemplateWithCommand elem, String withType, String withName, | |
| 912 var parentVarIndex, [String itemName = null]) { | |
| 913 TemplateDocument docFrag = elem.documentFragment; | |
| 914 | |
| 915 int withIndex = withs.length; | |
| 916 withs.add(""); | |
| 917 | |
| 918 StringBuffer funcBuff = new StringBuffer(); | |
| 919 // Prepare function call "each_N(iterName," parent param computed later. | |
| 920 String funcName = "with_${withIndex}"; | |
| 921 | |
| 922 String paramName = injectParamName(itemName); | |
| 923 if (paramName == null) { | |
| 924 world.error("Use a different local name; ${itemName} is reserved."); | |
| 925 } | |
| 926 funcBuff.add(" ${funcName}(${withType} ${paramName}, Element parent) {\n"); | |
| 927 | |
| 928 if (!pushBlock(4, CGBlock.WITH, itemName)) { | |
| 929 world.error("Error at ${elem}"); | |
| 930 } | |
| 931 | |
| 932 TemplateElement docFragChild = docFrag.children[0]; | |
| 933 var children = docFragChild.isFragment ? | |
| 934 docFragChild.children : docFrag.children; | |
| 935 for (var child in children) { | |
| 936 emitConstructHtml(child, withName, "parent"); | |
| 937 } | |
| 938 | |
| 939 addScope(4, funcBuff, itemName); | |
| 940 funcBuff.add(codeBody); | |
| 941 removeScope(4, funcBuff, itemName); | |
| 942 | |
| 943 popBlock(); | |
| 944 | |
| 945 funcBuff.add(" }\n"); | |
| 946 | |
| 947 withs[withIndex] = funcBuff.toString(); | |
| 948 | |
| 949 // Compute parent node variable before pushing with statement. | |
| 950 String parentVarName = lastBlockVarName; | |
| 951 | |
| 952 pushExactStatement(elem, parentVarIndex); | |
| 953 | |
| 954 // Setup call to each func as "each_n(xxxxx, " the parent param is filled | |
| 955 // in later when we known the parent variable. | |
| 956 add("${funcName}(${withName}, ${parentVarName})"); | |
| 957 } | |
| 958 | |
| 959 String get lastBlockVarName { | |
| 960 var varName; | |
| 961 if (lastBlock != null && lastBlock.anyStatements) { | |
| 962 varName = lastBlock.last.variableName; | |
| 963 } else { | |
| 964 varName = "_fragment"; | |
| 965 } | |
| 966 | |
| 967 return varName; | |
| 968 } | |
| 969 | |
| 970 String injectParamName(String name) { | |
| 971 // Local name _item is reserved. | |
| 972 if (name != null && name == "_item") { | |
| 973 return null; // Local name is not valid. | |
| 974 } | |
| 975 | |
| 976 return (name == null) ? "_item" : name; | |
| 977 } | |
| 978 | |
| 979 addScope(int indent, StringBuffer buff, String item) { | |
| 980 String spaces = Codegen.spaces(indent); | |
| 981 | |
| 982 if (item == null) { | |
| 983 item = "_item"; | |
| 984 } | |
| 985 buff.write("${spaces}_scopes[\"${item}\"] = ${item};\n"); | |
| 986 } | |
| 987 | |
| 988 removeScope(int indent, StringBuffer buff, String item) { | |
| 989 String spaces = Codegen.spaces(indent); | |
| 990 | |
| 991 if (item == null) { | |
| 992 item = "_item"; | |
| 993 } | |
| 994 buff.write("${spaces}_scopes.remove(\"${item}\");\n"); | |
| 995 } | |
| 996 | |
| 997 String defineScopes() { | |
| 998 StringBuffer buff = new StringBuffer(); | |
| 999 | |
| 1000 // Construct the active scope names for name resolution. | |
| 1001 List<String> names = activeBlocksLocalNames(); | |
| 1002 if (names.length > 0) { | |
| 1003 buff.write(" // Local scoped block names.\n"); | |
| 1004 for (String name in names) { | |
| 1005 buff.write(" var ${name} = _scopes[\"${name}\"];\n"); | |
| 1006 } | |
| 1007 buff.write("\n"); | |
| 1008 } | |
| 1009 | |
| 1010 return buff.toString(); | |
| 1011 } | |
| 1012 | |
| 1013 } | |
| OLD | NEW |