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