3 # The bugs you find are made by Tom Cato Amundsen <tomcato@xoommail.com>
4 # Send patches/questions/bugreports to a mailinglist:
5 # gnu-music-discuss@gnu.org
6 # bug-gnu-music@gnu.org
7 # help-gnu-music@gnu.org
9 # All non-english comments are NOT in swedish, they are norwegian!
11 # * output various stuff either via sys.stderr or sys.stdout, not using print.
12 # * center option (??)
13 # * make mudela-book understand usepackage{geometry}
14 # * check that linewidth set in \paper is not wider than actual linewidth?
15 # * the following fails because mudelabook doesn't care that the
16 # last } after \end{mudela} finishes the marginpar:
21 # * force-verbatim is maybe not that useful since latex fails with footnotes,
22 # marginpars and others
27 # much rewritten by new author. I think the work has been split more
28 # logical between different classes.
31 # - speedup, do all mudela parsing at the same time to avoid multiple
32 # guile startup that takes some seconds on my computer
34 # - fixed default latex fontsize, it should be 10pt not 11pt
35 # - verbatim option no longer visible
36 # - mudela-book produces .dvi output
37 # - change to use castingalgorithm = \Gourlay instead of \Wordwrap. It gives
38 # better result on small linewidths.
39 # - mudela-book-doc.doc rewritten
41 # - junked floating and fragment options, added eps option
42 # - mudela-book will scan the mudela code to find out if it has to add
43 # paper-definition and \score{\notes{...}}
44 # - possible to define commands that look like this: \mudela{ c d e }
45 # - don't produce .dvi output, it was a silly idea...
47 # - removed init/mudela-book-defs.py, \mudela is defined inside mudela-book
48 # - fragment and nonfragment options will override autodetection of type of
49 # in mudela-block (voice contents or complete code)
51 # - fixed verbatim option behaviour: don't show \begin{mudela} \end{mudela}
52 # and show both mudela code and music
53 # - veryverbatim option, same as verbatim but show \begin{mudela}
54 # and \end{mudela}. (saves keystrokes in mudela-book-doc.doc)
55 # - intertext="bla bla bla" option
56 # - mudela-book now understand latex \begin{verbatim}
58 # - bf: \mudela{ \times 2/3{...} }
59 # * \t in \times is not tab character and
60 # * dont treat the first '}' as command ending
62 # - .fly and .sly files in \mudelafile{} are treated as standalone Lilypond.
63 # - Fragments, .fly and .sly files are in \relative mode.
65 # - bf: Default fragments have linewidth=-1.0
66 # - Added 'singleline' and 'multiline' options.
76 program_version = '0.5.3'
81 fontsize_i2a = {11:'eleven', 13:'thirteen', 16:'sixteen',
82 20:'twenty', 26:'twentysix'}
83 fontsize_pt2i = {'11pt':11, '13pt':13, '16pt':16, '20pt':20, '26pt':26}
85 begin_mudela_re = re.compile ('^ *\\\\begin{mudela}')
86 begin_verbatim_re = re.compile ('^ *\\\\begin{verbatim}')
87 end_verbatim_re = re.compile ('^ *\\\\end{verbatim}')
88 extract_papersize_re = re.compile('\\\\documentclass[\[, ]*(\w*)paper[\w ,]*\]\{\w*\}')
89 extract_fontsize_re = re.compile('[ ,\[]*([0-9]*)pt')
90 begin_mudela_opts_re = re.compile('\[[^\]]*\]')
91 end_mudela_re = re.compile ('^ *\\\\end{mudela}')
92 section_re = re.compile ('\\\\section')
93 chapter_re = re.compile ('\\\\chapter')
94 input_re = re.compile ('^\\\\input{([^}]*)')
95 include_re = re.compile ('^\\\\include{([^}]*)')
96 begin_document_re = re.compile ('^ *\\\\begin{document}')
97 documentclass_re = re.compile('\\\\documentclass')
98 twocolumn_re = re.compile('\\\\twocolumn')
99 onecolumn_re = re.compile('\\\\onecolumn')
100 mudela_file_re = re.compile('\\\\mudelafile{([^}]+)}')
101 file_ext_re = re.compile('.+\\.([^.}]+$)')
102 preMudelaExample_re = re.compile('\\\\def\\\\preMudelaExample')
103 postMudelaExample_re = re.compile('\\\\def\\\\postMudelaExample')
104 boundingBox_re = re.compile('%%BoundingBox: ([0-9]+) ([0-9]+) ([0-9]+) ([0-9]+)')
105 intertext_re = re.compile("intertext=\"([^\"]*)\"")
107 def file_exist_b(name):
115 def ps_dimention(fname):
117 lines = fd.readlines()
119 s = boundingBox_re.search(line)
122 return (int(s.groups()[2])-int(s.groups()[0]),
123 int(s.groups()[3])-int(s.groups()[1]))
126 def find_file (name):
127 for a in include_path:
129 nm = os.path.join (a, name)
139 class SomethingIsSeriouslyBroken:
142 def file_mtime (name):
143 return os.stat (name)[8] #mod time
145 def need_recompile_b(infile, outfile):
146 indate = file_mtime (infile)
148 outdate = file_mtime (outfile)
149 return indate > outdate
154 # executes os.system(command) if infile is newer than
155 # outfile or outfile don't exist
157 def compile (command, workingdir, infile, outfile):
158 "Test if infile is newer than outfile. If so, cd to workingdir"
159 "and execute command"
160 indate = file_mtime (workingdir+infile)
162 outdate = file_mtime (workingdir+outfile)
163 recompile = indate > outdate
169 sys.stderr.write ('invoking `%s\'\n' % command)
171 status = os.system (command)
173 status = os.system ('cd %s; %s' %(workingdir, command))
183 1: {'a4':{10: 345, 11: 360, 12: 390},
184 'a5':{10: 276, 11: 276, 12: 276},
185 'b5':{10: 345, 11: 356, 12: 356},
186 'letter':{10: 345, 11: 360, 12: 390},
187 'legal': {10: 345, 11: 360, 12: 390},
188 'executive':{10: 345, 11: 360, 12: 379}},
189 2: {'a4':{10: 167, 11: 175, 12: 190},
190 'a5':{10: 133, 11: 133, 12: 133},
191 'b5':{10: 167, 11: 173, 12: 173},
192 'letter':{10: 167, 11: 175, 12: 190},
193 'legal':{10: 167, 11: 175, 12: 190},
194 'executive':{10: 167, 11: 175, 12: 184}}}
195 # >0 --> force all mudela to this pt size
196 self.force_mudela_fontsize = 0
197 self.force_verbatim_b = 0
199 'mudela-fontsize' : {'init': 16},
200 'papersize' : {'init': 'a4'},
201 'num-column' : {'init': 1},
202 'tex-fontsize' : {'init': 10}
204 def clear_for_new_file(self):
205 for var in self.__data.keys():
206 self.__data[var] = {'init': self.__data[var]['init']}
207 def clear_for_new_block(self):
208 for var in self.__data.keys():
209 if self.__data[var].has_key('block'):
210 del self.__data[var]['block']
211 def __get_variable(self, var):
212 if self.__data[var].has_key('block'):
213 return self.__data[var]['block']
214 elif self.__data[var].has_key('file'):
215 return self.__data[var]['file']
217 return self.__data[var]['init']
218 def setPapersize(self, size, requester):
219 self.__data['papersize'][requester] = size
220 def getPapersize(self):
221 return self.__get_variable('papersize')
222 def setMudelaFontsize(self, size, requester):
223 self.__data['mudela-fontsize'][requester] = size
224 def getMudelaFontsize(self):
225 if self.force_mudela_fontsize:
226 return self.force_mudela_fontsize
227 return self.__get_variable('mudela-fontsize')
228 def setTexFontsize(self, size, requester):
229 self.__data['tex-fontsize'][requester] = size
230 def getTexFontsize(self):
231 return self.__get_variable('tex-fontsize')
232 def setNumColumn(self, num, requester):
233 self.__data['num-column'][requester] = num
234 def getNumColumn(self):
235 return self.__get_variable('num-column')
236 def getLineWidth(self):
237 return self.__linewidth[self.getNumColumn()][self.getPapersize()][self.getTexFontsize()]
241 def __init__ (self, basename):
242 self.basename = basename
243 self.temp_filename = "%s/%s" %(outdir, 'mudela-temp.ly')
244 self.file = open (self.temp_filename, 'w')
247 self.graphic_type = 'tex'
248 self.code_type = 'unknown'
249 self.code_type_override = None
250 def write (self, line):
251 # match only if there is nothing but whitespace before \begin HACK
252 if re.search('^\s*\\\\begin{mudela}', line):
253 self.scan_begin_statement(line)
255 if self.code_type == 'unknown':
256 if re.search('^\s*\\\\score', line) or \
257 re.search('^\s*\\\\paper', line) or \
258 re.search('^\s*\\\\header', line) or \
259 re.search('^\s*[A-Za-z]*\s*=', line):
260 self.code_type = 'ly'
261 self.__lines.append(line)
262 def scan_begin_statement(self, line):
263 r = begin_mudela_opts_re.search(line)
266 optlist = re.compile('[\s,]*').split(o)
269 if 'fragment' in optlist:
270 self.code_type_override = 'sly'
271 if 'nonfragment' in optlist:
272 self.code_type_override = 'ly'
273 if 'singleline' in optlist:
274 self.code_type_override = 'sly'
275 if 'multiline' in optlist:
276 self.code_type_override = 'fly'
278 self.graphic_type = 'eps'
279 for pt in fontsize_pt2i.keys():
281 Props.setMudelaFontsize(fontsize_pt2i[pt], 'block')
282 def write_red_tape(self):
283 self.file.write ('\\include \"paper%d.ly\"\n' \
284 % Props.getMudelaFontsize())
286 s = fontsize_i2a[Props.getMudelaFontsize()]
287 if self.code_type == 'sly':
288 linewidth_str = 'linewidth = -1.\cm;'
289 self.code_type = 'fly'
291 linewidth_str = 'linewidth = %i.\\pt;' % Props.getLineWidth()
292 self.file.write("\\paper {"
295 + "castingalgorithm = \Gourlay; \n}")
296 #+ "castingalgorithm = \Wordwrap; indent = 2.\cm; \n}")
297 if self.code_type == 'fly':
298 self.file.write('\\score{\n\\notes\\relative c{')
300 if self.code_type == 'unknown':
301 self.code_type = 'sly'
302 if self.code_type_override:
303 self.code_type = self.code_type_override
304 self.write_red_tape()
305 for l in self.__lines:
307 if self.code_type == 'fly':
308 self.file.write('}}')
312 inf = outdir + self.basename + '.ly'
313 outf = outdir + self.basename + '.tex'
314 if not os.path.isfile(inf):
317 status = os.system ('diff -q %s %s' % (self.temp_filename, inf))
319 os.rename (self.temp_filename, inf)
321 recompile_b = need_recompile_b(inf, outf)
323 out_files.append((self.graphic_type, inf))
326 def insert_me_string(self):
327 "Returns a string that can be used directly in latex."
328 if self.graphic_type == 'tex':
329 return ['tex', self.basename]
330 elif self.graphic_type == 'eps':
331 return ['eps', self.basename]
333 raise SomethingIsSeriouslyBroken
336 def __init__ (self, name):
337 self.output_fn = '%s/%s' % (outdir, name)
339 def open_verbatim (self, line, level):
340 self.__lines.append('\\begin{verbatim}\n')
342 s = re.sub('veryverbatim[\s,]*', '', line)
343 s = re.sub('intertext=\"([^\"]*)\"[\s,]*', '', s)
344 s = re.sub(',\s*\]', ']', s)
345 s = re.sub('\[\]', '', s)
346 self.__lines.append(s);
347 def close_verbatim (self):
348 self.__lines.append('\\end{verbatim}\n')
350 self.__lines.append(s)
351 def create_graphics(self):
354 for line in self.__lines:
355 if type(line)==type([]):
358 if need_recompile_b(outdir+g[1]+'.ly', outdir+g[1]+'.tex'):
359 s = s + ' ' + g[1]+'.ly'
361 e = os.system('cd %s; lilypond %s' %(outdir, s))
363 print "error: lilypond exited with value", e
367 compile('tex %s' % g[1]+'.tex', outdir, g[1]+'.tex', g[1]+'.dvi')
368 compile('dvips -E -o %s %s' %(g[1]+'.eps', g[1]+'.dvi'), outdir,
369 g[1]+'.dvi', g[1]+'.eps')
370 def write_outfile(self):
371 file = open(self.output_fn+'.latex', 'w')
372 file.write('% Created by mudela-book\n')
373 for line in self.__lines:
374 if type(line)==type([]):
376 file.write('\\preMudelaExample\\input %s\n\postMudelaExample '\
377 # TeX applies the prefix of the main source automatically.
379 # % (outdir+line[1]+'.tex'))
381 ps_dim = ps_dimention(outdir+line[1]+'.eps')
382 file.write('\\parbox{%ipt}{\includegraphics{%s}}\n' \
383 % (ps_dim[0], line[1]+'.eps'))
384 # % (ps_dim[0], outdir+line[1]+'.eps'))
389 # given parameter s="\mudela[some options]{CODE} some text and commands"
390 # it returns a tuple:
392 # where the last number is the index of the ending '}'
393 def extract_command(s):
397 for idx in range(len(s)):
399 if not start_found_b:
405 if (start_found_b == 1) and (count == 0):
407 return s[start+1:idx], idx
410 def __init__ (self, filename):
411 for fn in [filename, filename+'.tex', filename+'.doc']:
413 self.infile = open (fn)
420 def get_lines (self):
421 lines = self.infile.readlines ()
422 (retlines, retdeps) = ([],[self.filename])
424 r_inp = input_re.search (line)
425 r_inc = include_re.search (line)
427 # Filename rules for \input :
429 # 1. will search for file with exact that name (tex-input.my will be found)
430 # 2. will search for file with .tex ext, (tex-input.my
431 # will find tex-input.my.tex)
432 # input: with .tex ext
433 # 1. will find exact match
435 # Filename rules for \include :
436 # 1. will only find files with name given to \include + .tex ext
439 t = Tex_input (r_inp.groups()[0])
441 retlines = retlines + ls[0]
442 retdeps = retdeps + ls[1]
444 print "warning: can't find %s, let's hope latex will" \
446 retlines.append (line)
449 t = Tex_input (r_inc.groups()[0]+'.tex')
451 ls[0].insert(0, '\\newpage\n')
452 ls[0].append('\\newpage\n')
453 retlines = retlines + ls[0]
454 retdeps = retdeps + ls[1]
456 print "warning: can't find %s, let's hope latex will" \
458 retlines.append (line)
460 # This code should be rewritten, it looks terrible
461 r_mud = defined_mudela_cmd_re.search(line)
464 ss = "\\\\verb(?P<xx>[^a-zA-Z])\s*\\\\%s\s*(?P=xx)" \
465 % re.escape(r_mud.group()[1:])
466 # just append the line if the command is inside \verb|..|
467 if re.search(ss, line):
468 retlines.append(line)
471 opts = r_mud.groups()[2]
472 cmd_start_idx = r_mud.span()[0]
473 if cmd_start_idx > 0:
474 retlines.append(line[:cmd_start_idx])
476 cmd_data, rest_idx = extract_command(line[cmd_start_idx:])
477 rest_idx = rest_idx + cmd_start_idx + 1
483 v = string.split(defined_mudela_cmd[r_mud.groups()[0]], '\n')
485 l = string.replace(l, '\\fontoptions', opts)
486 l = string.replace(l, '\\maininput', cmd_data)
488 r_mud = defined_mudela_cmd_re.search(line[rest_idx:])
494 retlines.append(line[rest_idx:])
496 line = line[rest_idx:]
498 retlines.append (line)
499 return (retlines, retdeps)
502 class Main_tex_input(Tex_input):
503 def __init__ (self, name, outname):
505 Tex_input.__init__ (self, name) # ugh
506 self.outname = outname
510 self.mudtex = Tex_output (self.outname)
514 # set to 'mudela' when we are processing mudela code,
515 # both verbatim and graphic-to-be
517 def set_sections (self, l):
518 if section_re.search (l):
519 self.section = self.section + 1
520 if chapter_re.search (l):
522 self.chapter = self.chapter + 1
524 def gen_basename (self):
525 return '%s-%d.%d.%d' % (self.outname, self.chapter,
526 self.section, self.fine_count)
527 def extract_papersize_from_documentclass(self, line):
528 pre = extract_papersize_re.search(line)
531 return pre.groups()[0]
532 def extract_fontsize_from_documentclass(self, line):
533 r = extract_fontsize_re.search(line)
535 return int(r.groups()[0])
537 preMudelaDef = postMudelaDef = 0
538 (lines, self.deps) = self.get_lines ()
542 if documentclass_re.search (line):
543 p = self.extract_papersize_from_documentclass (line)
545 Props.setPapersize(p, 'file')
546 f = self.extract_fontsize_from_documentclass (line)
548 Props.setTexFontsize (f, 'file')
549 elif twocolumn_re.search (line):
550 Props.setNumColumn (2, 'file')
551 elif onecolumn_re.search (line):
552 Props.setNumColumn (1, 'file')
553 elif preMudelaExample_re.search (line):
555 elif postMudelaExample_re.search (line):
557 elif begin_verbatim_re.search (line):
559 elif end_verbatim_re.search (line):
561 elif begin_document_re.search (line):
563 self.mudtex.write ('\\def\\preMudelaExample{}\n')
564 if not postMudelaDef:
565 self.mudtex.write ('\\def\\postMudelaExample{}\n')
567 elif mudela_file_re.search(line):
568 r = mudela_file_re.search(line)
570 self.mudela = Mudela_output(self.gen_basename())
572 full_path = find_file (fn)
574 print 'error: can\'t find file `%s\'.' % fn
578 r = file_ext_re.search(fn)
580 self.code_type = r.group(1)
582 f = open (full_path, 'r')
583 lines =f.readlines ()
585 self.mudela.write (x)
586 stat =self.mudela.close ()
588 print "(File %s needs recompiling)\n" % full_path
589 self.mudtex.write (self.mudela.insert_me_string())
590 self.deps.append (full_path)
593 self.fine_count = self.fine_count + 1
595 elif begin_mudela_re.search (line) and not latex_verbatim:
596 Props.clear_for_new_block()
598 if self.mode == 'mudela':
601 r = begin_mudela_opts_re.search (line)
603 o = r.group()[1:][:-1]
604 optlist = re.compile('[ ,]*').split(o)
605 m = intertext_re.search(r.group())
607 self.intertext = m.groups()[0]
609 self.intertext = None
612 if ('veryverbatim' in optlist):
614 elif ('verbatim' in optlist) or (Props.force_verbatim_b):
619 self.mudtex.open_verbatim (line, self.verbatim)
620 self.mudela = Mudela_output (self.gen_basename ())
621 self.mudela.write (line)
623 elif end_mudela_re.search (line) and not latex_verbatim:
625 if self.mode != 'mudela':
629 if self.verbatim == 2:
630 self.mudtex.write (line)
631 self.mudtex.close_verbatim ()
633 if self.verbatim and self.intertext:
634 self.mudtex.write(self.intertext)
635 self.mudtex.write (self.mudela.insert_me_string())
638 self.fine_count = self.fine_count + 1
643 if self.mode == 'mudela':
644 self.mudela.write (line)
646 self.mudtex.write (line)
648 self.mudtex.write (line)
649 self.set_sections(line)
650 self.mudtex.create_graphics()
651 self.mudtex.write_outfile()
656 sys.stdout.write("""Usage: mudela-book [options] FILE\n
657 Generate hybrid LaTeX input from Latex + mudela
659 -h, --help print this help
660 -d, --outdir=DIR directory to put generated files
661 -o, --outname=FILE prefix for filenames
662 --default-mudela-fontsize=??pt default fontsize for music
663 --force-mudela-fontsize=??pt force fontsize for all inline mudela
664 --force-verbatim make all mudela verbatim\n
665 --dependencies write dependencies
666 --include include path
667 --init mudela-book initfile
673 def write_deps (fn, out, deps):
674 out_fn = os.path.join (outdir, fn)
676 print 'writing `%s\'\n' % out_fn
678 f = open (out_fn, 'w')
679 target = re.sub (os.sep + os.sep, os.sep, os.path.join (outdir, out + '.latex'))
680 f.write ('%s: %s\n'% (target,
681 reduce (lambda x,y: x + ' '+ y, deps)))
685 sys.stderr.write ('This is %s version %s\n' % ('mudela-book', program_version))
688 global outdir, initfile, defined_mudela_cmd, defined_mudela_cmd_re
691 (options, files) = getopt.getopt(
692 sys.argv[1:], 'hd:o:I:', ['outdir=', 'outname=',
693 'default-mudela-fontsize=',
694 'force-mudela-fontsize=',
695 'help', 'dependencies', 'include=',
696 'force-verbatim', 'init='])
697 except getopt.error, msg:
705 if o == '--include' or o == '-I':
706 include_path.append (a)
707 elif o == '--outname' or o == '-o':
710 print "Mudela-book is confused by --outname on multiple files"
713 elif o == '--outdir' or o == '-d':
715 elif o == '--help' or o == '-h':
717 elif o == '--dependencies':
719 elif o == '--default-mudela-fontsize':
720 if not fontsize_pt2i.has_key(a):
721 print "Error: illegal fontsize:", a
722 print " accepted fontsizes are: 11pt, 13pt, 16pt, 20pt, 26pt"
724 Props.setMudelaFontsize(fontsize_pt2i[a], 'init')
725 elif o == '--force-mudela-fontsize':
726 if not fontsize_pt2i.has_key(a):
727 print "Error: illegal fontsize:", a
728 print " accepted fontsizes are: 11pt, 13pt, 16pt, 20pt, 26pt"
730 Props.force_mudela_fontsize = fontsize_pt2i[a]
731 elif o == '--force-verbatim':
732 Props.force_verbatim_b = 1
735 if outdir[-1:] != '/':
736 outdir = outdir + '/'
738 # r""" ... """ means: leave escape seqs alone.
739 defined_mudela_cmd = {'mudela': r"""
740 \begin{mudela}[eps \fontoptions]
754 defined_mudela_cmd[i] = d[i]
757 c = string.join (defined_mudela_cmd.keys(), '|')
759 defined_mudela_cmd_re = re.compile("\\\\(%s)(\[(\d*pt)\])*{([^}]*)}" %c)
761 if not os.path.isdir(outdir):
762 os.system('mkdir %s' % outdir)
764 for input_filename in files:
765 Props.clear_for_new_file()
769 my_outname = os.path.basename(os.path.splitext(input_filename)[0])
770 my_depname = my_outname + '.dep'
771 inp = Main_tex_input (input_filename, my_outname)
774 write_deps (my_depname, my_outname, inp.deps)