|
OLD | NEW |
---|---|
(Empty) | |
1 #!/usr/bin/env python | |
2 # Copyright (c) 2012 The Chromium Authors. All rights reserved. | |
3 # Use of this source code is governed by a BSD-style license that can be | |
4 # found in the LICENSE file. | |
5 | |
6 # Example run from the server2/ directory: | |
7 # $ converter.py ../static/ ../../api templates/articles/ templates/intros/ | |
8 # templates/public/ static/images/ -r | |
9 | |
10 import json | |
11 import optparse | |
12 import os | |
13 import re | |
14 import shutil | |
15 import subprocess | |
16 import sys | |
17 | |
18 sys.path.append(os.path.join(sys.path[0], os.pardir, os.pardir, os.pardir, | |
19 os.pardir, os.pardir, 'tools')) | |
20 import json_comment_eater | |
21 | |
22 from converter_html_parser import HandleDocFamily | |
23 from docs_server_utils import SanitizeAPIName | |
24 | |
25 IGNORED_FILES = [ | |
26 # These are custom files. | |
27 '404', | |
28 'apps_api_index', | |
29 'api_index', | |
30 'experimental', | |
31 'samples', | |
32 'index', | |
33 # These are APIs that should not have docs. | |
34 'test', | |
35 'experimental_idltest', | |
36 ] | |
37 | |
38 # These are mappings for APIs that have no intros. They are needed because the | |
39 # names of the JSON files do not give enough information on the actual API name. | |
40 CUSTOM_MAPPINGS = { | |
41 'experimental_input_virtual_keyboard': 'experimental_input_virtualKeyboard', | |
42 'input_ime': 'input_ime' | |
43 } | |
44 | |
45 # From api_page_generator.js:548. | |
not at google - send to devlin
2012/08/07 02:42:14
Make comment like
These are the extension-only AP
cduvall
2012/08/07 20:21:38
Done.
| |
46 EXTENSIONS_ONLY = [ | |
47 'browserAction', | |
48 'extension', | |
49 'fileBrowserHandler', | |
not at google - send to devlin
2012/08/07 02:42:14
this is in _permission_features.json so you should
cduvall
2012/08/07 20:21:38
Done.
| |
50 'fontSettings', | |
not at google - send to devlin
2012/08/07 02:42:14
ditto, and looks like it's available for platform_
cduvall
2012/08/07 20:21:38
Done.
| |
51 'input.ime', | |
52 'omnibox', | |
53 'pageAction', | |
54 'scriptBadge', | |
55 'windows', | |
56 'experimental.devtools.audits', | |
57 'experimental.devtools.console', | |
58 'experimental.discovery', | |
59 'experimental.infobars', | |
60 'experimental.commands', | |
not at google - send to devlin
2012/08/07 02:42:14
not experimental anymore, so can levae this out.
cduvall
2012/08/07 20:21:38
Done.
| |
61 'experimental.offscreenTabs', | |
62 'experimental.processes', | |
63 'experimental.speechInput' | |
64 ] | |
65 | |
66 def _ReadFile(filename): | |
67 with open(filename, 'r') as f: | |
68 return f.read() | |
69 | |
70 def _WriteFile(filename, data): | |
71 with open(filename, 'w+') as f: | |
72 f.write(data) | |
73 | |
74 def _UnixName(name): | |
75 """Returns the unix_style name for a given lowerCamelCase string. | |
76 Shamelessly stolen from json_schema_compiler/model.py. | |
77 """ | |
78 name = os.path.splitext(name)[0] | |
79 s1 = re.sub('([a-z])([A-Z])', r'\1_\2', name) | |
80 s2 = re.sub('([A-Z]+)([A-Z][a-z])', r'\1_\2', s1) | |
81 return s2.replace('.', '_').lower() | |
82 | |
83 def _GetDestinations(api_name, api_dir): | |
84 if api_name in EXTENSIONS_ONLY: | |
85 return ['extensions'] | |
86 if api_name.startswith('app'): | |
87 return ['apps'] | |
88 with open(os.path.join(api_dir, '_permission_features.json')) as f: | |
89 permissions = json.loads(json_comment_eater.Nom(f.read())) | |
90 permissions_key = api_name.replace('experimental_', '').replace('_', '.') | |
91 if permissions_key in permissions: | |
92 return_list = [] | |
93 types = permissions[permissions_key]['extension_types'] | |
94 if ('packaged_app' in types or | |
95 'hosted_app' in types or | |
96 'platform_app' in types): | |
97 return_list.append('apps') | |
98 if 'extension' in types: | |
99 return_list.append('extensions') | |
100 return return_list | |
101 return ['extensions', 'apps'] | |
102 | |
103 def _ListAllAPIs(dirname): | |
104 all_files = [] | |
105 for path, dirs, files in os.walk(dirname): | |
106 if path == '.': | |
107 all_files.extend([f for f in files | |
108 if f.endswith('.json') or f.endswith('.idl')]) | |
109 else: | |
110 all_files.extend([os.path.join(path, f) for f in files | |
111 if f.endswith('.json') or f.endswith('.idl')]) | |
112 dirname = dirname.rstrip('/') + '/' | |
113 return [f[len(dirname):] for f in all_files | |
114 if os.path.splitext(f)[0].split('_')[-1] not in ['private', | |
115 'internal']] | |
116 | |
117 def _MakeArticleTemplate(filename, path): | |
118 is_apps = 'is_apps:TRUE' if path == 'apps' else 'is_apps:FALSE' | |
119 return ('{{+partials.standard_%s_article article:intros.%s %s}}' % | |
120 (path, filename, is_apps)) | |
121 | |
122 def _MakeAPITemplate(intro_name, path, api_name, has_intro): | |
123 is_apps = 'is_apps:TRUE' if path == 'apps' else 'is_apps:FALSE' | |
124 if has_intro: | |
125 return ('{{+partials.standard_%s_api api:apis.%s intro:intros.%s %s}}' % | |
126 (path, api_name, intro_name, is_apps)) | |
127 else: | |
128 return ('{{+partials.standard_%s_api api:apis.%s %s}}' % | |
129 (path, api_name, is_apps)) | |
130 | |
131 def _GetAPIPath(name, api_dir): | |
132 api_files = _ListAllAPIs(api_dir) | |
133 for filename in api_files: | |
134 if name == _UnixName(SanitizeAPIName(filename)): | |
135 return _UnixName(filename) | |
136 | |
137 def _CleanAPIs(source_dir, api_dir, intros_dest, template_dest, exclude): | |
138 source_files = os.listdir(source_dir) | |
139 source_files_unix = set(_UnixName(f) for f in source_files) | |
140 api_files = set(_UnixName(SanitizeAPIName(f)) for f in _ListAllAPIs(api_dir)) | |
141 intros_files = set(_UnixName(f) for f in os.listdir(intros_dest)) | |
142 to_delete = api_files - source_files_unix - set(exclude) | |
143 for path, dirs, files in os.walk(template_dest): | |
144 for filename in files: | |
145 no_ext = os.path.splitext(filename)[0] | |
146 # Check for changes like appWindow -> app.window. | |
147 if (_UnixName(filename) in source_files_unix - set(exclude) and | |
148 no_ext not in source_files): | |
149 os.remove(os.path.join(path, filename)) | |
150 if _UnixName(filename) in to_delete: | |
151 try: | |
152 os.remove(os.path.join(intros_dest, filename)) | |
153 except OSError: | |
154 pass | |
155 os.remove(os.path.join(path, filename)) | |
156 | |
157 def _FormatFile(contents, path, name, image_dest, replace, is_api): | |
158 # Copy all images referenced in the page. | |
159 for image in re.findall(r'="\.\./images/([^"]*)"', contents): | |
160 if not replace and os.path.exists(os.path.join(image_dest, image)): | |
161 continue | |
162 if '/' in image: | |
163 try: | |
164 os.makedirs(os.path.join(image_dest, image.rsplit('/', 1)[0])) | |
165 except: | |
166 pass | |
167 shutil.copy( | |
168 os.path.join(path, os.pardir, 'images', image), | |
169 os.path.join(image_dest, image)) | |
170 contents = re.sub(r'<!--.*?(BEGIN|END).*?-->', r'', contents) | |
171 contents = re.sub(r'\.\./images', r'{{static}}/images', contents) | |
172 exp = re.compile(r'<div[^>.]*?id="pageData-showTOC"[^>.]*?>.*?</div>', | |
173 flags=re.DOTALL) | |
174 contents = re.sub(exp, r'', contents) | |
175 exp = re.compile(r'<div[^>.]*?id="pageData-name"[^>.]*?>(.*?)</div>', | |
176 flags=re.DOTALL) | |
177 if is_api: | |
178 contents = re.sub(exp, r'', contents) | |
179 else: | |
180 contents = re.sub(exp, r'<h1>\1</h1>', contents) | |
181 contents = contents.strip() | |
182 contents = HandleDocFamily(contents) | |
183 | |
184 # Attempt to guess if the page has no title. | |
185 if '<h1' not in contents and not is_api: | |
186 title = _UnixName(name) | |
187 title = ' '.join([part[0].upper() + part[1:] for part in title.split('_')]) | |
188 contents = ('<h1 class="page_title">%s</h1>' % title) + contents | |
189 return contents | |
190 | |
191 def _ProcessName(name): | |
192 processed_name = [] | |
193 if name.startswith('experimental_'): | |
194 name = name[len('experimental_'):] | |
195 processed_name.append('experimental_') | |
196 parts = name.split('_') | |
197 processed_name.append(parts[0]) | |
198 processed_name.extend([p[0].upper() + p[1:] for p in parts[1:]]) | |
199 return ''.join(processed_name) | |
200 | |
201 def _MoveAllFiles(source_dir, | |
202 api_dir, | |
203 articles_dest, | |
204 intros_dest, | |
205 template_dest, | |
206 image_dest, | |
207 replace=False, | |
208 exclude_dir=None, | |
209 svn=False): | |
210 if exclude_dir is None: | |
211 exclude_files = [] | |
212 else: | |
213 exclude_files = [_UnixName(f) for f in os.listdir(exclude_dir)] | |
214 exclude_files.extend(IGNORED_FILES) | |
215 api_files = _ListAllAPIs(api_dir) | |
216 original_files = [] | |
217 for path, dirs, files in os.walk(template_dest): | |
218 original_files.extend(files) | |
219 if replace: | |
220 _CleanAPIs(source_dir, api_dir, intros_dest, template_dest, exclude_files) | |
221 files = set(os.listdir(source_dir)) | |
222 unix_files = [_UnixName(f) for f in files] | |
223 for name in [SanitizeAPIName(f) for f in _ListAllAPIs(api_dir)]: | |
224 if _UnixName(name) not in unix_files: | |
225 files.add(name + '.html') | |
226 for file_ in files: | |
227 if (_UnixName(file_) in exclude_files or | |
228 file_.startswith('.') or | |
229 file_.startswith('_')): | |
230 continue | |
231 _MoveSingleFile(source_dir, | |
232 file_, | |
233 api_dir, | |
234 articles_dest, | |
235 intros_dest, | |
236 template_dest, | |
237 image_dest, | |
238 replace=replace, | |
239 svn=svn, | |
240 original_files=original_files) | |
241 | |
242 def _MoveSingleFile(source_dir, | |
243 source_file, | |
244 api_dir, | |
245 articles_dest, | |
246 intros_dest, | |
247 template_dest, | |
248 image_dest, | |
249 replace=False, | |
250 svn=False, | |
251 original_files=None): | |
252 unix_name = _UnixName(source_file) | |
253 is_api = unix_name in [_UnixName(SanitizeAPIName(f)) | |
254 for f in _ListAllAPIs(api_dir)] | |
255 if unix_name in CUSTOM_MAPPINGS: | |
256 processed_name = CUSTOM_MAPPINGS[unix_name] | |
257 else: | |
258 processed_name = os.path.splitext(source_file)[0].replace('.', '_') | |
259 if (is_api and | |
260 '_' in source_file.replace('experimental_', '') and | |
261 not os.path.exists(os.path.join(source_dir, source_file))): | |
262 processed_name = _ProcessName(processed_name) | |
263 if (original_files is None or | |
264 processed_name + '.html' not in original_files): | |
265 print 'WARNING: The correct name of this file was guessed:' | |
266 print | |
267 print '%s -> %s' % (os.path.splitext(source_file)[0], processed_name) | |
268 print | |
269 print ('If this is incorrect, change |CUSTOM_MAPPINGS| in chrome/' | |
270 'common/extensions/docs/server2/converter.py.') | |
271 try: | |
272 static_data = _FormatFile(_ReadFile(os.path.join(source_dir, source_file)), | |
273 source_dir, | |
274 source_file, | |
275 image_dest, | |
276 replace, | |
277 is_api) | |
278 except IOError: | |
279 static_data = None | |
280 for path in _GetDestinations(processed_name, api_dir): | |
281 template_file = os.path.join(template_dest, path, processed_name + '.html') | |
282 if is_api: | |
283 template_data = _MakeAPITemplate(processed_name, | |
284 path, | |
285 _GetAPIPath(unix_name, api_dir), | |
286 static_data is not None) | |
287 static_file = os.path.join(intros_dest, processed_name + '.html') | |
288 else: | |
289 template_data = _MakeArticleTemplate(unix_name, path) | |
290 static_file = os.path.join(articles_dest, processed_name + '.html') | |
291 if replace or not os.path.exists(template_file): | |
292 _WriteFile(template_file, template_data) | |
293 if static_data is not None and (replace or not os.path.exists(static_file)): | |
294 if svn: | |
295 if os.path.exists(static_file): | |
296 subprocess.call(['svn', 'rm', '--force', static_file]) | |
297 subprocess.call(['svn', | |
298 'cp', | |
299 os.path.join(source_dir, source_file), | |
300 static_file]) | |
301 _WriteFile(static_file, static_data) | |
302 | |
303 if __name__ == '__main__': | |
304 parser = optparse.OptionParser( | |
305 description='Converts static files from the old documentation system to ' | |
306 'the new one. If run without -f, all the files in |src| will ' | |
307 'be converted.', | |
308 usage='usage: %prog [options] static_src_dir [-f static_src_file] ' | |
309 'api_dir articles_dest intros_dest template_dest image_dest') | |
310 parser.add_option('-f', | |
311 '--file', | |
312 action='store_true', | |
313 default=False, | |
314 help='convert single file') | |
315 parser.add_option('-s', | |
316 '--svn', | |
317 action='store_true', | |
318 default=False, | |
319 help='use svn copy to copy intro files') | |
320 parser.add_option('-e', | |
321 '--exclude', | |
322 default=None, | |
323 help='exclude files matching the names in this dir') | |
324 parser.add_option('-r', | |
325 '--replace', | |
326 action='store_true', | |
327 default=False, | |
328 help='replace existing files') | |
329 (opts, args) = parser.parse_args() | |
330 if (not opts.file and len(args) != 6) or (opts.file and len(args) != 7): | |
331 parser.error('incorrect number of arguments.') | |
332 | |
333 if opts.file: | |
334 _MoveSingleFile(*args, replace=opts.replace, svn=opts.svn) | |
335 else: | |
336 _MoveAllFiles(*args, | |
337 replace=opts.replace, | |
338 exclude_dir=opts.exclude, | |
339 svn=opts.svn) | |
OLD | NEW |