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