OLD | NEW |
1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file | 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 | 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. | 3 // BSD-style license that can be found in the LICENSE file. |
4 | 4 |
5 part of polymer; | 5 part of polymer; |
6 | 6 |
7 /** | 7 /// **Warning**: this class is experiental and subject to change. |
8 * **Warning**: this class is experiental and subject to change. | 8 /// |
9 * | 9 /// The implementation for the `polymer-element` element. |
10 * The implementation for the `polymer-element` element. | 10 /// |
11 * | 11 /// Normally you do not need to use this class directly, see [PolymerElement]. |
12 * Normally you do not need to use this class directly, see [PolymerElement]. | |
13 */ | |
14 class PolymerDeclaration extends HtmlElement { | 12 class PolymerDeclaration extends HtmlElement { |
15 static const _TAG = 'polymer-element'; | 13 static const _TAG = 'polymer-element'; |
16 | 14 |
17 factory PolymerDeclaration() => new Element.tag(_TAG); | 15 factory PolymerDeclaration() => new Element.tag(_TAG); |
18 // Fully ported from revision: | 16 // Fully ported from revision: |
19 // https://github.com/Polymer/polymer/blob/b7200854b2441a22ce89f6563963f36c50f
5150d | 17 // https://github.com/Polymer/polymer/blob/b7200854b2441a22ce89f6563963f36c50f
5150d |
20 // | 18 // |
21 // src/declaration/attributes.js | 19 // src/declaration/attributes.js |
22 // src/declaration/events.js | 20 // src/declaration/events.js |
23 // src/declaration/polymer-element.js | 21 // src/declaration/polymer-element.js |
(...skipping 15 matching lines...) Expand all Loading... |
39 // TODO(jmesserly): this is also a cache, since we can't store .element on | 37 // TODO(jmesserly): this is also a cache, since we can't store .element on |
40 // each level of the __proto__ like JS does. | 38 // each level of the __proto__ like JS does. |
41 PolymerDeclaration _super; | 39 PolymerDeclaration _super; |
42 PolymerDeclaration get superDeclaration => _super; | 40 PolymerDeclaration get superDeclaration => _super; |
43 | 41 |
44 String _extendsName; | 42 String _extendsName; |
45 | 43 |
46 String _name; | 44 String _name; |
47 String get name => _name; | 45 String get name => _name; |
48 | 46 |
49 /** | 47 /// Map of publish properties. Can be a field or a property getter, but if |
50 * Map of publish properties. Can be a field or a property getter, but if this | 48 /// this map contains a getter, is because it also has a corresponding setter. |
51 * map contains a getter, is because it also has a corresponding setter. | 49 /// |
52 * | 50 /// Note: technically these are always single properties, so we could use a |
53 * Note: technically these are always single properties, so we could use a | 51 /// Symbol instead of a PropertyPath. However there are lookups between this |
54 * Symbol instead of a PropertyPath. However there are lookups between this | 52 /// map and [_observe] so it is easier to just track paths. |
55 * map and [_observe] so it is easier to just track paths. | |
56 */ | |
57 Map<PropertyPath, smoke.Declaration> _publish; | 53 Map<PropertyPath, smoke.Declaration> _publish; |
58 | 54 |
59 /** The names of published properties for this polymer-element. */ | 55 /// The names of published properties for this polymer-element. |
60 Iterable<String> get publishedProperties => | 56 Iterable<String> get publishedProperties => |
61 _publish != null ? _publish.keys.map((p) => '$p') : const []; | 57 _publish != null ? _publish.keys.map((p) => '$p') : const []; |
62 | 58 |
63 /** Same as [_publish] but with lower case names. */ | 59 /// Same as [_publish] but with lower case names. |
64 Map<String, smoke.Declaration> _publishLC; | 60 Map<String, smoke.Declaration> _publishLC; |
65 | 61 |
66 Map<PropertyPath, List<Symbol>> _observe; | 62 Map<PropertyPath, List<Symbol>> _observe; |
67 | 63 |
68 Map<String, Object> _instanceAttributes; | 64 Map<String, Object> _instanceAttributes; |
69 | 65 |
70 List<Element> _sheets; | 66 List<Element> _sheets; |
71 List<Element> get sheets => _sheets; | 67 List<Element> get sheets => _sheets; |
72 | 68 |
73 List<Element> _styles; | 69 List<Element> _styles; |
74 List<Element> get styles => _styles; | 70 List<Element> get styles => _styles; |
75 | 71 |
76 DocumentFragment get templateContent { | 72 DocumentFragment get templateContent { |
77 final template = this.querySelector('template'); | 73 final template = this.querySelector('template'); |
78 return template != null ? templateBind(template).content : null; | 74 return template != null ? templateBind(template).content : null; |
79 } | 75 } |
80 | 76 |
81 /** Maps event names and their associated method in the element class. */ | 77 /// Maps event names and their associated method in the element class. |
82 final Map<String, String> _eventDelegates = {}; | 78 final Map<String, String> _eventDelegates = {}; |
83 | 79 |
84 /** Expected events per element node. */ | 80 /// Expected events per element node. |
85 // TODO(sigmund): investigate whether we need more than 1 set of local events | 81 // TODO(sigmund): investigate whether we need more than 1 set of local events |
86 // per element (why does the js implementation stores 1 per template node?) | 82 // per element (why does the js implementation stores 1 per template node?) |
87 Expando<Set<String>> _templateDelegates; | 83 Expando<Set<String>> _templateDelegates; |
88 | 84 |
89 PolymerDeclaration.created() : super.created() { | 85 PolymerDeclaration.created() : super.created() { |
90 // fetch the element name | 86 // fetch the element name |
91 _name = attributes['name']; | 87 _name = attributes['name']; |
92 // fetch our extendee name | 88 // fetch our extendee name |
93 _extendsName = attributes['extends']; | 89 _extendsName = attributes['extends']; |
94 // install element definition, if ready | 90 // install element definition, if ready |
(...skipping 72 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
167 // more declarative features | 163 // more declarative features |
168 desugar(name, extendee); | 164 desugar(name, extendee); |
169 // register our custom element | 165 // register our custom element |
170 registerType(name); | 166 registerType(name); |
171 | 167 |
172 // NOTE: skip in Dart because we don't have mutable global scope. | 168 // NOTE: skip in Dart because we don't have mutable global scope. |
173 // reference constructor in a global named by 'constructor' attribute | 169 // reference constructor in a global named by 'constructor' attribute |
174 // publishConstructor(); | 170 // publishConstructor(); |
175 } | 171 } |
176 | 172 |
177 /** | 173 /// Gets the Dart type registered for this name, and sets up declarative |
178 * Gets the Dart type registered for this name, and sets up declarative | 174 /// features. Fills in the [type] and [supertype] fields. |
179 * features. Fills in the [type] and [supertype] fields. | 175 /// |
180 * | 176 /// *Note*: unlike the JavaScript version, we do not have to metaprogram the |
181 * *Note*: unlike the JavaScript version, we do not have to metaprogram the | 177 /// prototype, which simplifies this method. |
182 * prototype, which simplifies this method. | |
183 */ | |
184 void buildType(String name, String extendee) { | 178 void buildType(String name, String extendee) { |
185 // get our custom type | 179 // get our custom type |
186 _type = _getRegisteredType(name); | 180 _type = _getRegisteredType(name); |
187 | 181 |
188 // get basal prototype | 182 // get basal prototype |
189 _supertype = _getRegisteredType(extendee); | 183 _supertype = _getRegisteredType(extendee); |
190 if (_supertype != null) _super = _getDeclaration(extendee); | 184 if (_supertype != null) _super = _getDeclaration(extendee); |
191 | 185 |
192 // transcribe `attributes` declarations onto own prototype's `publish` | 186 // transcribe `attributes` declarations onto own prototype's `publish` |
193 publishAttributes(_super); | 187 publishAttributes(_super); |
194 | 188 |
195 publishProperties(); | 189 publishProperties(); |
196 | 190 |
197 inferObservers(); | 191 inferObservers(); |
198 | 192 |
199 // desugar compound observer syntax, e.g. @ObserveProperty('a b c') | 193 // desugar compound observer syntax, e.g. @ObserveProperty('a b c') |
200 explodeObservers(); | 194 explodeObservers(); |
201 | 195 |
202 // Skip the rest in Dart: | 196 // Skip the rest in Dart: |
203 // chain various meta-data objects to inherited versions | 197 // chain various meta-data objects to inherited versions |
204 // chain custom api to inherited | 198 // chain custom api to inherited |
205 // build side-chained lists to optimize iterations | 199 // build side-chained lists to optimize iterations |
206 // inherit publishing meta-data | 200 // inherit publishing meta-data |
207 // x-platform fixup | 201 // x-platform fixup |
208 } | 202 } |
209 | 203 |
210 /** Implement various declarative features. */ | 204 /// Implement various declarative features. |
211 void desugar(name, extendee) { | 205 void desugar(name, extendee) { |
212 // compile list of attributes to copy to instances | 206 // compile list of attributes to copy to instances |
213 accumulateInstanceAttributes(); | 207 accumulateInstanceAttributes(); |
214 // parse on-* delegates declared on `this` element | 208 // parse on-* delegates declared on `this` element |
215 parseHostEvents(); | 209 parseHostEvents(); |
216 // install external stylesheets as if they are inline | 210 // install external stylesheets as if they are inline |
217 installSheets(); | 211 installSheets(); |
218 | 212 |
219 adjustShadowElement(); | 213 adjustShadowElement(); |
220 | 214 |
(...skipping 100 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
321 | 315 |
322 static bool isInstanceAttribute(name) { | 316 static bool isInstanceAttribute(name) { |
323 // do not clone these attributes onto instances | 317 // do not clone these attributes onto instances |
324 final blackList = const { | 318 final blackList = const { |
325 'name': 1, 'extends': 1, 'constructor': 1, 'noscript': 1, | 319 'name': 1, 'extends': 1, 'constructor': 1, 'noscript': 1, |
326 'attributes': 1}; | 320 'attributes': 1}; |
327 | 321 |
328 return !blackList.containsKey(name) && !name.startsWith('on-'); | 322 return !blackList.containsKey(name) && !name.startsWith('on-'); |
329 } | 323 } |
330 | 324 |
331 /** Extracts events from the element tag attributes. */ | 325 /// Extracts events from the element tag attributes. |
332 void parseHostEvents() { | 326 void parseHostEvents() { |
333 addAttributeDelegates(_eventDelegates); | 327 addAttributeDelegates(_eventDelegates); |
334 } | 328 } |
335 | 329 |
336 void addAttributeDelegates(Map<String, String> delegates) { | 330 void addAttributeDelegates(Map<String, String> delegates) { |
337 attributes.forEach((name, value) { | 331 attributes.forEach((name, value) { |
338 if (_hasEventPrefix(name)) { | 332 if (_hasEventPrefix(name)) { |
339 var start = value.indexOf('{{'); | 333 var start = value.indexOf('{{'); |
340 var end = value.lastIndexOf('}}'); | 334 var end = value.lastIndexOf('}}'); |
341 if (start >= 0 && end >= 0) { | 335 if (start >= 0 && end >= 0) { |
342 delegates[_removeEventPrefix(name)] = | 336 delegates[_removeEventPrefix(name)] = |
343 value.substring(start + 2, end).trim(); | 337 value.substring(start + 2, end).trim(); |
344 } | 338 } |
345 } | 339 } |
346 }); | 340 }); |
347 } | 341 } |
348 | 342 |
349 String urlToPath(String url) { | 343 String urlToPath(String url) { |
350 if (url == null) return ''; | 344 if (url == null) return ''; |
351 return (url.split('/')..removeLast()..add('')).join('/'); | 345 return (url.split('/')..removeLast()..add('')).join('/'); |
352 } | 346 } |
353 | 347 |
354 /** | 348 /// Install external stylesheets loaded in <element> elements into the |
355 * Install external stylesheets loaded in <element> elements into the | 349 /// element's template. |
356 * element's template. | |
357 */ | |
358 void installSheets() { | 350 void installSheets() { |
359 cacheSheets(); | 351 cacheSheets(); |
360 cacheStyles(); | 352 cacheStyles(); |
361 installLocalSheets(); | 353 installLocalSheets(); |
362 installGlobalStyles(); | 354 installGlobalStyles(); |
363 } | 355 } |
364 | 356 |
365 void cacheSheets() { | 357 void cacheSheets() { |
366 _sheets = findNodes(_SHEET_SELECTOR); | 358 _sheets = findNodes(_SHEET_SELECTOR); |
367 for (var s in sheets) s.remove(); | 359 for (var s in sheets) s.remove(); |
368 } | 360 } |
369 | 361 |
370 void cacheStyles() { | 362 void cacheStyles() { |
371 _styles = findNodes('$_STYLE_SELECTOR[$_SCOPE_ATTR]'); | 363 _styles = findNodes('$_STYLE_SELECTOR[$_SCOPE_ATTR]'); |
372 for (var s in styles) s.remove(); | 364 for (var s in styles) s.remove(); |
373 } | 365 } |
374 | 366 |
375 /** | 367 /// Takes external stylesheets loaded in an `<element>` element and moves |
376 * Takes external stylesheets loaded in an `<element>` element and moves | 368 /// their content into a style element inside the `<element>`'s template. |
377 * their content into a style element inside the `<element>`'s template. | 369 /// The sheet is then removed from the `<element>`. This is done only so |
378 * The sheet is then removed from the `<element>`. This is done only so | 370 /// that if the element is loaded in the main document, the sheet does |
379 * that if the element is loaded in the main document, the sheet does | 371 /// not become active. |
380 * not become active. | 372 /// Note, ignores sheets with the attribute 'polymer-scope'. |
381 * Note, ignores sheets with the attribute 'polymer-scope'. | |
382 */ | |
383 void installLocalSheets() { | 373 void installLocalSheets() { |
384 var sheets = this.sheets.where( | 374 var sheets = this.sheets.where( |
385 (s) => !s.attributes.containsKey(_SCOPE_ATTR)); | 375 (s) => !s.attributes.containsKey(_SCOPE_ATTR)); |
386 var content = templateContent; | 376 var content = templateContent; |
387 if (content != null) { | 377 if (content != null) { |
388 var cssText = new StringBuffer(); | 378 var cssText = new StringBuffer(); |
389 for (var sheet in sheets) { | 379 for (var sheet in sheets) { |
390 cssText..write(_cssTextFromSheet(sheet))..write('\n'); | 380 cssText..write(_cssTextFromSheet(sheet))..write('\n'); |
391 } | 381 } |
392 if (cssText.length > 0) { | 382 if (cssText.length > 0) { |
393 content.insertBefore( | 383 content.insertBefore( |
394 new StyleElement()..text = '$cssText', | 384 new StyleElement()..text = '$cssText', |
395 content.firstChild); | 385 content.firstChild); |
396 } | 386 } |
397 } | 387 } |
398 } | 388 } |
399 | 389 |
400 List<Element> findNodes(String selector, [bool matcher(Element e)]) { | 390 List<Element> findNodes(String selector, [bool matcher(Element e)]) { |
401 var nodes = this.querySelectorAll(selector).toList(); | 391 var nodes = this.querySelectorAll(selector).toList(); |
402 var content = templateContent; | 392 var content = templateContent; |
403 if (content != null) { | 393 if (content != null) { |
404 nodes = nodes..addAll(content.querySelectorAll(selector)); | 394 nodes = nodes..addAll(content.querySelectorAll(selector)); |
405 } | 395 } |
406 if (matcher != null) return nodes.where(matcher).toList(); | 396 if (matcher != null) return nodes.where(matcher).toList(); |
407 return nodes; | 397 return nodes; |
408 } | 398 } |
409 | 399 |
410 /** | 400 /// Promotes external stylesheets and style elements with the attribute |
411 * Promotes external stylesheets and style elements with the attribute | 401 /// polymer-scope='global' into global scope. |
412 * polymer-scope='global' into global scope. | 402 /// This is particularly useful for defining @keyframe rules which |
413 * This is particularly useful for defining @keyframe rules which | 403 /// currently do not function in scoped or shadow style elements. |
414 * currently do not function in scoped or shadow style elements. | 404 /// (See wkb.ug/72462) |
415 * (See wkb.ug/72462) | |
416 */ | |
417 // TODO(sorvell): remove when wkb.ug/72462 is addressed. | 405 // TODO(sorvell): remove when wkb.ug/72462 is addressed. |
418 void installGlobalStyles() { | 406 void installGlobalStyles() { |
419 var style = styleForScope(_STYLE_GLOBAL_SCOPE); | 407 var style = styleForScope(_STYLE_GLOBAL_SCOPE); |
420 Polymer.applyStyleToScope(style, document.head); | 408 Polymer.applyStyleToScope(style, document.head); |
421 } | 409 } |
422 | 410 |
423 String cssTextForScope(String scopeDescriptor) { | 411 String cssTextForScope(String scopeDescriptor) { |
424 var cssText = new StringBuffer(); | 412 var cssText = new StringBuffer(); |
425 // handle stylesheets | 413 // handle stylesheets |
426 var selector = '[$_SCOPE_ATTR=$scopeDescriptor]'; | 414 var selector = '[$_SCOPE_ATTR=$scopeDescriptor]'; |
(...skipping 15 matching lines...) Expand all Loading... |
442 } | 430 } |
443 | 431 |
444 StyleElement cssTextToScopeStyle(String cssText, String scopeDescriptor) { | 432 StyleElement cssTextToScopeStyle(String cssText, String scopeDescriptor) { |
445 if (cssText == '') return null; | 433 if (cssText == '') return null; |
446 | 434 |
447 return new StyleElement() | 435 return new StyleElement() |
448 ..text = cssText | 436 ..text = cssText |
449 ..attributes[_STYLE_SCOPE_ATTRIBUTE] = '$name-$scopeDescriptor'; | 437 ..attributes[_STYLE_SCOPE_ATTRIBUTE] = '$name-$scopeDescriptor'; |
450 } | 438 } |
451 | 439 |
452 /** | 440 /// Fetch a list of all *Changed methods so we can observe the associated |
453 * Fetch a list of all *Changed methods so we can observe the associated | 441 /// properties. |
454 * properties. | |
455 */ | |
456 void inferObservers() { | 442 void inferObservers() { |
457 var options = const smoke.QueryOptions(includeFields: false, | 443 var options = const smoke.QueryOptions(includeFields: false, |
458 includeProperties: false, includeMethods: true, includeInherited: true); | 444 includeProperties: false, includeMethods: true, includeInherited: true); |
459 for (var decl in smoke.query(_type, options)) { | 445 for (var decl in smoke.query(_type, options)) { |
460 String name = smoke.symbolToName(decl.name); | 446 String name = smoke.symbolToName(decl.name); |
461 if (name.endsWith(_OBSERVE_SUFFIX) && name != 'attributeChanged') { | 447 if (name.endsWith(_OBSERVE_SUFFIX) && name != 'attributeChanged') { |
462 // TODO(jmesserly): now that we have a better system, should we | 448 // TODO(jmesserly): now that we have a better system, should we |
463 // deprecate *Changed methods? | 449 // deprecate *Changed methods? |
464 if (_observe == null) _observe = new HashMap(); | 450 if (_observe == null) _observe = new HashMap(); |
465 name = name.substring(0, name.length - 7); | 451 name = name.substring(0, name.length - 7); |
466 _observe[new PropertyPath(name)] = [decl.name]; | 452 _observe[new PropertyPath(name)] = [decl.name]; |
467 } | 453 } |
468 } | 454 } |
469 } | 455 } |
470 | 456 |
471 /** | 457 /// Fetch a list of all methods annotated with [ObserveProperty] so we can |
472 * Fetch a list of all methods annotated with [ObserveProperty] so we can | 458 /// observe the associated properties. |
473 * observe the associated properties. | |
474 */ | |
475 void explodeObservers() { | 459 void explodeObservers() { |
476 var options = const smoke.QueryOptions(includeFields: false, | 460 var options = const smoke.QueryOptions(includeFields: false, |
477 includeProperties: false, includeMethods: true, includeInherited: true, | 461 includeProperties: false, includeMethods: true, includeInherited: true, |
478 withAnnotations: const [ObserveProperty]); | 462 withAnnotations: const [ObserveProperty]); |
479 for (var decl in smoke.query(_type, options)) { | 463 for (var decl in smoke.query(_type, options)) { |
480 for (var meta in decl.annotations) { | 464 for (var meta in decl.annotations) { |
481 if (meta is! ObserveProperty) continue; | 465 if (meta is! ObserveProperty) continue; |
482 if (_observe == null) _observe = new HashMap(); | 466 if (_observe == null) _observe = new HashMap(); |
483 for (String name in meta.names) { | 467 for (String name in meta.names) { |
484 _observe.putIfAbsent(new PropertyPath(name), () => []).add(decl.name); | 468 _observe.putIfAbsent(new PropertyPath(name), () => []).add(decl.name); |
(...skipping 52 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
537 var options = const smoke.QueryOptions(includeInherited: true, | 521 var options = const smoke.QueryOptions(includeInherited: true, |
538 withAnnotations: const [PublishedProperty]); | 522 withAnnotations: const [PublishedProperty]); |
539 for (var decl in smoke.query(type, options)) { | 523 for (var decl in smoke.query(type, options)) { |
540 if (decl.isFinal) continue; | 524 if (decl.isFinal) continue; |
541 if (props == null) props = {}; | 525 if (props == null) props = {}; |
542 props[new PropertyPath([decl.name])] = decl; | 526 props[new PropertyPath([decl.name])] = decl; |
543 } | 527 } |
544 return props; | 528 return props; |
545 } | 529 } |
546 | 530 |
547 /** Attribute prefix used for declarative event handlers. */ | 531 /// Attribute prefix used for declarative event handlers. |
548 const _EVENT_PREFIX = 'on-'; | 532 const _EVENT_PREFIX = 'on-'; |
549 | 533 |
550 /** Whether an attribute declares an event. */ | 534 /// Whether an attribute declares an event. |
551 bool _hasEventPrefix(String attr) => attr.startsWith(_EVENT_PREFIX); | 535 bool _hasEventPrefix(String attr) => attr.startsWith(_EVENT_PREFIX); |
552 | 536 |
553 String _removeEventPrefix(String name) => name.substring(_EVENT_PREFIX.length); | 537 String _removeEventPrefix(String name) => name.substring(_EVENT_PREFIX.length); |
554 | 538 |
555 /** | 539 /// Using Polymer's platform/src/ShadowCSS.js passing the style tag's content. |
556 * Using Polymer's platform/src/ShadowCSS.js passing the style tag's content. | |
557 */ | |
558 void _shimShadowDomStyling(DocumentFragment template, String name, | 540 void _shimShadowDomStyling(DocumentFragment template, String name, |
559 String extendee) { | 541 String extendee) { |
560 if (template == null || !_hasShadowDomPolyfill) return; | 542 if (template == null || !_hasShadowDomPolyfill) return; |
561 | 543 |
562 var platform = js.context['Platform']; | 544 var platform = js.context['Platform']; |
563 if (platform == null) return; | 545 if (platform == null) return; |
564 var shadowCss = platform['ShadowCSS']; | 546 var shadowCss = platform['ShadowCSS']; |
565 if (shadowCss == null) return; | 547 if (shadowCss == null) return; |
566 shadowCss.callMethod('shimStyling', [template, name, extendee]); | 548 shadowCss.callMethod('shimStyling', [template, name, extendee]); |
567 } | 549 } |
(...skipping 76 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
644 | 626 |
645 // Dart note: we need this function because we have additional renames JS does | 627 // Dart note: we need this function because we have additional renames JS does |
646 // not have. The JS renames are simply case differences, whereas we have ones | 628 // not have. The JS renames are simply case differences, whereas we have ones |
647 // like doubleclick -> dblclick and stripping the webkit prefix. | 629 // like doubleclick -> dblclick and stripping the webkit prefix. |
648 String _eventNameFromType(String eventType) { | 630 String _eventNameFromType(String eventType) { |
649 final result = _reverseEventTranslations[eventType]; | 631 final result = _reverseEventTranslations[eventType]; |
650 return result != null ? result : eventType; | 632 return result != null ? result : eventType; |
651 } | 633 } |
652 | 634 |
653 final _ATTRIBUTES_REGEX = new RegExp(r'\s|,'); | 635 final _ATTRIBUTES_REGEX = new RegExp(r'\s|,'); |
OLD | NEW |