]> git.donarmstrong.com Git - lilypond.git/blob - scripts/mudela-book.py
release: 1.1.13
[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         self.code_type_override = None
205     def write (self, line):
206         # match only if there is nothing but whitespace before \begin HACK
207         if re.search('^\s*\\\\begin{mudela}', line):
208             self.scan_begin_statement(line)
209         else:
210             if self.code_type == 'unknown':
211                 if re.search('^\s*\\\\score', line) or \
212                    re.search('^\s*\\\\paper', line) or \
213                    re.search('^\s*\\\\header', line) or \
214                    re.search('^\s*[A-Za-z]*\s*=', line):
215                     self.code_type = 'ly'
216             self.__lines.append(line)
217     def scan_begin_statement(self, line):
218         r  = begin_mudela_opts_re.search(line)
219         if r:
220             o = r.group()[1:-1]
221             optlist =  re.compile('[\s,]*').split(o)
222         else:
223             optlist = []
224         if 'fragment' in optlist:
225             self.code_type_override = 'fly'
226         if 'nonfragment' in optlist:
227             self.code_type_override = 'ly'
228         if 'eps' in optlist:
229             self.graphic_type = 'eps'
230         for pt in fontsize_pt2i.keys():
231             if pt in optlist:
232                 Props.setMudelaFontsize(fontsize_pt2i[pt], 'block')
233     def write_red_tape(self):
234         self.file.write ('\\include \"paper%d.ly\"\n' \
235                          % Props.getMudelaFontsize())
236                          
237         s = fontsize_i2a[Props.getMudelaFontsize()]
238         if self.code_type == 'fly':
239             linewidth_str = 'linewidth = -1.\cm;'
240         else:
241             linewidth_str = 'linewidth = %i.\\pt;' % Props.getLineWidth()
242         self.file.write("\\paper {"
243                         + "\\paper_%s " % s
244                         + linewidth_str
245                         + "castingalgorithm = \Gourlay; \n}")
246                         #+ "castingalgorithm = \Wordwrap; indent = 2.\cm; \n}")
247         if self.code_type == 'fly':
248             self.file.write('\\score{\n\\notes{')
249     def close (self):
250         if self.code_type == 'unknown':
251             self.code_type = 'fly'
252         if self.code_type_override:
253             self.code_type = self.code_type_override
254             print "override:", self.code_type_override
255         self.write_red_tape()
256         for l in self.__lines:
257             self.file.write(l)
258         if self.code_type == 'fly':
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
333 class Tex_input:
334     def __init__ (self, filename):
335         for fn in [filename, filename+'.tex', filename+'.doc']:
336             try:
337                 self.infile = open (fn)
338                 self.filename = fn
339                 return
340             except:
341                 continue
342         raise IOError
343     def get_lines (self):
344         lines = self.infile.readlines ()
345         (retlines, retdeps) = ([],[self.filename])
346         for line in lines:
347             r_inp = input_re.search (line)
348             r_inc = include_re.search (line)
349
350             # Filename rules for \input :
351             # input: no .tex ext
352             # 1. will search for file with exact that name (tex-input.my will be found)
353             # 2. will search for file with .tex ext, (tex-input.my
354             #    will find tex-input.my.tex)
355             # input: with .tex ext
356             # 1. will find exact match
357             
358             # Filename rules for \include :
359             # 1. will only find files with name given to \include + .tex ext
360             if r_inp:
361                 try:
362                     t = Tex_input (r_inp.groups()[0])
363                     ls = t.get_lines ()
364                     retlines = retlines + ls[0]
365                     retdeps = retdeps + ls[1]
366                 except:
367                     print "warning: can't find %s, let's hope latex will" \
368                           % r_inp.groups()[0]
369                     retlines.append (line)
370             elif r_inc:
371                 try:
372                     t = Tex_input (r_inc.groups()[0]+'.tex')
373                     ls =t.get_lines ()
374                     ls[0].insert(0, '\\newpage\n')
375                     ls[0].append('\\newpage\n')
376                     retlines = retlines + ls[0]
377                     retdeps = retdeps + ls[1]
378                 except:
379                     print "warning: can't find %s, let's hope latex will" \
380                           % r_inc.groups()[0]
381                     retlines.append (line)
382             else:
383                 r_mud = defined_mudela_cmd_re.search(line)
384                 if r_mud:
385                     ss = "\\\\verb(?P<xx>[^a-zA-Z])\s*\\\\%s\s*(?P=xx)" \
386                          % re.escape(r_mud.group()[1:])
387                     if re.search(ss, line):
388                         retlines.append(line)
389                         continue
390                     while 1:
391                         opts = r_mud.groups()[2]
392                         if opts == None:
393                             opts = ''
394                         else:
395                             opts = ', '+opts
396                         (start, rest) = string.split(line, r_mud.group(), 1)
397                         retlines.append(start)#+'\n')
398                         v = string.split(defined_mudela_cmd[r_mud.groups()[0]], '\n')
399                         for l in v[1:-1]:
400                             l = re.sub(r'\\fontoptions', opts, l)
401                             l = re.sub(r'\\maininput', r_mud.groups()[3], l)
402                             retlines.append(l)
403                         r_mud = defined_mudela_cmd_re.search(rest)
404                         if not r_mud:
405                             retlines.append(rest)
406                             break;
407                         line = rest
408                 else:
409                     retlines.append (line)
410         else:
411             return (retlines, retdeps)
412
413
414 class Main_tex_input(Tex_input):
415     def __init__ (self, name, outname):
416
417         Tex_input.__init__ (self, name) # ugh
418         self.outname = outname
419         self.chapter = 0
420         self.section = 0
421         self.fine_count =0
422         self.mudtex = Tex_output (self.outname)
423         self.mudela = None
424         self.deps = []
425         self.verbatim = 0
426         # set to 'mudela' when we are processing mudela code,
427         # both verbatim and graphic-to-be
428         self.mode = 'latex'
429     def set_sections (self, l):
430         if section_re.search (l):
431             self.section = self.section + 1
432         if chapter_re.search (l):
433             self.section = 0
434             self.chapter = self.chapter + 1
435
436     def gen_basename (self):
437         return '%s-%d.%d.%d' % (self.outname, self.chapter,
438                                 self.section, self.fine_count)
439     def extract_papersize_from_documentclass(self, line):
440         pre = extract_papersize_re.search(line)
441         if not pre:
442             return None
443         return pre.groups()[0]
444     def extract_fontsize_from_documentclass(self, line):
445         r = extract_fontsize_re.search(line)
446         if r:
447             return int(r.groups()[0])
448     def do_it(self):
449         preMudelaDef = postMudelaDef = 0
450         (lines, self.deps) = self.get_lines ()
451         for line in lines:
452             if documentclass_re.search (line):
453                 p = self.extract_papersize_from_documentclass (line)
454                 if p:
455                     Props.setPapersize(p, 'file')
456                 f = self.extract_fontsize_from_documentclass (line)
457                 if f:
458                     Props.setTexFontsize (f, 'file')
459             elif twocolumn_re.search (line):
460                 Props.setNumColumn (2, 'file')
461             elif onecolumn_re.search (line):
462                 Props.setNumColumn (1, 'file')
463             elif preMudelaExample_re.search (line):
464                 preMudelaDef = 1
465             elif postMudelaExample_re.search (line):
466                 postMudelaDef = 1
467             elif begin_document_re.search (line):
468                 if not preMudelaDef:
469                     self.mudtex.write ('\\def\\preMudelaExample{}\n')
470                 if not postMudelaDef:
471                     self.mudtex.write ('\\def\\postMudelaExample{}\n')
472             elif begin_mudela_re.search (line):
473                 Props.clear_for_new_block()
474                 if __debug__:
475                     if self.mode == 'mudela':
476                         raise AssertionError
477                 self.mode = 'mudela'
478                 r  = begin_mudela_opts_re.search (line)
479                 if r:
480                     o = r.group()[1:][:-1]
481                     optlist =  re.compile('[ ,]*').split(o)
482                 else:
483                     optlist = []
484                 if ('verbatim' in optlist) or (Props.force_verbatim_b):
485                     self.verbatim = 1
486                     self.mudtex.open_verbatim (line)
487                     continue
488                 else:
489                     self.verbatim = 0
490                     self.mudela = Mudela_output (self.gen_basename ())
491             elif end_mudela_re.search (line):
492                 if __debug__:
493                     if self.mode != 'mudela':
494                         raise AssertionError
495                 if self.mudela:
496                     self.mudela.close ()
497                     self.mudtex.write (self.mudela.insert_me_string())
498                     del self.mudela
499                     self.mudela = None
500                     self.fine_count = self.fine_count + 1
501                 else:                    
502                     self.mudtex.write (line)
503                     self.mudtex.close_verbatim ()
504                 self.mode = 'latex'
505                 continue
506
507             if self.mode == 'mudela' and not self.verbatim:
508                 self.mudela.write (line)
509             else:
510                 self.mudtex.write (line)
511                 self.set_sections(line)
512         self.mudtex.create_graphics()
513         self.mudtex.write_outfile()
514         del self.mudtex
515                 
516
517 def help():
518     sys.stdout.write("""Usage: mudela-book [options] FILE\n
519 Generate hybrid LaTeX input from Latex + mudela
520 Options:\n
521   -h, --help                     print this help
522   -d, --outdir=DIR               directory to put generated files
523   -o, --outname=FILE             prefix for filenames
524   --default-mudela-fontsize=??pt default fontsize for music
525   --force-mudela-fontsize=??pt   force fontsize for all inline mudela
526   --force-verbatim               make all mudela verbatim\n
527   --dependencies                 write dependencies
528   --init                         mudela-book initfile
529   """
530                      )
531     sys.exit (0)
532
533
534 def write_deps (fn, out,  deps):
535         out_fn = outdir + '/' + fn
536         print '`writing `%s\'\n\'' % out_fn
537         
538         f = open (out_fn, 'w')
539         f.write ('%s: %s\n'% (outdir + '/' + out + '.dvi',
540                               reduce (lambda x,y: x + ' '+ y, deps)))
541         f.close ()
542
543 def identify():
544     sys.stderr.write ('This is %s version %s\n' % ('mudela-book', program_version))
545
546 def main():
547     global outdir, initfile, defined_mudela_cmd, defined_mudela_cmd_re
548     outname = ''
549     try:
550         (options, files) = getopt.getopt(
551             sys.argv[1:], 'hd:o:', ['outdir=', 'outname=',
552                                     'default-mudela-fontsize=',
553                                     'force-mudela-fontsize=',
554                                     'help', 'dependencies',
555                                     'force-verbatim', 'init='])
556     except getopt.error, msg:
557         print "error:", msg
558         sys.exit(1)
559         
560     do_deps = 0
561     for opt in options:    
562         o = opt[0]
563         a = opt[1]
564         if o == '--outname' or o == '-o':
565             if len(files) > 1:
566                 #HACK
567                 print "Mudela-book is confused by --outname on multiple files"
568                 sys.exit(1)
569             outname = a
570         if o == '--outdir' or o == '-d':
571             outdir = a
572         if o == '--help' or o == '-h':
573             help ()
574         if o == '--dependencies':
575             do_deps = 1
576         if o == '--default-mudela-fontsize':
577             if not fontsize_pt2i.has_key(a):
578                 print "Error: illegal fontsize:", a
579                 print " accepted fontsizes are: 11pt, 13pt, 16pt, 20pt, 26pt"
580                 sys.exit()
581             Props.setMudelaFontsize(fontsize_pt2i[a], 'init')
582         if o == '--force-mudela-fontsize':
583             if not fontsize_pt2i.has_key(a):
584                 print "Error: illegal fontsize:", a
585                 print " accepted fontsizes are: 11pt, 13pt, 16pt, 20pt, 26pt"
586                 sys.exit()
587             Props.force_mudela_fontsize = fontsize_pt2i[a]
588         if o == '--force-verbatim':
589             Props.force_verbatim_b = 1
590         if o == '--init':
591             initfile =  a
592     if outdir[-1:] != '/':
593         outdir = outdir + '/'
594
595     std_init_filename = ''
596     for p in string.split(os.environ['LILYINCLUDE'], ':'):
597         try:
598             std_init_filename =  p+os.sep+'mudela-book-defs.py'
599             break
600         except:
601             continue
602     defined_mudela_cmd_re = {}
603     try:
604         f = open(std_init_filename)
605         s = f.read()
606         f.close()
607         defined_mudela_cmd = eval(s)    # UGH
608     except IOError, w:
609         sys.stderr.write("%s (`%s')\n" % (w[1], std_init_filename))
610 #        sys.exit(1)
611
612
613     if initfile != '':
614         f = open(initfile)
615         s = f.read()
616         f.close()
617         d = eval(s)
618         for i in d.keys():
619             defined_mudela_cmd[i] = d[i]
620         del d
621
622     c = defined_mudela_cmd.keys()[0]
623     for x in defined_mudela_cmd.keys()[1:]:
624         c = c + '|'+x
625     defined_mudela_cmd_re = re.compile("\\\\(%s)(\[(\d*pt)\])*{([^}]*)}" %c)
626
627     if not os.path.isdir(outdir):
628         os.system('mkdir %s' % outdir)
629
630     for input_filename in files:
631         Props.clear_for_new_file()
632         if outname:
633             my_outname = outname
634         else:
635             my_outname = os.path.basename(os.path.splitext(input_filename)[0])
636         my_depname = my_outname + '.dep'        
637         inp = Main_tex_input (input_filename, my_outname)
638         inp.do_it ()
639 #        os.system('latex %s/%s.latex' % (outdir, my_outname))
640         if do_deps:
641             write_deps (my_depname, my_outname, inp.deps)
642
643 identify()
644 Props = Properties()
645 main()