| 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 /** |  | 
|    6  * A script to assist in documenting the difference between the dart:html API |  | 
|    7  * and the old DOM API. |  | 
|    8  */ |  | 
|    9 #library('html_diff'); |  | 
|   10  |  | 
|   11 #import('../../../frog/lang.dart'); |  | 
|   12 #import('../../../frog/file_system_node.dart'); |  | 
|   13 #import('../../../frog/file_system.dart'); |  | 
|   14 #import('../../../utils/dartdoc/dartdoc.dart'); |  | 
|   15  |  | 
|   16 void main() { |  | 
|   17   var files = new NodeFileSystem(); |  | 
|   18   parseOptions('../../frog', [] /* args */, files); |  | 
|   19   initializeWorld(files); |  | 
|   20   initializeDartDoc(); |  | 
|   21   HtmlDiff.initialize(); |  | 
|   22  |  | 
|   23   var diff = new HtmlDiff(); |  | 
|   24   diff.run(); |  | 
|   25  |  | 
|   26   diff.domToHtml.forEach((domMember, htmlMembers) { |  | 
|   27     for (var htmlMember in htmlMembers) { |  | 
|   28       if (diff.sameName(domMember, htmlMember)) continue; |  | 
|   29       var htmlTypeName = htmlMember.declaringType.name; |  | 
|   30       var htmlName = '$htmlTypeName.${htmlMember.name}'; |  | 
|   31       if (htmlMember.isConstructor || htmlMember.isFactory) { |  | 
|   32         final separator = htmlMember.constructorName == '' ? '' : '.'; |  | 
|   33         htmlName = 'new $htmlTypeName$separator${htmlMember.constructorName}'; |  | 
|   34       } |  | 
|   35       print('${domMember.declaringType.name}.${domMember.name} -> ${htmlName}'); |  | 
|   36     } |  | 
|   37   }); |  | 
|   38  |  | 
|   39   for (var type in world.dom.types.getValues()) { |  | 
|   40     if (type.name == null) continue; |  | 
|   41     if (type.definition is FunctionTypeDefinition) continue; |  | 
|   42     for (var member in type.members.getValues()) { |  | 
|   43       if (!member.isPrivate && member.name != 'typeName' && |  | 
|   44           !diff.domToHtml.containsKey(member) && |  | 
|   45           (member is MethodMember || member is PropertyMember)) { |  | 
|   46         print('No dart:html wrapper for ${type.name}.${member.name}'); |  | 
|   47       } |  | 
|   48     } |  | 
|   49   } |  | 
|   50 } |  | 
|   51  |  | 
|   52 /** |  | 
|   53  * A class for computing a many-to-many mapping between the types and members in |  | 
|   54  * `dart:dom` and `dart:html`. This mapping is based on two indicators: |  | 
|   55  * |  | 
|   56  *   1. Auto-detected wrappers. Most `dart:html` types correspond |  | 
|   57  *      straightforwardly to a single `dart:dom` type, and have the same name. |  | 
|   58  *      In addition, most `dart:htmlimpl` methods just call a single `dart:dom` |  | 
|   59  *      method. This class detects these simple correspondences automatically. |  | 
|   60  * |  | 
|   61  *   2. Manual annotations. When it's not clear which `dart:dom` items a given |  | 
|   62  *      `dart:html` item corresponds to, the `dart:htmlimpl` item can be |  | 
|   63  *      annotated in the documentation comments using the `@domName` annotation. |  | 
|   64  * |  | 
|   65  * The `@domName` annotations for types and members are of the form `@domName |  | 
|   66  * NAME(, NAME)*`, where the `NAME`s refer to the `dart:dom` types/members that |  | 
|   67  * correspond to the annotated `dart:htmlimpl` type/member. `NAME`s on member |  | 
|   68  * annotations can refer to either fully-qualified member names (e.g. |  | 
|   69  * `Document.createElement`) or unqualified member names (e.g. `createElement`). |  | 
|   70  * Unqualified member names are assumed to refer to members of one of the |  | 
|   71  * corresponding `dart:dom` types. |  | 
|   72  */ |  | 
|   73 class HtmlDiff { |  | 
|   74   /** A map from `dart:dom` members to corresponding `dart:html` members. */ |  | 
|   75   final Map<Member, Set<Member>> domToHtml; |  | 
|   76  |  | 
|   77   /** A map from `dart:html` members to corresponding `dart:dom` members. */ |  | 
|   78   final Map<Member, Set<Member>> htmlToDom; |  | 
|   79  |  | 
|   80   /** A map from `dart:dom` types to corresponding `dart:html` types. */ |  | 
|   81   final Map<Type, Set<Type>> domTypesToHtml; |  | 
|   82  |  | 
|   83   /** A map from `dart:html` types to corresponding `dart:dom` types. */ |  | 
|   84   final Map<Type, Set<Type>> htmlTypesToDom; |  | 
|   85  |  | 
|   86   final CommentMap comments; |  | 
|   87  |  | 
|   88   /** |  | 
|   89    * Perform static initialization of [world]. This should be run before |  | 
|   90    * calling [HtmlDiff.run]. |  | 
|   91    */ |  | 
|   92   static void initialize() { |  | 
|   93     world.processDartScript('dart:htmlimpl'); |  | 
|   94     world.resolveAll(); |  | 
|   95   } |  | 
|   96  |  | 
|   97   HtmlDiff() : |  | 
|   98     domToHtml = new Map<Member, Set<Member>>(), |  | 
|   99     htmlToDom = new Map<Member, Set<Member>>(), |  | 
|  100     domTypesToHtml = new Map<Type, Set<Type>>(), |  | 
|  101     htmlTypesToDom = new Map<Type, Set<Type>>(), |  | 
|  102     comments = new CommentMap(); |  | 
|  103  |  | 
|  104   /** |  | 
|  105    * Computes the `dart:dom` to `dart:html` mapping, and places it in |  | 
|  106    * [domToHtml], [htmlToDom], [domTypesToHtml], and [htmlTypesToDom]. Before |  | 
|  107    * this is run, Frog should be initialized (via [parseOptions] and |  | 
|  108    * [initializeWorld]) and [HtmlDiff.initialize] should be called. |  | 
|  109    */ |  | 
|  110   void run() { |  | 
|  111     final htmlLib = world.libraries['dart:htmlimpl']; |  | 
|  112     for (var implType in htmlLib.types.getValues()) { |  | 
|  113       final domTypes = htmlToDomTypes(implType); |  | 
|  114       final htmlType = htmlImplToHtmlType(implType); |  | 
|  115       if (htmlType == null) continue; |  | 
|  116  |  | 
|  117       htmlTypesToDom.putIfAbsent(htmlType, () => new Set()).addAll(domTypes); |  | 
|  118       domTypes.forEach((t) => |  | 
|  119           domTypesToHtml.putIfAbsent(t, () => new Set()).add(htmlType)); |  | 
|  120  |  | 
|  121       final members = new List.from(implType.members.getValues()); |  | 
|  122       members.addAll(implType.constructors.getValues()); |  | 
|  123       implType.factories.forEach((f) => members.add(f)); |  | 
|  124       members.forEach((m) => _addMemberDiff(m, domTypes)); |  | 
|  125     } |  | 
|  126   } |  | 
|  127  |  | 
|  128   /** |  | 
|  129    * Returns whether or not [domMember] (from `dart:dom`) and [htmlMember] (from |  | 
|  130    * `dart:html`) have the same name from the user's perspective. The names are |  | 
|  131    * the same if the type names and the member names are the same, but allowance |  | 
|  132    * is made for `dart:dom` names that start with "HTML" or "WebKit", and for |  | 
|  133    * `dart:html` properties that have the same name as fields in `dart:dom`. |  | 
|  134    */ |  | 
|  135   bool sameName(Member domMember, Member htmlMember) { |  | 
|  136     var domTypeName = domMember.declaringType.name; |  | 
|  137     if (domTypeName == 'DOMWindow') domTypeName = 'Window'; |  | 
|  138     domTypeName = domTypeName.replaceFirst(new RegExp('^(HTML|WebKit)'), ''); |  | 
|  139     var htmlTypeName = htmlMember.declaringType.name; |  | 
|  140  |  | 
|  141     var domName = domMember.name; |  | 
|  142     var htmlName = htmlMember.name; |  | 
|  143     if (htmlName.startsWith('get:') || htmlName.startsWith('set:')) { |  | 
|  144       htmlName = htmlName.substring(4); |  | 
|  145     } |  | 
|  146  |  | 
|  147     return domTypeName == htmlTypeName && domName == htmlName; |  | 
|  148   } |  | 
|  149  |  | 
|  150   /** |  | 
|  151    * Records the `dart:dom` to `dart:html` mapping for [implMember] (from |  | 
|  152    * `dart:htmlimpl`). [domTypes] are the `dart:dom` [Type]s that correspond to |  | 
|  153    * [implMember]'s defining [Type]. |  | 
|  154    */ |  | 
|  155   void _addMemberDiff(Member implMember, List<Type> domTypes) { |  | 
|  156     if (implMember.isProperty) { |  | 
|  157       if (implMember.canGet) _addMemberDiff(implMember.getter, domTypes); |  | 
|  158       if (implMember.canSet) _addMemberDiff(implMember.setter, domTypes); |  | 
|  159     } |  | 
|  160  |  | 
|  161     var domMembers = htmlToDomMembers(implMember, domTypes); |  | 
|  162     var htmlMember = htmlImplToHtmlMember(implMember); |  | 
|  163     if (htmlMember == null && !domMembers.isEmpty()) { |  | 
|  164       print('Warning: dart:htmlimpl member ${implMember.declaringType.name}.' + |  | 
|  165           '${implMember.name} has no corresponding dart:html member.'); |  | 
|  166     } |  | 
|  167  |  | 
|  168     if (htmlMember == null) return; |  | 
|  169     if (!domMembers.isEmpty()) htmlToDom[htmlMember] = domMembers; |  | 
|  170     domMembers.forEach((m) => |  | 
|  171         domToHtml.putIfAbsent(m, () => new Set()).add(htmlMember)); |  | 
|  172   } |  | 
|  173  |  | 
|  174   /** |  | 
|  175    * Returns the `dart:html` [Type] that corresponds to [implType] from |  | 
|  176    * `dart:htmlimpl`, or `null` if there is no such correspondence. |  | 
|  177    */ |  | 
|  178   Type htmlImplToHtmlType(Type implType) { |  | 
|  179     if (implType == null || implType.isTop || implType.interfaces.isEmpty() || |  | 
|  180         implType.interfaces[0].library.name != 'html') { |  | 
|  181       return null; |  | 
|  182     } |  | 
|  183  |  | 
|  184     return implType.interfaces[0]; |  | 
|  185   } |  | 
|  186  |  | 
|  187   /** |  | 
|  188    * Returns the `dart:html` [Member] that corresponds to [implMember] from |  | 
|  189    * `dart:htmlimpl`, or `null` if there is no such correspondence. |  | 
|  190    */ |  | 
|  191   Member htmlImplToHtmlMember(Member implMember) { |  | 
|  192     var htmlType = htmlImplToHtmlType(implMember.declaringType); |  | 
|  193     if (htmlType == null) return null; |  | 
|  194  |  | 
|  195     bool getter, setter; |  | 
|  196     if (implMember.isConstructor || implMember.isFactory) { |  | 
|  197       var constructor = htmlType.constructors[implMember.name]; |  | 
|  198       if (constructor != null) return constructor; |  | 
|  199       return htmlType.factories[implMember.name]; |  | 
|  200     } else if ((getter = implMember.name.startsWith('get:')) || |  | 
|  201         (setter = implMember.name.startsWith('set:'))) { |  | 
|  202       // Use getMember to follow interface inheritance chains. If it's a |  | 
|  203       // ConcreteMember, though, it's an implementation of some data structure |  | 
|  204       // and we don't care about it. |  | 
|  205       var htmlProperty = htmlType.getMember(implMember.name.substring(4)); |  | 
|  206       if (htmlProperty != null && htmlProperty is! ConcreteMember) { |  | 
|  207         return getter ? htmlProperty.getter : htmlProperty.setter; |  | 
|  208       } else { |  | 
|  209         return null; |  | 
|  210       } |  | 
|  211     } else { |  | 
|  212       return htmlType.getMember(implMember.name); |  | 
|  213     } |  | 
|  214   } |  | 
|  215  |  | 
|  216   /** |  | 
|  217    * Returns the `dart:dom` [Type]s that correspond to [htmlType] from |  | 
|  218    * `dart:htmlimpl`. This can be the empty list if no correspondence is found. |  | 
|  219    */ |  | 
|  220   List<Type> htmlToDomTypes(Type htmlType) { |  | 
|  221     if (htmlType.name == null) return []; |  | 
|  222     final tags = _getTags(comments.find(htmlType.span)); |  | 
|  223  |  | 
|  224     if (tags.containsKey('domName')) { |  | 
|  225       var domNames = map(tags['domName'].split(','), (s) => s.trim()); |  | 
|  226       if (domNames.length == 1 && domNames[0] == 'none') return []; |  | 
|  227       return map(domNames, (domName) { |  | 
|  228         // DOMWindow is Chrome-specific, so we don't use it in our annotations. |  | 
|  229         if (domName == 'Window') domName = 'DOMWindow'; |  | 
|  230         final domType = world.dom.types[domName]; |  | 
|  231         if (domType == null) print('Warning: no dart:dom type named $domName'); |  | 
|  232         return domType; |  | 
|  233       }); |  | 
|  234     } else { |  | 
|  235       if (!htmlType.name.endsWith('WrappingImplementation')) return []; |  | 
|  236       final domName = htmlType.name.replaceFirst('WrappingImplementation', ''); |  | 
|  237       var domType = world.dom.types[domName]; |  | 
|  238       if (domType == null && domName.endsWith('Element')) { |  | 
|  239         domType = world.dom.types['HTML$domName']; |  | 
|  240       } |  | 
|  241       if (domType == null) domType = world.dom.types['WebKit$domName']; |  | 
|  242       if (domType == null) { |  | 
|  243         print('Warning: no dart:dom type matches dart:htmlimpl ' + |  | 
|  244             htmlType.name); |  | 
|  245         return []; |  | 
|  246       } |  | 
|  247       return [domType]; |  | 
|  248     } |  | 
|  249   } |  | 
|  250  |  | 
|  251   /** |  | 
|  252    * Returns the `dart:dom` [Member]s that correspond to [htmlMember] from |  | 
|  253    * `dart:htmlimpl`. This can be the empty set if no correspondence is found. |  | 
|  254    * [domTypes] are the `dart:dom` [Type]s that correspond to [implMember]'s |  | 
|  255    * defining [Type]. |  | 
|  256    */ |  | 
|  257   Set<Member> htmlToDomMembers(Member htmlMember, List<Type> domTypes) { |  | 
|  258     if (htmlMember.isPrivate || htmlMember is! MethodMember) return new Set(); |  | 
|  259     final tags = _getTags(comments.find(htmlMember.span)); |  | 
|  260     if (tags.containsKey('domName')) { |  | 
|  261       final domNames = map(tags['domName'].split(','), (s) => s.trim()); |  | 
|  262       if (domNames.length == 1 && domNames[0] == 'none') return new Set(); |  | 
|  263       final members = new Set(); |  | 
|  264       domNames.forEach((name) { |  | 
|  265         var nameMembers = _membersFromName(name, domTypes); |  | 
|  266         if (nameMembers.isEmpty()) { |  | 
|  267           if (name.contains('.')) { |  | 
|  268             print('Warning: no member $name'); |  | 
|  269           } else { |  | 
|  270             final options = Strings.join( |  | 
|  271                 map(domTypes, (t) => "${t.name}.$name"), ' or '); |  | 
|  272             print('Warning: no member $options'); |  | 
|  273           } |  | 
|  274         } |  | 
|  275         members.addAll(nameMembers); |  | 
|  276       }); |  | 
|  277       return members; |  | 
|  278     } |  | 
|  279  |  | 
|  280     if (domTypes.isEmpty() || htmlMember.definition == null) return new Set(); |  | 
|  281     if (htmlMember.name == 'get:on') { |  | 
|  282       final members = _membersFromName('addEventListener', domTypes); |  | 
|  283       members.addAll(_membersFromName('dispatchEvent', domTypes)); |  | 
|  284       members.addAll(_membersFromName('removeEventListener', domTypes)); |  | 
|  285       return members; |  | 
|  286     } |  | 
|  287  |  | 
|  288     if (htmlMember.isFactory && htmlMember.name == '' && |  | 
|  289         domTypes.length == 1 && domTypes[0].name.endsWith('Event')) { |  | 
|  290       return _membersFromName('init${domTypes[0].name}', domTypes); |  | 
|  291     } |  | 
|  292  |  | 
|  293     return _getDomMembers(htmlMember.definition.body, domTypes); |  | 
|  294   } |  | 
|  295  |  | 
|  296   /** |  | 
|  297    * Returns the `dart:dom` [Member]s that are indicated by [name]. [name] can |  | 
|  298    * be either an unqualified member name (e.g. `createElement`), in which case |  | 
|  299    * it's treated as the name of a member of one of [defaultTypes], or a |  | 
|  300    * fully-qualified member name (e.g. `Document.createElement`), in which case |  | 
|  301    * it's looked up in `dart:dom` and [defaultTypes] is ignored. |  | 
|  302    */ |  | 
|  303   Set<Member> _membersFromName(String name, List<Type> defaultTypes) { |  | 
|  304     if (!name.contains('.', 0)) { |  | 
|  305       if (defaultTypes.isEmpty()) { |  | 
|  306         print('Warning: no default type for ${name}'); |  | 
|  307         return new Set(); |  | 
|  308       } |  | 
|  309       final members = new Set<Member>(); |  | 
|  310       defaultTypes.forEach((t) { |  | 
|  311         if (t.members.containsKey(name)) members.add(t.members[name]); |  | 
|  312       }); |  | 
|  313       return members; |  | 
|  314     } |  | 
|  315  |  | 
|  316     final splitName = name.split('.'); |  | 
|  317     if (splitName.length != 2) { |  | 
|  318       print('Warning: invalid member name ${name}'); |  | 
|  319       return new Set(); |  | 
|  320     } |  | 
|  321     var typeName = splitName[0]; |  | 
|  322     if (typeName == 'Window') typeName = 'DOMWindow'; |  | 
|  323     final type = world.dom.types[typeName]; |  | 
|  324     if (type == null) return new Set(); |  | 
|  325     final member = type.members[splitName[1]]; |  | 
|  326     if (member == null) return new Set(); |  | 
|  327     return new Set.from([member]); |  | 
|  328   } |  | 
|  329  |  | 
|  330   /** |  | 
|  331    * Returns the `dart:dom` [Member]s that are referred to in [stmt]. This only |  | 
|  332    * extracts references from relatively simple statements; methods containing |  | 
|  333    * more complex wrappers should be manually annotated with `@domName`. |  | 
|  334    * |  | 
|  335    * [domTypes] are the `dart:dom` [Type]s that correspond to the current |  | 
|  336    * [Member]'s defining [Type]. |  | 
|  337    */ |  | 
|  338   Set<Member> _getDomMembers(Statement stmt, List<Type> domTypes) { |  | 
|  339     if (stmt is BlockStatement) { |  | 
|  340       final body = stmt.body.filter((s) => !_ignorableStatement(s)); |  | 
|  341       if (body.length != 1) return new Set(); |  | 
|  342       return _getDomMembers(stmt.body[0], domTypes); |  | 
|  343     } else if (stmt is ReturnStatement) { |  | 
|  344       return _domMembersFromExpression(stmt.value, domTypes); |  | 
|  345     } else if (stmt is ExpressionStatement) { |  | 
|  346       return _domMembersFromExpression(stmt.body, domTypes); |  | 
|  347     } else if (stmt is TryStatement) { |  | 
|  348       return _getDomMembers(stmt.body, domTypes); |  | 
|  349     } else if (stmt is IfStatement) { |  | 
|  350       final members = _getDomMembers(stmt.trueBranch, domTypes); |  | 
|  351       members.addAll(_getDomMembers(stmt.falseBranch, domTypes)); |  | 
|  352       return members; |  | 
|  353     } else { |  | 
|  354       return new Set(); |  | 
|  355     } |  | 
|  356   } |  | 
|  357  |  | 
|  358   /** |  | 
|  359    * Whether [stmt] can be ignored for the purpose of determining the DOM name |  | 
|  360    * of the enclosing method. The Webkit-to-Dart conversion process leaves |  | 
|  361    * behind various `throw`s and `return`s that we want to ignore. |  | 
|  362    */ |  | 
|  363   bool _ignorableStatement(Statement stmt) { |  | 
|  364     if (stmt is BlockStatement) { |  | 
|  365       return Collections.every(stmt.body, (s) => _ignorableStatement(s)); |  | 
|  366     } else if (stmt is TryStatement) { |  | 
|  367       return _ignorableStatement(stmt.body); |  | 
|  368     } else if (stmt is IfStatement) { |  | 
|  369       return _ignorableStatement(stmt.trueBranch) && |  | 
|  370         _ignorableStatement(stmt.falseBranch); |  | 
|  371     } else if (stmt is ReturnStatement) { |  | 
|  372       return stmt.value == null || stmt.value is ThisExpression; |  | 
|  373     } else { |  | 
|  374       return stmt is ThrowStatement; |  | 
|  375     } |  | 
|  376   } |  | 
|  377  |  | 
|  378   /** |  | 
|  379    * Returns the `dart:dom` [Member]s that are referred to in [expr]. This only |  | 
|  380    * extracts references from relatively simple expressions; methods containing |  | 
|  381    * more complex wrappers should be manually annotated with `@domName`. |  | 
|  382    * |  | 
|  383    * [domTypes] are the `dart:dom` [Type]s that correspond to the current |  | 
|  384    * [Member]'s defining [Type]. |  | 
|  385    */ |  | 
|  386   Set<Member> _domMembersFromExpression(Expression expr, List<Type> domTypes) { |  | 
|  387     if (expr is BinaryExpression && expr.op.kind == TokenKind.ASSIGN) { |  | 
|  388       return _domMembersFromExpression(expr.x, domTypes); |  | 
|  389     } else if (expr is CallExpression) { |  | 
|  390       if (expr.target is DotExpression && expr.target.self is VarExpression && |  | 
|  391           expr.target.self.name.name == 'LevelDom' && |  | 
|  392           (expr.target.name.name.startsWith('wrap') || |  | 
|  393            expr.target.name.name == 'unwrap')) { |  | 
|  394         return _domMembersFromExpression(expr.arguments[0].value, domTypes); |  | 
|  395       } |  | 
|  396       return _domMembersFromExpression(expr.target, domTypes); |  | 
|  397     } else if (expr is DotExpression) { |  | 
|  398       if (expr.self is NewExpression && expr.name.name == '_wrap' && |  | 
|  399           expr.self.arguments.length == 1) { |  | 
|  400         return _domMembersFromExpression(expr.self.arguments[0].value, |  | 
|  401             domTypes); |  | 
|  402       } else if (expr.self is VarExpression && expr.self.name.name == '_ptr') { |  | 
|  403         return _membersFromName(expr.name.name, domTypes); |  | 
|  404       } |  | 
|  405       final bases = _domMembersFromExpression(expr.self, domTypes); |  | 
|  406       return new Set.from(map(bases, (base) { |  | 
|  407           if (base == null || base.returnType == null) return null; |  | 
|  408           return base.returnType.members[expr.name.name]; |  | 
|  409         }).filter((m) => m != null)); |  | 
|  410     } else if (expr is NewExpression && expr.arguments.length == 1) { |  | 
|  411       return _domMembersFromExpression(expr.arguments[0].value, domTypes); |  | 
|  412     } else { |  | 
|  413       return new Set(); |  | 
|  414     } |  | 
|  415   } |  | 
|  416  |  | 
|  417   /** |  | 
|  418    * Extracts a [Map] from tag names to values from [comment], which is parsed |  | 
|  419    * from a Dart source file via dartdoc. Tags are of the form `@NAME VALUE`, |  | 
|  420    * where `NAME` is alphabetic and `VALUE` can contain any character other than |  | 
|  421    * `;`. Multiple tags can be separated by semicolons. |  | 
|  422    * |  | 
|  423    * At time of writing, the only tag that's used is `@domName`. |  | 
|  424    */ |  | 
|  425   Map<String, String> _getTags(String comment) { |  | 
|  426     if (comment == null) return const <String>{}; |  | 
|  427     final re = new RegExp("@([a-zA-Z]+) ([^;]+)(?:;|\$)"); |  | 
|  428     final tags = <String>{}; |  | 
|  429     for (var m in re.allMatches(comment.trim())) { |  | 
|  430       tags[m[1]] = m[2]; |  | 
|  431     } |  | 
|  432     return tags; |  | 
|  433   } |  | 
|  434 } |  | 
| OLD | NEW |