]> git.donarmstrong.com Git - lilypond.git/blob - scripts/mudela-book.py
release: 1.1.5
[lilypond.git] / scripts / mudela-book.py
1 #!@PYTHON@
2 # All non-english comments are NOT in swedish, they are norwegian!
3
4 #  TODO:  center option (??)
5 # * the   verbatim  option should not be visible in the created latex file
6 # * what the h.. does castingalgorithm do/mean???
7 # * the following fails because mudelabook doesn't care that the
8 #   last } after \end{mudela} finishes the marginpar:
9 #     \marginpar{
10 #     \begin{mudela}[fragment]
11 #        c d e f g
12 #     \end{mudela}}
13 # * fragments should know about margins
14
15 # log:
16 # 0.3:
17 #   rewrite in Python.
18 # 0.4:
19 #   much rewritten by new author. I think the work has been split more
20 #   logical between different classes.
21 # 0.4.1:
22 #   - cleanup
23 #   - speedup, do all mudela parsing at the same time to avoid multiple
24 #     guile startup that takes some seconds on my computer
25
26 import os
27 import string
28 import re
29 import getopt
30 import sys
31
32 outdir = 'out'
33 program_version = '0.4.1'
34
35 out_files = []
36
37 fontsize_i2a = {11:'eleven', 13:'thirteen', 16:'sixteen', 20:'twenty', 26:'twentysix'}
38 fontsize_pt2i = {'11pt':11, '13pt':13, '16pt':16, '20pt':20, '26pt':26}
39
40 begin_mudela_re = re.compile ('^ *\\\\begin{mudela}')
41 extract_papersize_re = re.compile('\\\\documentclass[\[, ]*(\w*)paper[\w ,]*\]\{\w*\}')
42 extract_fontsize1_re = re.compile('\\\\documentclass\[[^\]]*\]\{[^\}]*\}')
43 extract_fontsize2_re = re.compile('[ ,\[]*([0-9]*)pt')
44 begin_mudela_opts_re = re.compile('\[[^\]]*\]')
45 end_mudela_re = re.compile ('^ *\\\\end{mudela}')
46 section_re = re.compile ('\\\\section')
47 chapter_re = re.compile ('\\\\chapter')
48 input_re = re.compile ('^\\\\input{([^}]*)')
49 include_re = re.compile ('^\\\\include{([^}]*)')
50 begin_document_re = re.compile ('^ *\\\\begin{document}')
51 documentclass_re = re.compile('\\\\documentclass')
52 twocolumn_re = re.compile('\\\\twocolumn')
53 onecolumn_re = re.compile('\\\\onecolumn')
54 preMudelaExample_re = re.compile('\\\\def\\\\preMudelaExample')
55 postMudelaExample_re = re.compile('\\\\def\\\\postMudelaExample')
56 boundingBox_re = re.compile('%%BoundingBox: ([0-9]*) ([0-9]*) ([0-9]*) ([0-9]*)')
57
58 def file_exist_b(name):
59     try: 
60         f = open(name)
61     except IOError:
62         return 0
63     f.close ()
64     return 1
65
66 def ps_dimention(fname):
67     fd = open(fname)
68     lines = fd.readlines()
69     for line in lines:
70         s = boundingBox_re.search(line)
71         if s:
72             break
73     return (int(s.groups()[2])-int(s.groups()[0]), 
74             int(s.groups()[3])-int(s.groups()[1]))
75
76
77 class CompileStatus:
78     pass
79 class SomethingIsSeriouslyBroken:
80     pass
81
82 def file_mtime (name):
83     return os.stat (name)[8] #mod time
84
85 def need_recompile_b(infile, outfile):
86     indate = file_mtime (infile)
87     try:
88         outdate = file_mtime (outfile)
89         return indate > outdate
90     except os.error:
91         return 1
92
93 #
94 # executes os.system(command) if infile is newer than
95 # outfile or outfile don't exist
96 #
97 def compile (command, workingdir, infile, outfile):
98     "Test if infile is newer than outfile. If so, cd to workingdir"
99     "and execute command"
100     indate = file_mtime (workingdir+infile)
101     try:
102         outdate = file_mtime (workingdir+outfile)
103         recompile = indate > outdate
104
105     except os.error:
106         recompile = 1
107
108     if recompile:
109         sys.stderr.write ('invoking `%s\'\n' % command)
110         if workingdir == '':
111             status = os.system (command)
112         else:
113             status = os.system ('cd %s; %s' %(workingdir, command))
114         if status:
115             raise CompileStatus
116
117 class Properties:
118     #
119     # init
120     #
121     def __init__(self):
122         self.__linewidth = {
123             1: {'a4':{10: 345, 11: 360, 12: 390},
124                 'a5':{10: 276, 11: 276, 12: 276},
125                 'b5':{10: 345, 11: 356, 12: 356},
126                 'letter':{10: 345, 11: 360, 12: 390},
127                 'legal': {10: 345, 11: 360, 12: 390},
128                 'executive':{10: 345, 11: 360, 12: 379}},
129             2: {'a4':{10: 167, 11: 175, 12: 190},
130                 'a5':{10: 133, 11: 133, 12: 133},
131                 'b5':{10: 167, 11: 173, 12: 173},
132                 'letter':{10: 167, 11: 175, 12: 190},
133                 'legal':{10: 167, 11: 175, 12: 190},
134                 'executive':{10: 167, 11: 175, 12: 184}}}
135         # >0 --> force all mudela to this pt size
136         self.force_mudela_fontsize = 0
137         self.force_verbatim_b = 0
138         self.__data = {
139             'mudela-fontsize' : {'init': 16},
140             'papersize' : {'init': 'a4'},
141             'num-column' : {'init': 1},
142             'tex-fontsize' : {'init': 11}
143             }
144     def clear_for_new_file(self):
145         for var in self.__data.keys():
146             self.__data[var] = {'init': self.__data[var]['init']}
147     def clear_for_new_block(self):
148         for var in self.__data.keys():
149             if self.__data[var].has_key('block'):
150                 del self.__data[var]['block']
151     def __get_variable(self, var):
152         if self.__data[var].has_key('block'):
153             return self.__data[var]['block']
154         elif self.__data[var].has_key('file'):
155             return self.__data[var]['file']
156         else:
157             return self.__data[var]['init']
158     def setPapersize(self, size, requester):
159         self.__data['papersize'][requester] = size
160     def getPapersize(self):
161         return self.__get_variable('papersize')
162     def setMudelaFontsize(self, size, requester):
163         self.__data['mudela-fontsize'][requester] = size
164     def getMudelaFontsize(self):
165         if self.force_mudela_fontsize:
166             return self.force_mudela_fontsize
167         return self.__get_variable('mudela-fontsize')
168     def setTexFontsize(self, size, requester):
169         self.__data['tex-fontsize'][requester] = size
170     def getTexFontsize(self):
171         return self.__get_variable('tex-fontsize')
172     def setNumColumn(self, num, requester):
173         self.__data['num-column'][requester] = num
174     def getNumColumn(self):
175         return self.__get_variable('num-column')
176     def getLineWidth(self):
177         return self.__linewidth[self.getNumColumn()][self.getPapersize()][self.getTexFontsize()]
178
179
180 class Mudela_output:
181     def __init__ (self, basename):
182         self.basename = basename
183         self.temp_filename = "%s/%s" %(outdir, 'mudela-temp.ly')
184         self.file = open (self.temp_filename, 'w')
185         # 'tex' or 'eps'
186         self.graphic_type = 'tex'
187         self.fragment = 0
188     def write (self, line):
189         # match only if there is nothing but whitespace before \begin HACK
190         if re.search('^\s*\\\\begin{mudela}', line):
191             self.scan_begin_statement(line)
192             self.write_red_tape()
193         else:
194             self.file.write (line)
195     def scan_begin_statement(self, line):
196         r  = begin_mudela_opts_re.search(line)
197         if r:
198             o = r.group()[1:][:-1]
199             optlist =  re.compile('[ ,]*').split(o)
200         else:
201             optlist = []
202         if 'floating' in optlist:
203             self.graphic_type = 'eps'
204         else:
205             self.graphic_type = 'tex'
206         if 'fragment' in optlist:
207             self.fragment = 1
208         else:
209             self.fragment = 0
210         for pt in fontsize_pt2i.keys():
211             if pt in optlist:
212                 Props.setMudelaFontsize(fontsize_pt2i[pt], 'block')
213     def write_red_tape(self):
214         self.file.write ('\\include \"paper%d.ly\"\n' % Props.getMudelaFontsize())
215         s = fontsize_i2a[Props.getMudelaFontsize()]
216         if self.fragment:
217             self.file.write("\\paper {" # mudela  1.0.4
218                             + "\\paper_%s\n linewidth = -1.\\pt;" % s
219                             + "castingalgorithm = \Wordwrap; indent = 2.\cm; \n}")
220             self.file.write("\\score{\n\\notes{")
221         else:
222             self.file.write ("\paper {"
223                              + "\\paper_%s\n linewidth = %i.\\pt;" % \
224                              (s, Props.getLineWidth()) \
225                              + "castingalgorithm = \Wordwrap; indent = 2.\cm;\n}")
226     def close (self):
227         if self.fragment:
228             self.file.write ('}\\paper { } }\n')
229         self.file.close ()
230
231         inf = outdir + self.basename + '.ly'
232         outf = outdir + self.basename + '.tex'
233         if not os.path.isfile(inf):
234             status = 1
235         else:
236             status = os.system ('diff -q %s %s' % (self.temp_filename, inf))
237         if status:
238             os.rename (self.temp_filename, inf)
239         if need_recompile_b(inf, outf):
240             out_files.append((self.graphic_type, inf))
241     def insert_me_string(self):
242         "Returns a string that can be used directly in latex."
243         if self.graphic_type == 'tex':
244             return ['tex', self.basename]
245         elif self.graphic_type == 'eps':
246             return ['eps', self.basename]
247         else:
248             raise SomethingIsSeriouslyBroken
249
250 class Tex_output:
251     def __init__ (self, name):
252         self.output_fn = '%s/%s' % (outdir, name)
253         self.__lines = []
254     def open_verbatim (self):
255         self.__lines.append('\\begin{verbatim}\n')
256     def close_verbatim (self):
257         self.__lines.append('\\end{verbatim}\n')
258     def write (self, s):
259         self.__lines.append(s)
260     def create_graphics(self):
261         s = ''
262         g_vec = []
263         for line in self.__lines:
264             if type(line)==type([]):
265                 g_vec.append(line)
266         for g in g_vec:
267             if need_recompile_b(outdir+g[1]+'.ly', outdir+g[1]+'.tex'):
268                     s = s + ' ' + g[1]+'.ly'
269         if s != '':
270             os.system('cd %s; lilypond %s' %(outdir, s))
271         for g in g_vec:
272             if g[0] == 'eps':
273                 compile('tex %s' % g[1]+'.tex', outdir, g[1]+'.tex', g[1]+'.dvi')
274                 compile('dvips -E -o %s %s' %(g[1]+'.eps', g[1]+'.dvi'), outdir,
275                         g[1]+'.dvi', g[1]+'.eps')
276     def write_outfile(self):
277         file = open(self.output_fn+'.latex', 'w')
278         file.write('% Created by mudela-book\n')
279         for line in self.__lines:
280             if type(line)==type([]):
281                 if line[0] == 'tex':
282                     file.write('\\preMudelaExample\\input %s\n\postMudelaExample '\
283 #                               % (outdir+line[1]+'.tex'))
284
285                                # TeX applies the prefix of the main source automatically.
286                                % (line[1]+'.tex'))                             
287                 if line[0] == 'eps':
288                     ps_dim = ps_dimention(outdir+line[1]+'.eps')
289                     file.write('\\parbox{%ipt}{\includegraphics{%s}}\n' \
290 #                               % (ps_dim[0], outdir+line[1]+'.eps'))
291                                % (ps_dim[0], line[1]+'.eps'))
292                     
293             else:
294                 file.write(line)
295         file.close()
296 class Tex_input:
297     def __init__ (self, filename):
298         for fn in [filename, filename+'.tex', filename+'.doc']:
299             try:
300                 self.infile = open (fn)
301             except:
302                 continue
303             self.filename = fn
304             break
305     def get_lines (self):
306         lines = self.infile.readlines ()
307         (retlines, retdeps) = ([],[self.filename])
308         for line in lines:
309             r_inp = input_re.search (line)
310             r_inc = include_re.search (line)
311             # Filename rules for \input :
312             # input: no .tex ext
313             # 1. will search for file with exact that name (tex-input.my will be found)
314             # 2. will search for file with .tex ext, (tex-input.my
315             #    will find tex-input.my.tex)
316             # input: with .tex ext
317             # 1. will find exact match
318             
319             # Filename rules for \include :
320             # 1. will only find files with name given to \include + .tex ext
321             if r_inp:
322                 t = Tex_input (r_inp.groups()[0])
323                 ls = t.get_lines ()
324                 retlines = retlines + ls[0]
325                 retdeps = retdeps + ls[1]
326             elif r_inc:
327                 t = Tex_input (r_inc.groups()[0]+'.tex')
328                 ls =t.get_lines ()
329                 ls[0].insert(0, '\\newpage\n')
330                 ls[0].append('\\newpage\n')
331                 retlines = retlines + ls[0]
332                 retdeps = retdeps + ls[1]
333             else:
334                 retlines.append (line)
335         return (retlines, retdeps)
336
337
338 class Main_tex_input(Tex_input):
339     def __init__ (self, name, outname):
340
341         Tex_input.__init__ (self, name) # ugh
342         self.outname = outname
343         self.chapter = 0
344         self.section = 0
345         self.fine_count =0
346         self.mudtex = Tex_output (self.outname)
347         self.mudela = None
348         self.deps = []
349         self.verbatim = 0
350         # set to 'mudela' when we are processing mudela code,
351         # both verbatim and graphic-to-be
352         self.mode = 'latex'
353     def set_sections (self, l):
354         if section_re.search (l):
355             self.section = self.section + 1
356         if chapter_re.search (l):
357             self.section = 0
358             self.chapter = self.chapter + 1
359
360     def gen_basename (self):
361         return '%s-%d.%d.%d' % (self.outname,self.chapter,self.section,self.fine_count)
362         #return '%s/%s-%d.%d.%d' % (outdir, self.outname,self.chapter,self.section,self.fine_count)
363
364     def extract_papersize_from_documentclass(self, line):
365         pre = extract_papersize_re.search(line)
366         if not pre:
367             return None
368         return pre.groups()[0]
369     def extract_fontsize_from_documentclass(self, line):
370         if extract_fontsize1_re.search(line):
371             r = extract_fontsize2_re.search(line)
372             if r:
373                 return int(r.groups()[0])
374     def do_it(self):
375         preMudelaDef = postMudelaDef = 0
376         (lines, self.deps) = self.get_lines ()
377         for line in lines:
378             if documentclass_re.search (line):
379                 p = self.extract_papersize_from_documentclass (line)
380                 if p:
381                     Props.setPapersize(p, 'file')
382                 f = self.extract_fontsize_from_documentclass (line)
383                 if f:
384                     Props.setTexFontsize (f, 'file')
385             elif twocolumn_re.search (line):
386                 Props.setNumColumn (2, 'file')
387             elif onecolumn_re.search (line):
388                 Props.setNumColumn (1, 'file')
389             elif preMudelaExample_re.search (line):
390                 preMudelaDef = 1
391             elif postMudelaExample_re.search (line):
392                 postMudelaDef = 1
393             elif begin_document_re.search (line):
394                 if not preMudelaDef:
395                     self.mudtex.write ('\\def\\preMudelaExample{}\n')
396                 if not postMudelaDef:
397                     self.mudtex.write ('\\def\\postMudelaExample{}\n')
398             elif begin_mudela_re.search (line):
399                 Props.clear_for_new_block()
400                 if __debug__:
401                     if self.mode == 'mudela':
402                         raise AssertionError
403                 self.mode = 'mudela'
404                 r  = begin_mudela_opts_re.search (line)
405                 if r:
406                     o = r.group()[1:][:-1]
407                     optlist =  re.compile('[ ,]*').split(o)
408                 else:
409                     optlist = []
410                 if ('verbatim' in optlist) or (Props.force_verbatim_b):
411                     self.verbatim = 1
412                     self.mudtex.open_verbatim ()
413                 else:
414                     self.verbatim = 0
415                     self.mudela = Mudela_output (self.gen_basename ())
416
417             elif end_mudela_re.search (line):
418                 if __debug__:
419                     if self.mode != 'mudela':
420                         raise AssertionError
421                 if self.mudela:
422                     self.mudela.close ()
423                     self.mudtex.write (self.mudela.insert_me_string())
424                     del self.mudela
425                     self.mudela = None
426                     self.fine_count = self.fine_count + 1
427                 else:                    
428                     self.mudtex.write (line)
429                     self.mudtex.close_verbatim ()
430                 self.mode = 'latex'
431                 continue
432
433             if self.mode == 'mudela' and not self.verbatim:
434                 self.mudela.write (line)
435             else:
436                 self.mudtex.write (line)
437                 self.set_sections(line)
438         self.mudtex.create_graphics()
439         self.mudtex.write_outfile()
440         del self.mudtex
441                 
442
443 def help():
444     sys.stdout.write("""Usage: mudela-book [options] FILE\n
445 Generate hybrid LaTeX input from Latex + mudela
446 Options:\n
447   -h, --help                     print this help
448   -d, --outdir=DIR               directory to put generated files
449   -o, --outname=FILE             prefix for filenames
450   --default-mudela-fontsize=??pt default fontsize for music
451   --force-mudela-fontsize=??pt   force fontsize for all inline mudela
452   --force-verbatim               make all mudela verbatim\n"""
453                      )
454     sys.exit (0)
455
456
457 def write_deps (fn, out,  deps):
458         out_fn = outdir + '/' + fn
459         print '`writing `%s\'\n\'' % out_fn
460         
461         f = open (out_fn, 'w')
462         f.write ('%s: %s\n'% (outdir + '/' + out + '.dvi',
463                               reduce (lambda x,y: x + ' '+ y, deps)))
464         f.close ()
465
466 def identify():
467     sys.stderr.write ('This is %s version %s\n' % ('mudela-book', program_version))
468
469 def main():
470     global outdir
471     outname = ''
472     try:
473         (options, files) = getopt.getopt(
474             sys.argv[1:], 'hd:o:', ['outdir=', 'outname=',
475                                     'default-mudela-fontsize=',
476                                     'force-mudela-fontsize=',
477                                     'help', 'dependencies',
478                                     'force-verbatim'])
479     except getopt.error, msg:
480         print "error:", msg
481         sys.exit(1)
482         
483     do_deps = 0
484     for opt in options:
485         o = opt[0]
486         a = opt[1]
487         if o == '--outname' or o == '-o':
488             if len(files) > 1:
489                 #HACK
490                 print "Mudela-book is confused by --outname on multiple files"
491                 sys.exit(1)
492             outname = a
493         if o == '--outdir' or o == '-d':
494             outdir = a
495         if o == '--help' or o == '-h':
496             help ()
497         if o == '--dependencies':
498             do_deps = 1
499         if o == '--default-mudela-fontsize':
500             if not fontsize_pt2i.has_key(a):
501                 print "Error: illegal fontsize:", a
502                 print " accepted fontsizes are: 11pt, 13pt, 16pt, 20pt, 26pt"
503                 sys.exit()
504             Props.setMudelaFontsize(fontsize_pt2i[a], 'init')
505         if o == '--force-mudela-fontsize':
506             if not fontsize_pt2i.has_key(a):
507                 print "Error: illegal fontsize:", a
508                 print " accepted fontsizes are: 11pt, 13pt, 16pt, 20pt, 26pt"
509                 sys.exit()
510             Props.force_mudela_fontsize = fontsize_pt2i[a]
511         if o == '--force-verbatim':
512             Props.force_verbatim_b = 1
513
514     if outdir[-1:] != '/':
515         outdir = outdir + '/'
516
517     if not os.path.isdir(outdir):
518         os.system('mkdir %s' % outdir)
519
520     for input_filename in files:
521         Props.clear_for_new_file()
522         if outname:
523             my_outname = outname
524         else:
525             my_outname = os.path.basename(os.path.splitext(input_filename)[0])
526         my_depname = my_outname + '.dep'        
527         inp = Main_tex_input (input_filename, my_outname)
528         inp.do_it ()
529         print "LaTeX output to %s%s.latex" %(outdir, my_outname)
530
531         if do_deps:
532             write_deps (my_depname, my_outname, inp.deps)
533
534 identify()
535 Props = Properties()
536 main()