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
27 from urllib2
import urlopen
28 from getopt
import gnu_getopt
30 from fmbook
import log
, Book
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')
39 #spaces?, digits!, dot?, digits?, spaces?
40 #return re.compile(r'^\s*[+-]?\d+\.?\d*\s*$').match
47 def isfloat_or_auto(s
):
48 return isfloat(s
) or s
.lower() in ('', 'auto')
51 # 10 or 13 digits with any number of hyphens, perhaps with check-digit missing
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).
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
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())
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'.
89 query
= cgi
.FieldStorage()
90 options
, args
= gnu_getopt(sys
.argv
[1:], '', [x
+ '=' for x
in ARG_VALIDATORS
])
91 options
= dict(options
)
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')
98 if validator
is not None and not validator(value
):
99 log("argument '%s' is not valid ('%s')" % (key
, value
))
103 log(data
, debug
='STARTUP')
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()):
126 url
= 'http://%s/bin/view/TWiki/WebLeftBarWebsList?skin=text' % server
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
))
140 #order by increasing areal size.
141 def calc_size(name
, pointsize
, klass
):
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."""
160 if isinstance(x
, str):
163 # couple: value, name
165 options
.append('<option selected="selected" value="%s">%s</option>' % x
)
167 options
.append('<option value="%s">%s</option>' % x
)
169 # triple: value, class, name
171 options
.append('<option selected="selected" value="%s" class="%s">%s</option>' % x
)
173 options
.append('<option value="%s" class="%s">%s</option>' % x
)
175 return '\n'.join(options
)
178 """Make a list of strings into html <li> items, to fit in a <ul>
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"""
186 cssfile
= SERVER_DEFAULTS
[server
]['css-%s' % mode
]
195 """Links to various example pdfs."""
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
)
201 links
.append('<a href="%s?script=%s">%s</a>' % (config
.FONT_LIST_URL
, script
, script
))
205 def make_progress_page(book
, bookname
, mode
):
206 f
= open(PROGRESS_TEMPLATE
)
209 progress_list
= ''.join('<li id="%s">%s</li>\n' % x
[:2] for x
in config
.PROGRESS_POINTS
214 'bookname': bookname
,
215 'progress_list': progress_list
,
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>'
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
249 #get all the values including sizes first
251 for k
, extrema
in config
.PAGE_EXTREMA
.iteritems():
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
))
257 if v
< extrema
[0] or v
> extrema
[1]:
258 log('rejecting %s: outside %s' % (v
,) + extrema
)
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
266 size
= args
.get('booksize', config
.DEFAULT_SIZE
)
268 settings
.update(config
.PAGE_SIZE_DATA
[size
])
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']
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
):
284 if cgi_context(args
):
285 print "Content-type: text/html; charset=utf-8\n"
291 def mode_booklist(args
):
292 print optionise(get_book_list(args
.get('server', config
.DEFAULT_SERVER
)),
293 default
=args
.get('book'))
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'))
303 f
= open(FORM_TEMPLATE
)
306 f
= open(config
.FONT_LIST_INCLUDE
)
307 font_list
= [x
.strip() for x
in f
if x
.strip()]
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
)
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
),
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
)}
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
)
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
):
359 book
.set_title(args
.get('title'))
360 book
.add_css(args
.get('css'), mode
)
361 book
.add_section_titles()
368 book
.notify_watcher('finished')
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
))
380 mode
= args
.get('mode')
381 if mode
is None and 'book' in args
:
384 if not args
and not cgi_context(args
):
388 output_function
= globals().get('mode_%s' % mode
, mode_form
)
389 output_function(args
)