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')
50 # ARG_VALIDATORS is a mapping between the expected cgi arguments and
51 # functions to validate their values. (None means no validation).
53 "book": re
.compile(r
'^(\w+/?)*\w+$').match
, # can be: BlahBlah/Blah_Blah
54 "css": None, # an url, empty (for default), or css content
55 "title": lambda x
: len(x
) < 999,
56 "header": None, # header text, UNUSED
57 "isbn": lambda x
: x
.isdigit() and len(x
) == 13,
58 "license": lambda x
: len(x
) < 999, #should be a codename?
59 "server": SERVER_DEFAULTS
.__contains
__,
60 "engine": config
.ENGINES
.__contains
__,
61 "booksize": config
.PAGE_SIZE_DATA
.__contains
__,
62 "page_width": isfloat
,
63 "page_height": isfloat
,
64 "gutter": isfloat_or_auto
,
65 "top_margin": isfloat_or_auto
,
66 "side_margin": isfloat_or_auto
,
67 "bottom_margin": isfloat_or_auto
,
68 "columns": isfloat_or_auto
,
69 "column_margin": isfloat_or_auto
,
70 "cgi-context": lambda x
: x
.lower() in '1true0false',
72 "rotate": u
"rotate".__eq
__,
75 __doc__
+= '\nValid arguments are: %s.\n' % ', '.join(ARG_VALIDATORS
.keys())
78 """Read and validate CGI or commandline arguments, putting the
79 good ones into the returned dictionary. Command line arguments
80 should be in the form --title='A Book'.
82 query
= cgi
.FieldStorage()
83 options
, args
= gnu_getopt(sys
.argv
[1:], '', [x
+ '=' for x
in ARG_VALIDATORS
])
84 options
= dict(options
)
87 for key
, validator
in ARG_VALIDATORS
.items():
88 value
= query
.getfirst(key
, options
.get('--' + key
, None))
89 log('%s: %s' % (key
, value
), debug
='STARTUP')
91 if validator
is not None and not validator(value
):
92 log("argument '%s' is not valid ('%s')" % (key
, value
))
96 log(data
, debug
='STARTUP')
99 def get_server_list():
100 return sorted(SERVER_DEFAULTS
.keys())
103 def get_book_list(server
):
104 """Ask the server for a list of books. Floss Manual TWikis keep such a list at
105 /bin/view/TWiki/WebLeftBarWebsList?skin=text but it needs a bit of processing
107 If BOOK_LIST_CACHE is non-zero, the book list won't be re-fetched
108 in that many seconds, rather it will be read from disk.
110 if config
.BOOK_LIST_CACHE
:
111 cache_name
= os
.path
.join(config
.BOOK_LIST_CACHE_DIR
, '%s.booklist' % server
)
112 if (os
.path
.exists(cache_name
) and
113 os
.stat(cache_name
).st_mtime
+ config
.BOOK_LIST_CACHE
> time
.time()):
119 url
= 'http://%s/bin/view/TWiki/WebLeftBarWebsList?skin=text' % server
125 items
= sorted(re
.findall(r
'/bin/view/([\w/]+)/WebHome', s
))
126 if config
.BOOK_LIST_CACHE
:
127 f
= open(cache_name
, 'w')
128 f
.write('\n'.join(items
))
133 #order by increasing areal size.
134 def calc_size(name
, pointsize
, klass
):
136 mmx
= pointsize
[0] * config
.POINT_2_MM
137 mmy
= pointsize
[1] * config
.POINT_2_MM
138 return (mmx
* mmy
, name
, klass
,
139 '%s (%dmm x %dmm)' % (name
, mmx
, mmy
))
141 return (0, name
, klass
, name
) # presumably 'custom'
143 return [x
[1:] for x
in sorted(calc_size(k
, v
.get('pointsize'), v
.get('class', ''))
144 for k
, v
in config
.PAGE_SIZE_DATA
.iteritems())
148 def optionise(items
, default
=None):
149 """Make a list of strings into an html option string, as would fit
150 inside <select> tags."""
153 if isinstance(x
, str):
156 # couple: value, name
158 options
.append('<option selected="selected" value="%s">%s</option>' % x
)
160 options
.append('<option value="%s">%s</option>' % x
)
162 # triple: value, class, name
164 options
.append('<option selected="selected" class="%s" value="%s">%s</option>' % x
)
166 options
.append('<option value="%s" class="%s">%s</option>' % x
)
168 return '\n'.join(options
)
171 """Make a list of strings into html <li> items, to fit in a <ul>
173 return '\n'.join('<li>%s</li>' % x
for x
in items
)
176 def get_default_css(server
=DEFAULT_SERVER
):
177 """Get the default CSS text for the selected server"""
179 cssfile
= SERVER_DEFAULTS
[server
]['css']
188 """Links to various example pdfs."""
190 for script
in os
.listdir(config
.FONT_EXAMPLE_SCRIPT_DIR
):
191 if not script
.isalnum():
192 log("warning: font-sample %s won't work; skipping" % script
)
194 links
.append('<a href="%s?script=%s">%s</a>' % (config
.FONT_LIST_URL
, script
, script
))
198 def make_progress_page(book
, bookname
):
199 f
= open(PROGRESS_TEMPLATE
)
202 progress_list
= ''.join('<li id="%s">%s</li>\n' for x
in config
.PROGRESS_POINTS
)
206 'bookname': bookname
,
207 'progress_list': progress_list
,
210 def progress_notifier(message
):
211 print ('<script type="text/javascript">\n'
212 'objavi_show_progress("%s");\n'
213 '</script>' % message
215 if message
== 'finished':
216 print '</body></html>'
218 return progress_notifier
220 def print_progress(message
):
221 print '******* got message "%s"' %message
223 def make_book_name(book
, server
):
224 lang
= SERVER_DEFAULTS
.get(server
, SERVER_DEFAULTS
[DEFAULT_SERVER
])['lang']
225 book
= ''.join(x
for x
in book
if x
.isalnum())
226 return '%s-%s-%s.pdf' % (book
, lang
,
227 time
.strftime('%Y.%m.%d-%H.%M.%S'))
230 def get_page_settings(args
):
231 """Find the size and any optional layout settings.
233 args['booksize'] is either a keyword describing a size or
234 'custom'. If it is custom, the form is inspected for specific
235 dimensions -- otherwise these are ignored.
237 The margins, gutter, number of columns, and column
238 margins all set themselves automatically based on the page
239 dimensions, but they can be overridden. Any that are are
241 #get all the values including sizes first
243 for k
, extrema
in config
.PAGE_EXTREMA
.iteritems():
245 v
= float(args
.get(k
))
246 except (ValueError, TypeError):
247 log("don't like %r as a float value for %s!" % (args
.get(k
), k
))
249 if v
< extrema
[0] or v
> extrema
[1]:
250 log('rejecting %s: outside %s' % (v
,) + extrema
)
252 log('found %s=%s' % (k
, v
))
253 settings
[k
] = v
* extrema
[2]
255 #now if args['size'] is not 'custom', the width height above
258 size
= args
.get('booksize', config
.DEFAULT_SIZE
)
260 settings
.update(config
.PAGE_SIZE_DATA
[size
])
262 #will raise KeyError if width, height aren't set
263 settings
['pointsize'] = (settings
['page_width'], settings
['page_height'])
264 del settings
['page_width']
265 del settings
['page_height']
271 def cgi_context(args
):
272 return 'SERVER_NAME' in os
.environ
or args
.get('cgi-context', 'NO').lower() in '1true'
274 def output_and_exit(f
):
276 if cgi_context(args
):
277 print "Content-type: text/html; charset=utf-8\n"
283 def mode_booklist(args
):
284 print optionise(get_book_list(args
.get('server', config
.DEFAULT_SERVER
)),
285 default
=args
.get('book'))
289 #XX sending as text/html, but it doesn't really matter
290 print get_default_css(args
.get('server', config
.DEFAULT_SERVER
))
295 f
= open(FORM_TEMPLATE
)
298 f
= open(config
.FONT_LIST_INCLUDE
)
299 font_list
= [x
.strip() for x
in f
if x
.strip()]
301 server
= args
.get('server', config
.DEFAULT_SERVER
)
302 book
= args
.get('book')
303 size
= args
.get('booksize', config
.DEFAULT_SIZE
)
304 engine
= args
.get('engine', config
.DEFAULT_ENGINE
)
306 'server_options': optionise(get_server_list(), default
=server
),
307 'book_options': optionise(get_book_list(server
), default
=book
),
308 'size_options': optionise(get_size_list(), default
=size
),
309 'engines': optionise(config
.ENGINES
.keys(), default
=engine
),
310 'css': get_default_css(server
),
311 'font_links': listify(font_links()),
312 'font_list': listify(font_list
),
313 'default_license' : config
.DEFAULT_LICENSE
,
314 'licenses' : optionise(config
.LICENSES
, default
=config
.DEFAULT_LICENSE
),
319 for id, title
, type, source
, classes
, epilogue
in config
.FORM_INPUTS
:
320 val
= d
.get(source
, '')
321 e
= config
.FORM_ELEMENT_TYPES
[type] % locals()
322 form
.append('\n<div id="%(id)s_div" class="form-item %(classes)s">\n'
323 '<div class="input_title">%(title)s</div>\n'
324 '<div class="input_contents"> %(e)s %(epilogue)s\n</div>'
325 '</div>\n' % locals())
328 print template
% {'form': ''.join(form
)}
333 # so we're making a book.
334 book
= args
.get('book')
335 server
= args
.get('server', config
.DEFAULT_SERVER
)
336 engine
= args
.get('engine', config
.DEFAULT_ENGINE
)
337 page_settings
= get_page_settings(args
)
338 bookname
= make_book_name(book
, server
)
340 if cgi_context(args
):
341 progress_bar
= make_progress_page(book
, bookname
)
343 progress_bar
= print_progress
345 with
Book(book
, server
, bookname
, page_settings
=page_settings
, engine
=engine
,
346 watcher
=progress_bar
) as book
:
347 if cgi_context(args
):
350 book
.set_title(args
.get('title'))
351 book
.add_css(args
.get('css'))
353 book
.compose_inside_cover(args
.get('license'), args
.get('isbn'))
354 book
.add_section_titles()
361 book
.notify_watcher('finished')
366 if __name__
== '__main__':
367 _valid_inputs
= set(ARG_VALIDATORS
)
368 _form_inputs
= set(x
[0] for x
in config
.FORM_INPUTS
)
369 log("valid but not used inputs: %s" % (_valid_inputs
- _form_inputs
))
370 log("invalid form inputs: %s" % (_form_inputs
- _valid_inputs
))
373 mode
= args
.get('mode')
374 if mode
is None and 'book' in args
:
377 if not args
and not cgi_context(args
):
381 output_function
= globals().get('mode_%s' % mode
, mode_form
)
382 output_function(args
)