name attribute in <ul> confused the form -- use id only
[objavi2.git] / objavi2.py
blobe4c64bd825ae555f4158e372b126f0153e2c4b0c
1 #!/usr/bin/python
3 # Part of Objavi2, which turns html manuals into books
5 # Copyright (C) 2009 Douglas Bagnall
7 # This program is free software; you can redistribute it and/or modify
8 # it under the terms of the GNU General Public License as published by
9 # the Free Software Foundation; either version 2 of the License, or
10 # (at your option) any later version.
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
17 # You should have received a copy of the GNU General Public License along
18 # with this program; if not, write to the Free Software Foundation, Inc.,
19 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
21 """Make a pdf from the specified book."""
22 from __future__ import with_statement
24 import os, sys
25 import cgi
26 import re, time
27 from urllib2 import urlopen
28 from getopt import gnu_getopt
30 from fmbook import log, Book
32 import config
33 from config import SERVER_DEFAULTS, DEFAULT_SERVER
35 FORM_TEMPLATE = os.path.abspath('templates/form.html')
36 PROGRESS_TEMPLATE = os.path.abspath('templates/progress.html')
38 def isfloat(s):
39 #spaces?, digits!, dot?, digits?, spaces?
40 #return re.compile(r'^\s*[+-]?\d+\.?\d*\s*$').match
41 try:
42 float(s)
43 return True
44 except ValueError:
45 return False
47 def isfloat_or_auto(s):
48 return isfloat(s) or s.lower() in ('', 'auto')
50 def is_isbn(s):
51 # 10 or 13 digits with any number of hyphens, perhaps with check-digit missing
52 s =s.replace('-', '')
53 return (re.match(r'^\d+[\dXx*]$', s) and len(s) in (9, 10, 12, 13))
56 # ARG_VALIDATORS is a mapping between the expected cgi arguments and
57 # functions to validate their values. (None means no validation).
58 ARG_VALIDATORS = {
59 "book": re.compile(r'^(\w+/?)*\w+$').match, # can be: BlahBlah/Blah_Blah
60 "css": None, # an url, empty (for default), or css content
61 "title": lambda x: len(x) < 999,
62 #"header": None, # header text, UNUSED
63 "isbn": is_isbn,
64 "license": config.LICENSES.__contains__,
65 "server": SERVER_DEFAULTS.__contains__,
66 "engine": config.ENGINES.__contains__,
67 "booksize": config.PAGE_SIZE_DATA.__contains__,
68 "page_width": isfloat,
69 "page_height": isfloat,
70 "gutter": isfloat_or_auto,
71 "top_margin": isfloat_or_auto,
72 "side_margin": isfloat_or_auto,
73 "bottom_margin": isfloat_or_auto,
74 "columns": isfloat_or_auto,
75 "column_margin": isfloat_or_auto,
76 "cgi-context": lambda x: x.lower() in '1true0false',
77 "mode": config.CGI_MODES.__contains__,
78 "pdftype": lambda x: config.CGI_MODES.get(x, [False])[0],
79 "rotate": u"rotate".__eq__,
82 __doc__ += '\nValid arguments are: %s.\n' % ', '.join(ARG_VALIDATORS.keys())
84 def parse_args():
85 """Read and validate CGI or commandline arguments, putting the
86 good ones into the returned dictionary. Command line arguments
87 should be in the form --title='A Book'.
88 """
89 query = cgi.FieldStorage()
90 options, args = gnu_getopt(sys.argv[1:], '', [x + '=' for x in ARG_VALIDATORS])
91 options = dict(options)
92 log(options)
93 data = {}
94 for key, validator in ARG_VALIDATORS.items():
95 value = query.getfirst(key, options.get('--' + key, None))
96 log('%s: %s' % (key, value), debug='STARTUP')
97 if value is not None:
98 if validator is not None and not validator(value):
99 log("argument '%s' is not valid ('%s')" % (key, value))
100 continue
101 data[key] = value
103 log(data, debug='STARTUP')
104 return data
106 def get_server_list():
107 return sorted(SERVER_DEFAULTS.keys())
110 def get_book_list(server):
111 """Ask the server for a list of books. Floss Manual TWikis keep such a list at
112 /bin/view/TWiki/WebLeftBarWebsList?skin=text but it needs a bit of processing
114 If BOOK_LIST_CACHE is non-zero, the book list won't be re-fetched
115 in that many seconds, rather it will be read from disk.
117 if config.BOOK_LIST_CACHE:
118 cache_name = os.path.join(config.BOOK_LIST_CACHE_DIR, '%s.booklist' % server)
119 if (os.path.exists(cache_name) and
120 os.stat(cache_name).st_mtime + config.BOOK_LIST_CACHE > time.time()):
121 f = open(cache_name)
122 s = f.read()
123 f.close()
124 return s.split()
126 url = 'http://%s/bin/view/TWiki/WebLeftBarWebsList?skin=text' % server
127 #XXX should use lxml
128 log(url)
129 f = urlopen(url)
130 s = f.read()
131 f.close()
132 items = sorted(re.findall(r'/bin/view/([\w/]+)/WebHome', s))
133 if config.BOOK_LIST_CACHE:
134 f = open(cache_name, 'w')
135 f.write('\n'.join(items))
136 f.close()
137 return items
139 def get_size_list():
140 #order by increasing areal size.
141 def calc_size(name, pointsize, klass):
142 if pointsize:
143 mmx = pointsize[0] * config.POINT_2_MM
144 mmy = pointsize[1] * config.POINT_2_MM
145 return (mmx * mmy, name, klass,
146 '%s (%dmm x %dmm)' % (name, mmx, mmy))
148 return (0, name, klass, name) # presumably 'custom'
150 return [x[1:] for x in sorted(calc_size(k, v.get('pointsize'), v.get('class', ''))
151 for k, v in config.PAGE_SIZE_DATA.iteritems())
155 def optionise(items, default=None):
156 """Make a list of strings into an html option string, as would fit
157 inside <select> tags."""
158 options = []
159 for x in items:
160 if isinstance(x, str):
161 x = (x, x)
162 if len(x) == 2:
163 # couple: value, name
164 if x[0] == default:
165 options.append('<option selected="selected" value="%s">%s</option>' % x)
166 else:
167 options.append('<option value="%s">%s</option>' % x)
168 else:
169 # triple: value, class, name
170 if x[0] == default:
171 options.append('<option selected="selected" value="%s" class="%s">%s</option>' % x)
172 else:
173 options.append('<option value="%s" class="%s">%s</option>' % x)
175 return '\n'.join(options)
177 def listify(items):
178 """Make a list of strings into html <li> items, to fit in a <ul>
179 or <ol> element."""
180 return '\n'.join('<li>%s</li>' % x for x in items)
183 def get_default_css(server=DEFAULT_SERVER, mode='book'):
184 """Get the default CSS text for the selected server"""
185 log(server)
186 cssfile = SERVER_DEFAULTS[server]['css-%s' % mode]
187 log(cssfile)
188 f = open(cssfile)
189 s = f.read()
190 f.close()
191 #log(s)
192 return s
194 def font_links():
195 """Links to various example pdfs."""
196 links = []
197 for script in os.listdir(config.FONT_EXAMPLE_SCRIPT_DIR):
198 if not script.isalnum():
199 log("warning: font-sample %s won't work; skipping" % script)
200 continue
201 links.append('<a href="%s?script=%s">%s</a>' % (config.FONT_LIST_URL, script, script))
202 return links
205 def make_progress_page(book, bookname, mode):
206 f = open(PROGRESS_TEMPLATE)
207 template = f.read()
208 f.close()
209 progress_list = ''.join('<li id="%s">%s</li>\n' % x[:2] for x in config.PROGRESS_POINTS
210 if mode in x[2])
212 d = {
213 'book': book,
214 'bookname': bookname,
215 'progress_list': progress_list,
217 print template % d
218 def progress_notifier(message):
219 print ('<script type="text/javascript">\n'
220 'objavi_show_progress("%s");\n'
221 '</script>' % message
223 if message == 'finished':
224 print '</body></html>'
225 sys.stdout.flush()
226 return progress_notifier
228 def print_progress(message):
229 print '******* got message "%s"' %message
231 def make_book_name(book, server):
232 lang = SERVER_DEFAULTS.get(server, SERVER_DEFAULTS[DEFAULT_SERVER])['lang']
233 book = ''.join(x for x in book if x.isalnum())
234 return '%s-%s-%s.pdf' % (book, lang,
235 time.strftime('%Y.%m.%d-%H.%M.%S'))
238 def get_page_settings(args):
239 """Find the size and any optional layout settings.
241 args['booksize'] is either a keyword describing a size or
242 'custom'. If it is custom, the form is inspected for specific
243 dimensions -- otherwise these are ignored.
245 The margins, gutter, number of columns, and column
246 margins all set themselves automatically based on the page
247 dimensions, but they can be overridden. Any that are are
248 collected here."""
249 #get all the values including sizes first
250 settings = {}
251 for k, extrema in config.PAGE_EXTREMA.iteritems():
252 try:
253 v = float(args.get(k))
254 except (ValueError, TypeError):
255 log("don't like %r as a float value for %s!" % (args.get(k), k))
256 continue
257 if v < extrema[0] or v > extrema[1]:
258 log('rejecting %s: outside %s' % (v,) + extrema)
259 else:
260 log('found %s=%s' % (k, v))
261 settings[k] = v * extrema[2]
263 #now if args['size'] is not 'custom', the width height above
264 # is overruled.
266 size = args.get('booksize', config.DEFAULT_SIZE)
267 if size != 'custom':
268 settings.update(config.PAGE_SIZE_DATA[size])
269 else:
270 #will raise KeyError if width, height aren't set
271 settings['pointsize'] = (settings['page_width'], settings['page_height'])
272 del settings['page_width']
273 del settings['page_height']
275 return settings
279 def cgi_context(args):
280 return 'SERVER_NAME' in os.environ or args.get('cgi-context', 'NO').lower() in '1true'
282 def output_and_exit(f):
283 def output(args):
284 if cgi_context(args):
285 print "Content-type: text/html; charset=utf-8\n"
286 f(args)
287 sys.exit()
288 return output
290 @output_and_exit
291 def mode_booklist(args):
292 print optionise(get_book_list(args.get('server', config.DEFAULT_SERVER)),
293 default=args.get('book'))
295 @output_and_exit
296 def mode_css(args):
297 #XX sending as text/html, but it doesn't really matter
298 print get_default_css(args.get('server', config.DEFAULT_SERVER), args.get('pdftype', 'book'))
301 @output_and_exit
302 def mode_form(args):
303 f = open(FORM_TEMPLATE)
304 template = f.read()
305 f.close()
306 f = open(config.FONT_LIST_INCLUDE)
307 font_list = [x.strip() for x in f if x.strip()]
308 f.close()
309 server = args.get('server', config.DEFAULT_SERVER)
310 book = args.get('book')
311 size = args.get('booksize', config.DEFAULT_SIZE)
312 engine = args.get('engine', config.DEFAULT_ENGINE)
313 d = {
314 'server_options': optionise(get_server_list(), default=server),
315 'book_options': optionise(get_book_list(server), default=book),
316 'size_options': optionise(get_size_list(), default=size),
317 'engines': optionise(config.ENGINES.keys(), default=engine),
318 'css': get_default_css(server),
319 'font_links': listify(font_links()),
320 'font_list': listify(font_list),
321 'default_license' : config.DEFAULT_LICENSE,
322 'licenses' : optionise(config.LICENSES, default=config.DEFAULT_LICENSE),
323 None: '',
326 form = []
327 for id, title, type, source, classes, epilogue in config.FORM_INPUTS:
328 val = d.get(source, '')
329 e = config.FORM_ELEMENT_TYPES[type] % locals()
330 form.append('\n<div id="%(id)s_div" class="form-item %(classes)s">\n'
331 '<div class="input_title">%(title)s</div>\n'
332 '<div class="input_contents"> %(e)s %(epilogue)s\n</div>'
333 '</div>\n' % locals())
336 print template % {'form': ''.join(form)}
339 @output_and_exit
340 def mode_book(args):
341 # so we're making a book.
342 bookid = args.get('book')
343 server = args.get('server', config.DEFAULT_SERVER)
344 engine = args.get('engine', config.DEFAULT_ENGINE)
345 page_settings = get_page_settings(args)
346 bookname = make_book_name(bookid, server)
348 if cgi_context(args):
349 progress_bar = make_progress_page(bookid, bookname)
350 else:
351 progress_bar = print_progress
353 with Book(bookid, server, bookname, page_settings=page_settings, engine=engine,
354 watcher=progress_bar, isbn=args.get('isbn'),
355 license=args.get('license')) as book:
356 if cgi_context(args):
357 book.spawn_x()
358 book.load()
359 book.set_title(args.get('title'))
360 book.add_css(args.get('css'), mode)
361 book.add_section_titles()
362 book.make_book_pdf()
364 if "rotate" in args:
365 book.rotate180()
367 book.publish_pdf()
368 book.notify_watcher('finished')
369 #book.cleanup()
373 if __name__ == '__main__':
374 _valid_inputs = set(ARG_VALIDATORS)
375 _form_inputs = set(x[0] for x in config.FORM_INPUTS)
376 log("valid but not used inputs: %s" % (_valid_inputs - _form_inputs))
377 log("invalid form inputs: %s" % (_form_inputs - _valid_inputs))
379 args = parse_args()
380 mode = args.get('mode')
381 if mode is None and 'book' in args:
382 mode = 'book'
384 if not args and not cgi_context(args):
385 print __doc__
386 sys.exit()
388 output_function = globals().get('mode_%s' % mode, mode_form)
389 output_function(args)