Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(184)

Side by Side Diff: tools/dom/scripts/htmlrenamer.py

Issue 2875773003: Roll 50: Updated for push to origin/master. (Closed)
Patch Set: Roll 50: Updated to latest Created 3 years, 7 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
1 #!/usr/bin/python 1 #!/usr/bin/python
2 # Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file 2 # Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file
3 # for details. All rights reserved. Use of this source code is governed by a 3 # for details. All rights reserved. Use of this source code is governed by a
4 # BSD-style license that can be found in the LICENSE file. 4 # BSD-style license that can be found in the LICENSE file.
5 import logging 5 import logging
6 import monitored 6 import monitored
7 import re 7 import re
8 8
9 typed_array_renames = { 9 typed_array_renames = {
10 'ArrayBuffer': 'ByteBuffer', 10 'ArrayBuffer': 'ByteBuffer',
(...skipping 53 matching lines...) Expand 10 before | Expand all | Expand 10 after
64 'WebGL2RenderingContextBase': 'RenderingContextBase2', 64 'WebGL2RenderingContextBase': 'RenderingContextBase2',
65 'WindowTimers': '_WindowTimers', 65 'WindowTimers': '_WindowTimers',
66 'XMLHttpRequest': 'HttpRequest', 66 'XMLHttpRequest': 'HttpRequest',
67 'XMLHttpRequestUpload': 'HttpRequestUpload', 67 'XMLHttpRequestUpload': 'HttpRequestUpload',
68 'XMLHttpRequestEventTarget': 'HttpRequestEventTarget', 68 'XMLHttpRequestEventTarget': 'HttpRequestEventTarget',
69 }, **typed_array_renames)) 69 }, **typed_array_renames))
70 70
71 # Interfaces that are suppressed, but need to still exist for Dartium and to 71 # Interfaces that are suppressed, but need to still exist for Dartium and to
72 # properly wrap DOM objects if/when encountered. 72 # properly wrap DOM objects if/when encountered.
73 _removed_html_interfaces = [ 73 _removed_html_interfaces = [
74 'Bluetooth',
75 'BluetoothAdvertisingData',
76 'BluetoothCharacteristicProperties',
77 'BluetoothDevice',
78 'BluetoothRemoteGATTCharacteristic',
79 'BluetoothRemoteGATTServer',
80 'BluetoothRemoteGATTService',
81 'BluetoothUUID',
74 'Cache', # TODO: Symbol conflicts with Angular: dartbug.com/20937 82 'Cache', # TODO: Symbol conflicts with Angular: dartbug.com/20937
75 'CanvasPathMethods', 83 'CanvasPathMethods',
76 'CDataSection', 84 'CDataSection',
77 'CSSPrimitiveValue', 85 'CSSPrimitiveValue',
78 'CSSUnknownRule', 86 'CSSUnknownRule',
79 'CSSValue', 87 'CSSValue',
80 'Counter', 88 'Counter',
81 'DOMFileSystemSync', # Workers 89 'DOMFileSystemSync', # Workers
82 'DatabaseSync', # Workers 90 'DatabaseSync', # Workers
83 'DataView', # Typed arrays 91 'DataView', # Typed arrays
84 'DirectoryEntrySync', # Workers 92 'DirectoryEntrySync', # Workers
85 'DirectoryReaderSync', # Workers 93 'DirectoryReaderSync', # Workers
86 'DocumentType', 94 'DocumentType',
87 'EntrySync', # Workers 95 'EntrySync', # Workers
88 'FileEntrySync', # Workers 96 'FileEntrySync', # Workers
89 'FileReaderSync', # Workers 97 'FileReaderSync', # Workers
90 'FileWriterSync', # Workers 98 'FileWriterSync', # Workers
91 'HTMLAllCollection', 99 'HTMLAllCollection',
92 'HTMLAppletElement', 100 'HTMLAppletElement',
93 'HTMLBaseFontElement', 101 'HTMLBaseFontElement',
94 'HTMLDirectoryElement', 102 'HTMLDirectoryElement',
95 'HTMLFontElement', 103 'HTMLFontElement',
96 'HTMLFrameElement', 104 'HTMLFrameElement',
97 'HTMLFrameSetElement', 105 'HTMLFrameSetElement',
98 'HTMLMarqueeElement', 106 'HTMLMarqueeElement',
99 'IDBAny', 107 'IDBAny',
108 'NFC',
100 'Notation', 109 'Notation',
101 'PagePopupController', 110 'PagePopupController',
102 'RGBColor', 111 'RGBColor',
103 'RadioNodeList', # Folded onto NodeList in dart2js. 112 'RadioNodeList', # Folded onto NodeList in dart2js.
104 'Rect', 113 'Rect',
105 'Response', # TODO: Symbol conflicts with Angular: dartbug.com/20937 114 'Response', # TODO: Symbol conflicts with Angular: dartbug.com/20937
106 'ServiceWorker', 115 'ServiceWorker',
107 'SQLTransactionSync', # Workers 116 'SQLTransactionSync', # Workers
108 'SQLTransactionSyncCallback', # Workers 117 'SQLTransactionSyncCallback', # Workers
109 'SVGAltGlyphDefElement', # Webkit only. 118 'SVGAltGlyphDefElement', # Webkit only.
(...skipping 11 matching lines...) Expand all
121 'SVGFontFaceUriElement', 130 'SVGFontFaceUriElement',
122 'SVGGlyphElement', 131 'SVGGlyphElement',
123 'SVGGlyphRefElement', 132 'SVGGlyphRefElement',
124 'SVGHKernElement', 133 'SVGHKernElement',
125 'SVGMPathElement', 134 'SVGMPathElement',
126 'SVGPaint', 135 'SVGPaint',
127 'SVGMissingGlyphElement', 136 'SVGMissingGlyphElement',
128 'SVGTRefElement', 137 'SVGTRefElement',
129 'SVGVKernElement', 138 'SVGVKernElement',
130 'SubtleCrypto', 139 'SubtleCrypto',
140 'USB',
141 'USBAlternateInterface',
142 'USBConfiguration',
143 'USBConnectionEvent',
144 'USBDevice',
145 'USBEndpoint',
146 'USBInTransferResult',
147 'USBInterface',
148 'USBIsochronousInTransferPacket',
149 'USBIsochronousInTransferResult',
150 'USBIsochronousOutTransferPacket',
151 'USBIsochronousOutTransferResult',
152 'USBOutTransferResult',
131 'WebKitCSSFilterValue', 153 'WebKitCSSFilterValue',
132 'WebKitCSSMatrix', 154 'WebKitCSSMatrix',
133 'WebKitCSSMixFunctionValue', 155 'WebKitCSSMixFunctionValue',
134 'WebKitCSSTransformValue', 156 'WebKitCSSTransformValue',
135 'WebKitMediaSource', 157 'WebKitMediaSource',
136 'WebKitNotification', 158 'WebKitNotification',
137 'WebGLRenderingContextBase', 159 'WebGLRenderingContextBase',
138 'WebGL2RenderingContextBase', 160 'WebGL2RenderingContextBase',
139 'WebKitSourceBuffer', 161 'WebKitSourceBuffer',
140 'WebKitSourceBufferList', 162 'WebKitSourceBufferList',
141 'WorkerLocation', # Workers 163 'WorkerLocation', # Workers
142 'WorkerNavigator', # Workers 164 'WorkerNavigator', # Workers
165 'Worklet', # Rendering Workers
166 'WorkletGlobalScope', # Rendering Workers
143 'XMLHttpRequestProgressEvent', 167 'XMLHttpRequestProgressEvent',
144 # Obsolete event for NaCl. 168 # Obsolete event for NaCl.
145 'ResourceProgressEvent', 169 'ResourceProgressEvent',
146 ] 170 ]
147 171
148 for interface in _removed_html_interfaces: 172 for interface in _removed_html_interfaces:
149 html_interface_renames[interface] = '_' + interface 173 html_interface_renames[interface] = '_' + interface
150 174
151 convert_to_future_members = monitored.Set( 175 convert_to_future_members = monitored.Set(
152 'htmlrenamer.converted_to_future_members', [ 176 'htmlrenamer.converted_to_future_members', [
(...skipping 614 matching lines...) Expand 10 before | Expand all | Expand 10 after
767 'KeyboardEvent.keyCode', 791 'KeyboardEvent.keyCode',
768 'KeyboardEvent.which', 792 'KeyboardEvent.which',
769 'Location.valueOf', 793 'Location.valueOf',
770 'MessageEvent.data', 794 'MessageEvent.data',
771 'MessageEvent.ports', 795 'MessageEvent.ports',
772 'MessageEvent.webkitInitMessageEvent', 796 'MessageEvent.webkitInitMessageEvent',
773 'MouseEvent.webkitMovementX', 797 'MouseEvent.webkitMovementX',
774 'MouseEvent.webkitMovementY', 798 'MouseEvent.webkitMovementY',
775 'MouseEvent.x', 799 'MouseEvent.x',
776 'MouseEvent.y', 800 'MouseEvent.y',
801 'Navigator.bluetooth',
777 'Navigator.registerServiceWorker', 802 'Navigator.registerServiceWorker',
778 'Navigator.unregisterServiceWorker', 803 'Navigator.unregisterServiceWorker',
779 'Navigator.isProtocolHandlerRegistered', 804 'Navigator.isProtocolHandlerRegistered',
780 'Navigator.unregisterProtocolHandler', 805 'Navigator.unregisterProtocolHandler',
806 'Navigator.usb',
781 'Node.compareDocumentPosition', 807 'Node.compareDocumentPosition',
782 'Node.get:DOCUMENT_POSITION_CONTAINED_BY', 808 'Node.get:DOCUMENT_POSITION_CONTAINED_BY',
783 'Node.get:DOCUMENT_POSITION_CONTAINS', 809 'Node.get:DOCUMENT_POSITION_CONTAINS',
784 'Node.get:DOCUMENT_POSITION_DISCONNECTED', 810 'Node.get:DOCUMENT_POSITION_DISCONNECTED',
785 'Node.get:DOCUMENT_POSITION_FOLLOWING', 811 'Node.get:DOCUMENT_POSITION_FOLLOWING',
786 'Node.get:DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC', 812 'Node.get:DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC',
787 'Node.get:DOCUMENT_POSITION_PRECEDING', 813 'Node.get:DOCUMENT_POSITION_PRECEDING',
788 'Node.get:childNodes', 814 'Node.get:childNodes',
789 'Node.get:prefix', 815 'Node.get:prefix',
790 'Node.hasAttributes', 816 'Node.hasAttributes',
791 'Node.isDefaultNamespace', 817 'Node.isDefaultNamespace',
792 'Node.isEqualNode', 818 'Node.isEqualNode',
793 'Node.isSameNode', 819 'Node.isSameNode',
794 'Node.isSupported', 820 'Node.isSupported',
795 'Node.lookupNamespaceURI', 821 'Node.lookupNamespaceURI',
796 'Node.lookupPrefix', 822 'Node.lookupPrefix',
797 'Node.normalize', 823 'Node.normalize',
798 'Node.set:nodeValue', 824 'Node.set:nodeValue',
799 'NodeFilter.acceptNode', 825 'NodeFilter.acceptNode',
800 'NodeIterator.expandEntityReferences', 826 'NodeIterator.expandEntityReferences',
801 'NodeIterator.filter', 827 'NodeIterator.filter',
802 'NodeList.item', 828 'NodeList.item',
803 'ParentNode.append', 829 'ParentNode.append',
804 'ParentNode.prepend', 830 'ParentNode.prepend',
831 'RTCPeerConnection.generateCertificate',
805 'ServiceWorkerMessageEvent.data', 832 'ServiceWorkerMessageEvent.data',
806 'ShadowRoot.getElementsByTagNameNS', 833 'ShadowRoot.getElementsByTagNameNS',
807 'SVGElement.getPresentationAttribute', 834 'SVGElement.getPresentationAttribute',
808 'SVGElementInstance.on:wheel', 835 'SVGElementInstance.on:wheel',
809 'Touch.get:webkitRadiusX', 836 'Touch.get:webkitRadiusX',
810 'Touch.get:webkitRadiusY', 837 'Touch.get:webkitRadiusY',
811 'Touch.get:webkitForce', 838 'Touch.get:webkitForce',
812 'Touch.get:webkitRotationAngle', 839 'Touch.get:webkitRotationAngle',
813 'WheelEvent.wheelDelta', 840 'WheelEvent.wheelDelta',
814 'WheelEvent.wheelDeltaX', 841 'WheelEvent.wheelDeltaX',
815 'WheelEvent.wheelDeltaY', 842 'WheelEvent.wheelDeltaY',
816 'Window.on:wheel', 843 'Window.on:wheel',
817 'WindowEventHandlers.on:beforeUnload', 844 'WindowEventHandlers.on:beforeUnload',
818 'WorkerGlobalScope.webkitIndexedDB', 845 'WorkerGlobalScope.webkitIndexedDB',
819 'XMLHttpRequest.open', 846 'XMLHttpRequest.open',
820 # TODO(jacobr): should these be removed? 847 # TODO(jacobr): should these be removed?
821 'Document.close', 848 'Document.close',
822 'Document.hasFocus', 849 'Document.hasFocus',
823 ]) 850 ])
824 851
825 # Manual dart: library name lookup. 852 # Manual dart: library name lookup.
826 _library_names = monitored.Dict('htmlrenamer._library_names', { 853 _library_names = monitored.Dict('htmlrenamer._library_names', {
827 'ANGLEInstancedArrays': 'web_gl', 854 'ANGLEInstancedArrays': 'web_gl',
828 'CHROMIUMSubscribeUniform': 'web_gl', 855 'CHROMIUMSubscribeUniform': 'web_gl',
829 'Database': 'web_sql', 856 'Database': 'web_sql',
830 'Navigator': 'html', 857 'Navigator': 'html',
831 'Window': 'html', 858 'Window': 'html',
859 'AnalyserNode': 'web_audio',
860 'AudioBufferCallback': 'web_audio',
861 'AudioBuffer': 'web_audio',
862 'AudioBufferSourceNode': 'web_audio',
863 'AudioContext': 'web_audio',
864 'AudioDestinationNode': 'web_audio',
865 'AudioListener': 'web_audio',
866 'AudioNode': 'web_audio',
867 'AudioParam': 'web_audio',
868 'AudioProcessingEvent': 'web_audio',
869 'AudioSourceNode': 'web_audio',
870 'BiquadFilterNode': 'web_audio',
871 'ChannelMergerNode': 'web_audio',
872 'ChannelSplitterNode': 'web_audio',
873 'ConvolverNode': 'web_audio',
874 'DelayNode': 'web_audio',
875 'DynamicsCompressorNode': 'web_audio',
876 'GainNode': 'web_audio',
877 'IIRFilterNode': 'web_audio',
878 'MediaElementAudioSourceNode': 'web_audio',
879 'MediaStreamAudioDestinationNode': 'web_audio',
880 'MediaStreamAudioSourceNode': 'web_audio',
881 'OfflineAudioCompletionEvent': 'web_audio',
882 'OfflineAudioContext': 'web_audio',
883 'OscillatorNode': 'web_audio',
884 'PannerNode': 'web_audio',
885 'PeriodicWave': 'web_audio',
886 'ScriptProcessorNode': 'web_audio',
887 'StereoPannerNode': 'web_audio',
888 'WaveShaperNode': 'web_audio',
889 'WindowWebAudio': 'web_audio',
832 }) 890 })
833 891
834 _library_ids = monitored.Dict('htmlrenamer._library_names', { 892 _library_ids = monitored.Dict('htmlrenamer._library_names', {
835 'ANGLEInstancedArrays': 'WebGl', 893 'ANGLEInstancedArrays': 'WebGl',
836 'CHROMIUMSubscribeUniform': 'WebGl', 894 'CHROMIUMSubscribeUniform': 'WebGl',
837 'Database': 'WebSql', 895 'Database': 'WebSql',
838 'Navigator': 'Html', 896 'Navigator': 'Html',
839 'Window': 'Html', 897 'Window': 'Html',
898 'AnalyserNode': 'WebAudio',
899 'AudioBufferCallback': 'WebAudio',
900 'AudioBuffer': 'WebAudio',
901 'AudioBufferSourceNode': 'WebAudio',
902 'AudioContext': 'WebAudio',
903 'AudioDestinationNode': 'WebAudio',
904 'AudioListener': 'WebAudio',
905 'AudioNode': 'WebAudio',
906 'AudioParam': 'WebAudio',
907 'AudioProcessingEvent': 'WebAudio',
908 'AudioSourceNode': 'WebAudio',
909 'BiquadFilterNode': 'WebAudio',
910 'ChannelMergerNode': 'WebAudio',
911 'ChannelSplitterNode': 'WebAudio',
912 'ConvolverNode': 'WebAudio',
913 'DelayNode': 'WebAudio',
914 'DynamicsCompressorNode': 'WebAudio',
915 'GainNode': 'WebAudio',
916 'IIRFilterNode': 'WebAudio',
917 'MediaElementAudioSourceNode': 'WebAudio',
918 'MediaStreamAudioDestinationNode': 'WebAudio',
919 'MediaStreamAudioSourceNode': 'WebAudio',
920 'OfflineAudioCompletionEvent': 'WebAudio',
921 'OfflineAudioContext': 'WebAudio',
922 'OscillatorNode': 'WebAudio',
923 'PannerNode': 'WebAudio',
924 'PeriodicWave': 'WebAudio',
925 'ScriptProcessorNode': 'WebAudio',
926 'StereoPannerNode': 'WebAudio',
927 'WaveShaperNode': 'WebAudio',
928 'WindowWebAudio': 'WebAudio',
840 }) 929 })
841 930
842 class HtmlRenamer(object): 931 class HtmlRenamer(object):
843 def __init__(self, database, metadata): 932 def __init__(self, database, metadata):
844 self._database = database 933 self._database = database
845 self._metadata = metadata 934 self._metadata = metadata
846 935
847 def RenameInterface(self, interface): 936 def RenameInterface(self, interface):
848 if 'Callback' in interface.ext_attrs: 937 if 'Callback' in interface.ext_attrs:
849 if interface.id in _removed_html_interfaces: 938 if interface.id in _removed_html_interfaces:
(...skipping 89 matching lines...) Expand 10 before | Expand all | Expand 10 after
939 for interface in self._database.Hierarchy(interface): 1028 for interface in self._database.Hierarchy(interface):
940 match = find_match(interface.id) 1029 match = find_match(interface.id)
941 if match: 1030 if match:
942 return match 1031 return match
943 1032
944 def GetLibraryName(self, interface): 1033 def GetLibraryName(self, interface):
945 # Some types have attributes merged in from many other interfaces. 1034 # Some types have attributes merged in from many other interfaces.
946 if interface.id in _library_names: 1035 if interface.id in _library_names:
947 return _library_names[interface.id] 1036 return _library_names[interface.id]
948 1037
949 # TODO(ager, blois): The conditional has been removed from indexed db, 1038 # Support for IDL conditional has been removed from indexed db, web_sql,
950 # so we can no longer determine the library based on the conditionals. 1039 # svg and web_gl so we can no longer determine the library based on conditio nal.
1040 # Use interface prefix to do that. web_audio interfaces have no common pref ix
1041 # - all audio interfaces added to _library_names/_library_ids.
951 if interface.id.startswith("IDB"): 1042 if interface.id.startswith("IDB"):
952 return 'indexed_db' 1043 return 'indexed_db'
953 if interface.id.startswith("SQL"): 1044 if interface.id.startswith("SQL"):
954 return 'web_sql' 1045 return 'web_sql'
955 if interface.id.startswith("SVG"): 1046 if interface.id.startswith("SVG"):
956 return 'svg' 1047 return 'svg'
957 if interface.id.startswith("WebGL") or interface.id.startswith("OES") \ 1048 if interface.id.startswith("WebGL") or interface.id.startswith("OES") \
958 or interface.id.startswith("EXT"): 1049 or interface.id.startswith("EXT"):
959 return 'web_gl' 1050 return 'web_gl'
960 1051
961 if 'Conditional' in interface.ext_attrs:
962 if 'WEB_AUDIO' in interface.ext_attrs['Conditional']:
963 return 'web_audio'
964 if 'INDEXED_DATABASE' in interface.ext_attrs['Conditional']:
965 return 'indexed_db'
966 if 'SQL_DATABASE' in interface.ext_attrs['Conditional']:
967 return 'web_sql'
968
969 if interface.id in typed_array_renames: 1052 if interface.id in typed_array_renames:
970 return 'typed_data' 1053 return 'typed_data'
971 1054
972 return 'html' 1055 return 'html'
973 1056
974 def GetLibraryId(self, interface): 1057 def GetLibraryId(self, interface):
975 # Some types have attributes merged in from many other interfaces. 1058 # Some types have attributes merged in from many other interfaces.
976 if interface.id in _library_ids: 1059 if interface.id in _library_ids:
977 return _library_ids[interface.id] 1060 return _library_ids[interface.id]
978 1061
979 # TODO(ager, blois): The conditional has been removed from indexed db, 1062 # Support for IDL conditional has been removed from indexed db, web_sql,
980 # so we can no longer determine the library based on the conditionals. 1063 # svg and web_gl so we can no longer determine the library based on conditio nal.
1064 # Use interface prefix to do that. web_audio interfaces have no common pref ix
1065 # - all audio interfaces added to _library_names/_library_ids.
981 if interface.id.startswith("IDB"): 1066 if interface.id.startswith("IDB"):
982 return 'IndexedDb' 1067 return 'IndexedDb'
983 if interface.id.startswith("SQL"): 1068 if interface.id.startswith("SQL"):
984 return 'WebSql' 1069 return 'WebSql'
985 if interface.id.startswith("SVG"): 1070 if interface.id.startswith("SVG"):
986 return 'Svg' 1071 return 'Svg'
987 if interface.id.startswith("WebGL") or interface.id.startswith("OES") \ 1072 if interface.id.startswith("WebGL") or interface.id.startswith("OES") \
988 or interface.id.startswith("EXT"): 1073 or interface.id.startswith("EXT"):
989 return 'WebGl' 1074 return 'WebGl'
990 1075
991 if 'Conditional' in interface.ext_attrs:
992 if 'WEB_AUDIO' in interface.ext_attrs['Conditional']:
993 return 'WebAudio'
994 if 'INDEXED_DATABASE' in interface.ext_attrs['Conditional']:
995 return 'IndexedDb'
996 if 'SQL_DATABASE' in interface.ext_attrs['Conditional']:
997 return 'WebSql'
998
999 if interface.id in typed_array_renames: 1076 if interface.id in typed_array_renames:
1000 return 'TypedData' 1077 return 'TypedData'
1001 1078
1002 return 'Html' 1079 return 'Html'
1003 1080
1004 def DartifyTypeName(self, type_name): 1081 def DartifyTypeName(self, type_name):
1005 """Converts a DOM name to a Dart-friendly class name. """ 1082 """Converts a DOM name to a Dart-friendly class name. """
1006 1083
1007 if type_name in html_interface_renames: 1084 if type_name in html_interface_renames:
1008 return html_interface_renames[type_name] 1085 return html_interface_renames[type_name]
(...skipping 21 matching lines...) Expand all
1030 1107
1031 # We're looking for a sequence of letters which start with capital letter 1108 # We're looking for a sequence of letters which start with capital letter
1032 # then a series of caps and finishes with either the end of the string or 1109 # then a series of caps and finishes with either the end of the string or
1033 # a capital letter. 1110 # a capital letter.
1034 # The [0-9] check is for names such as 2D or 3D 1111 # The [0-9] check is for names such as 2D or 3D
1035 # The following test cases should match as: 1112 # The following test cases should match as:
1036 # WebKitCSSFilterValue: WebKit(C)(SS)(F)ilterValue 1113 # WebKitCSSFilterValue: WebKit(C)(SS)(F)ilterValue
1037 # XPathNSResolver: (X)()(P)ath(N)(S)(R)esolver (no change) 1114 # XPathNSResolver: (X)()(P)ath(N)(S)(R)esolver (no change)
1038 # IFrameElement: (I)()(F)rameElement (no change) 1115 # IFrameElement: (I)()(F)rameElement (no change)
1039 return re.sub(r'([A-Z])([A-Z]{2,})([A-Z]|$)', toLower, name) 1116 return re.sub(r'([A-Z])([A-Z]{2,})([A-Z]|$)', toLower, name)
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698