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
10 # * center option (??)
11 # * make mudela-book understand usepackage{geometry}
12 # * check that linewidth set in \paper is not wider than actual linewidth?
13 # * the following fails because mudelabook doesn't care that the
14 # last } after \end{mudela} finishes the marginpar:
19 # * force-verbatim is maybe not that useful since latex fails with footnotes,
20 # marginpars and others
25 # much rewritten by new author. I think the work has been split more
26 # logical between different classes.
29 # - speedup, do all mudela parsing at the same time to avoid multiple
30 # guile startup that takes some seconds on my computer
32 # - fixed default latex fontsize, it should be 10pt not 11pt
33 # - verbatim option no longer visible
34 # - mudela-book produces .dvi output
35 # - change to use castingalgorithm = \Gourlay instead of \Wordwrap. It gives
36 # better result on small linewidths.
37 # - mudela-book-doc.doc rewritten
39 # - junked floating and fragment options, added eps option
40 # - mudela-book will scan the mudela code to find out if it has to add
41 # paper-definition and \score{\notes{...}}
42 # - possible to define commands that look like this: \mudela{ c d e }
43 # - don't produce .dvi output, it was a silly idea...
45 # - removed init/mudela-book-defs.py, \mudela is defined inside mudela-book
46 # - fragment and nonfragment options will override autodetection of type of
47 # in mudela-block (voice contents or complete code)
49 # - fixed verbatim option behaviour: don't show \begin{mudela} \end{mudela}
50 # and show both mudela code and music
51 # - veryverbatim option, same as verbatim but show \begin{mudela}
52 # and \end{mudela}. (saves keystrokes in mudela-book-doc.doc)
53 # - intertext="bla bla bla" option
54 # - mudela-book now understand latex \begin{verbatim}
56 # - bf: \mudela{ \times 2/3{...} }
57 # * \t in \times is not tab character and
58 # * dont treat the first '}' as command ending
60 # - .fly and .sly files in \mudelafile{} are treated as standalone Lilypond.
61 # - Fragments, .fly and .sly files are in \relative mode.
63 # - bf: Default fragments have linewidth=-1.0
64 # - Added 'singleline' and 'multiline' options.
66 # - \mudelafile{} set linewidth correct, -1 for .sly and texlinewidth for .fly
67 # - changes to Mudela_output
68 # - changed RE to search for pre/postMudelaExample to make it possible to
69 # comment out a definition.
70 # - use sys.stderr and sys.stdout instead of print
72 # - junked separate versioning
73 # - pass -I options to lily.
74 # - portability stuff (os.sep).
85 program_version = '@TOPLEVEL_VERSION@'
90 fontsize_i2a = {11:'eleven', 13:'thirteen', 16:'sixteen',
91 20:'twenty', 26:'twentysix'}
92 fontsize_pt2i = {'11pt':11, '13pt':13, '16pt':16, '20pt':20, '26pt':26}
94 # perhaps we can do without this?
96 begin_mudela_re = re.compile ('^ *\\\\begin{mudela}')
97 begin_verbatim_re = re.compile ('^ *\\\\begin{verbatim}')
98 end_verbatim_re = re.compile ('^ *\\\\end{verbatim}')
99 extract_papersize_re = re.compile('\\\\documentclass[\[, ]*(\w*)paper[\w ,]*\]\{\w*\}')
100 extract_fontsize_re = re.compile('[ ,\[]*([0-9]*)pt')
101 begin_mudela_opts_re = re.compile('\[[^\]]*\]')
102 end_mudela_re = re.compile ('^ *\\\\end{mudela}')
103 section_re = re.compile ('\\\\section')
104 chapter_re = re.compile ('\\\\chapter')
105 input_re = re.compile ('^\\\\input{([^}]*)')
106 include_re = re.compile ('^\\\\include{([^}]*)')
107 begin_document_re = re.compile ('^ *\\\\begin{document}')
108 documentclass_re = re.compile('\\\\documentclass')
109 twocolumn_re = re.compile('\\\\twocolumn')
110 onecolumn_re = re.compile('\\\\onecolumn')
111 mudela_file_re = re.compile('\\\\mudelafile{([^}]+)}')
112 file_ext_re = re.compile('.+\\.([^.}]+$)')
113 preMudelaExample_re = re.compile('^\s*\\\\def\\\\preMudelaExample')
114 postMudelaExample_re = re.compile('^\s*\\\\def\\\\postMudelaExample')
115 boundingBox_re = re.compile('%%BoundingBox: ([0-9]+) ([0-9]+) ([0-9]+) ([0-9]+)')
116 intertext_re = re.compile("intertext=\"([^\"]*)\"")
118 def file_exist_b(name):
126 def ps_dimention(fname):
128 lines = fd.readlines()
130 s = boundingBox_re.search(line)
133 return (int(s.groups()[2])-int(s.groups()[0]),
134 int(s.groups()[3])-int(s.groups()[1]))
137 def find_file (name):
138 for a in include_path:
140 nm = os.path.join (a, name)
150 class SomethingIsSeriouslyBroken:
153 def file_mtime (name):
154 return os.stat (name)[8] #mod time
156 def need_recompile_b(infile, outfile):
157 indate = file_mtime (infile)
159 outdate = file_mtime (outfile)
160 return indate > outdate
165 # executes os.system(command) if infile is newer than
166 # outfile or outfile don't exist
168 def compile (command, workingdir, infile, outfile):
169 "Test if infile is newer than outfile. If so, cd to workingdir"
170 "and execute command"
172 indate = file_mtime (workingdir+infile)
174 outdate = file_mtime (workingdir+outfile)
175 recompile = indate > outdate
181 sys.stderr.write ('invoking `%s\'\n' % command)
183 status = os.system (command)
185 status = os.system ('cd %s; %s' %(workingdir, command))
195 1: {'a4':{10: 345, 11: 360, 12: 390},
196 'a5':{10: 276, 11: 276, 12: 276},
197 'b5':{10: 345, 11: 356, 12: 356},
198 'letter':{10: 345, 11: 360, 12: 390},
199 'legal': {10: 345, 11: 360, 12: 390},
200 'executive':{10: 345, 11: 360, 12: 379}},
201 2: {'a4':{10: 167, 11: 175, 12: 190},
202 'a5':{10: 133, 11: 133, 12: 133},
203 'b5':{10: 167, 11: 173, 12: 173},
204 'letter':{10: 167, 11: 175, 12: 190},
205 'legal':{10: 167, 11: 175, 12: 190},
206 'executive':{10: 167, 11: 175, 12: 184}}}
207 # >0 --> force all mudela to this pt size
208 self.force_mudela_fontsize = 0
209 self.force_verbatim_b = 0
211 'mudela-fontsize' : {'init': 16},
212 'papersize' : {'init': 'a4'},
213 'num-column' : {'init': 1},
214 'tex-fontsize' : {'init': 10}
216 def clear_for_new_file(self):
217 for var in self.__data.keys():
218 self.__data[var] = {'init': self.__data[var]['init']}
219 def clear_for_new_block(self):
220 for var in self.__data.keys():
221 if self.__data[var].has_key('block'):
222 del self.__data[var]['block']
223 def __get_variable(self, var):
224 if self.__data[var].has_key('block'):
225 return self.__data[var]['block']
226 elif self.__data[var].has_key('file'):
227 return self.__data[var]['file']
229 return self.__data[var]['init']
230 def setPapersize(self, size, requester):
231 self.__data['papersize'][requester] = size
232 def getPapersize(self):
233 return self.__get_variable('papersize')
234 def setMudelaFontsize(self, size, requester):
235 self.__data['mudela-fontsize'][requester] = size
236 def getMudelaFontsize(self):
237 if self.force_mudela_fontsize:
238 return self.force_mudela_fontsize
239 return self.__get_variable('mudela-fontsize')
240 def setTexFontsize(self, size, requester):
241 self.__data['tex-fontsize'][requester] = size
242 def getTexFontsize(self):
243 return self.__get_variable('tex-fontsize')
244 def setNumColumn(self, num, requester):
245 self.__data['num-column'][requester] = num
246 def getNumColumn(self):
247 return self.__get_variable('num-column')
248 def getLineWidth(self):
249 return self.__linewidth[self.getNumColumn()][self.getPapersize()][self.getTexFontsize()]
253 """ Using only self.code_type to deside both value of linewith and
254 if we have to put code into \score{...} was silly. Now we use:
255 self.code_type: show us what need to be added.
257 'NOTES' : add \context Voice{ ... }
258 'CONTEXT' : add \score{ ... }
260 self.single_line_b: 0 : linewidth=-1 1: linewith=textwidth
262 def __init__ (self, basename):
263 self.basename = basename
264 self.temp_filename = "%s/%s" %(outdir, 'mudela-temp.ly')
265 self.file = open (self.temp_filename, 'w')
268 self.graphic_type = 'tex'
269 self.code_type = None
270 self.single_line_b = 1
272 def write (self, line):
273 # match only if there is nothing but whitespace before \begin.
274 # we should not have to do this RE for every line
275 if re.search('^\s*\\\\begin{mudela}', line):
276 r = begin_mudela_opts_re.search(line)
279 self.optlist = re.compile('[\s,]*').split(o)
282 else: # ugh this is NOT bulletproof...
283 if not self.code_type:
284 if re.search('^\s*%', line) or re.search('^\s*$', line):
286 elif re.search('^\s*\\\\context', line):
287 self.code_type = 'CONTEXT'
288 self.single_line_b = 0
289 elif re.search('^\s*\\\\score', line) or \
290 re.search('^\s*\\\\paper', line) or \
291 re.search('^\s*\\\\header', line) or \
292 re.search('^\s*\\\\version', line) or \
293 re.search('^\s*\\\\include', line) or \
294 re.search('^\s*[A-Za-z]*\s*=', line):
295 self.code_type = 'COMPLETE'
296 self.single_line_b = 0
298 self.code_type = 'NOTES'
299 self.single_line_b = 1
300 self.__lines.append(line)
301 def write_red_tape(self):
302 if 'eps' in self.optlist:
303 self.graphic_type = 'eps'
304 #self.single_line_b = 1
305 if 'fragment' in self.optlist:
306 self.code_type = 'NOTES'
307 if 'nonfragment' in self.optlist:
308 self.code_type = 'COMPLETE'
309 if 'multiline' in self.optlist:
310 self.single_line_b = 0
311 for pt in fontsize_pt2i.keys():
312 if pt in self.optlist:
313 Props.setMudelaFontsize(fontsize_pt2i[pt], 'block')
314 self.file.write ('\\include \"paper%d.ly\"\n' \
315 % Props.getMudelaFontsize())
317 s = fontsize_i2a[Props.getMudelaFontsize()]
318 if 'singleline' in self.optlist:
319 self.single_line_b = 1
320 if self.single_line_b:
321 linewidth_str = 'linewidth = -1.\cm;'
323 linewidth_str = 'linewidth = %i.\\pt;' % Props.getLineWidth()
324 self.file.write("\\paper {"
327 + "castingalgorithm = \Gourlay; \n}\n")
328 #+ "castingalgorithm = \Wordwrap; indent = 2.\cm; \n}")
329 if self.code_type == 'CONTEXT':
330 self.file.write('\\score{\n\\notes\\relative c{')
331 if self.code_type == 'NOTES' :
332 self.file.write('\\score{\n\\notes\\relative c{\\context Voice{')
334 self.write_red_tape()
335 for l in self.__lines:
337 if self.code_type == 'CONTEXT':
338 self.file.write('}}')
339 elif self.code_type == 'NOTES':
340 self.file.write('}}}')
343 inf = outdir + self.basename + '.ly'
344 outf = outdir + self.basename + '.tex'
345 if not os.path.isfile(inf):
348 status = os.system ('diff -q %s %s' % (self.temp_filename, inf))
350 os.rename (self.temp_filename, inf)
352 recompile_b = need_recompile_b(inf, outf)
354 out_files.append((self.graphic_type, inf))
357 def insert_me_string(self):
358 "ugh the name of this function is wrong"
359 if self.graphic_type == 'tex':
360 return ['tex', self.basename]
361 elif self.graphic_type == 'eps':
362 return ['eps', self.basename]
364 raise SomethingIsSeriouslyBroken
367 def __init__ (self, name):
368 self.output_fn = '%s/%s' % (outdir, name)
370 def open_verbatim (self, line, level):
371 self.__lines.append('\\begin{verbatim}\n')
373 s = re.sub('veryverbatim[\s,]*', '', line)
374 s = re.sub('intertext=\"([^\"]*)\"[\s,]*', '', s)
375 s = re.sub(',\s*\]', ']', s)
376 s = re.sub('\[\]', '', s)
377 self.__lines.append(s);
378 def close_verbatim (self):
379 self.__lines.append('\\end{verbatim}\n')
381 self.__lines.append(s)
382 def create_graphics(self):
385 for line in self.__lines:
386 if type(line)==type([]):
389 if need_recompile_b(outdir+g[1]+'.ly', outdir+g[1]+'.tex'):
390 s = s + ' ' + g[1]+'.ly'
393 for inc in __main__.include_path :
395 if p[0] <> os.sep and outdir: # UGH-> win32 ?
396 p = '..' + os.sep + p
397 lilyoptions = lilyoptions + " -I \"%s\"" % p
400 cmd = 'cd %s; lilypond %s %s' %(outdir, lilyoptions, s)
401 sys.stderr.write ('invoking command %s' % cmd)
404 sys.stderr.write("error: lilypond exited with value %i\n" % e)
408 compile('tex %s' % g[1]+'.tex', outdir, g[1]+'.tex', g[1]+'.dvi')
409 compile('dvips -E -o %s %s' %(g[1]+'.eps', g[1]+'.dvi'), outdir,
410 g[1]+'.dvi', g[1]+'.eps')
411 def write_outfile(self):
412 file = open(self.output_fn+'.latex', 'w')
413 file.write('% Created by mudela-book\n')
415 for line in self.__lines:
416 if type(line)==type([]):
417 if last_line == '\n':
418 file.write(r'\vspace{0.5cm}')
420 file.write('\\preMudelaExample \\input %s \\postMudelaExample\n'\
423 ps_dim = ps_dimention(outdir+line[1]+'.eps')
424 file.write('\\noindent\\parbox{%ipt}{\includegraphics{%s}}\n' \
425 % (ps_dim[0], line[1]+'.eps'))
428 if type(last_line)==type([]):
430 file.write(r'\vspace{0.5cm}')
434 # given parameter s="\mudela[some options]{CODE} some text and commands"
435 # it returns a tuple:
437 # where the last number is the index of the ending '}'
438 def extract_command(s):
442 for idx in range(len(s)):
444 if not start_found_b:
450 if (start_found_b == 1) and (count == 0):
452 return s[start+1:idx], idx
455 def __init__ (self, filename):
456 for fn in [filename, filename+'.tex', filename+'.doc']:
458 self.infile = open (fn)
465 def get_lines (self):
466 lines = self.infile.readlines ()
467 (retlines, retdeps) = ([],[self.filename])
469 r_inp = input_re.search (line)
470 r_inc = include_re.search (line)
472 # Filename rules for \input :
474 # 1. will search for file with exact that name (tex-input.my will be found)
475 # 2. will search for file with .tex ext, (tex-input.my
476 # will find tex-input.my.tex)
477 # input: with .tex ext
478 # 1. will find exact match
480 # Filename rules for \include :
481 # 1. will only find files with name given to \include + .tex ext
484 t = Tex_input (r_inp.groups()[0])
486 retlines = retlines + ls[0]
487 retdeps = retdeps + ls[1]
489 sys.stderr.write("warning: can't find %s, let's hope latex will\n" % r_inp.groups()[0])
490 retlines.append (line)
493 t = Tex_input (r_inc.groups()[0]+'.tex')
495 ls[0].insert(0, '\\newpage\n')
496 ls[0].append('\\newpage\n')
497 retlines = retlines + ls[0]
498 retdeps = retdeps + ls[1]
500 sys.stderr.write("warning: can't find %s, let's hope latex will" % r_inc.groups()[0])
501 retlines.append (line)
503 # This code should be rewritten, it looks terrible
504 r_mud = defined_mudela_cmd_re.search(line)
507 ss = "\\\\verb(?P<xx>[^a-zA-Z])\s*\\\\%s\s*(?P=xx)" \
508 % re.escape(r_mud.group()[1:])
509 # just append the line if the command is inside \verb|..|
510 if re.search(ss, line):
511 retlines.append(line)
514 opts = r_mud.groups()[2]
515 cmd_start_idx = r_mud.span()[0]
516 if cmd_start_idx > 0:
517 retlines.append(line[:cmd_start_idx])
519 cmd_data, rest_idx = extract_command(line[cmd_start_idx:])
520 rest_idx = rest_idx + cmd_start_idx + 1
526 v = string.split(defined_mudela_cmd[r_mud.groups()[0]], '\n')
528 l = string.replace(l, '\\fontoptions', opts)
529 l = string.replace(l, '\\maininput', cmd_data)
531 r_mud = defined_mudela_cmd_re.search(line[rest_idx:])
537 retlines.append(line[rest_idx:])
539 line = line[rest_idx:]
541 retlines.append (line)
542 return (retlines, retdeps)
545 class Main_tex_input(Tex_input):
546 def __init__ (self, name, outname):
548 Tex_input.__init__ (self, name) # ugh
549 self.outname = outname
553 self.mudtex = Tex_output (self.outname)
557 # set to 'mudela' when we are processing mudela code,
558 # both verbatim and graphic-to-be
560 def set_sections (self, l):
561 if section_re.search (l):
562 self.section = self.section + 1
563 if chapter_re.search (l):
565 self.chapter = self.chapter + 1
567 def gen_basename (self):
568 return '%s-%d.%d.%d' % (self.outname, self.chapter,
569 self.section, self.fine_count)
570 def extract_papersize_from_documentclass(self, line):
571 pre = extract_papersize_re.search(line)
574 return pre.groups()[0]
575 def extract_fontsize_from_documentclass(self, line):
576 r = extract_fontsize_re.search(line)
578 return int(r.groups()[0])
580 preMudelaDef = postMudelaDef = 0
581 (lines, self.deps) = self.get_lines ()
585 if documentclass_re.search (line):
586 p = self.extract_papersize_from_documentclass (line)
588 Props.setPapersize(p, 'file')
589 f = self.extract_fontsize_from_documentclass (line)
591 Props.setTexFontsize (f, 'file')
592 elif twocolumn_re.search (line):
593 Props.setNumColumn (2, 'file')
594 elif onecolumn_re.search (line):
595 Props.setNumColumn (1, 'file')
596 elif preMudelaExample_re.search (line):
598 elif postMudelaExample_re.search (line):
600 elif begin_verbatim_re.search (line):
602 elif end_verbatim_re.search (line):
604 elif begin_document_re.search (line):
606 self.mudtex.write ('\\def\\preMudelaExample{}\n')
607 if not postMudelaDef:
608 self.mudtex.write ('\\def\\postMudelaExample{}\n')
610 elif mudela_file_re.search(line):
611 r = mudela_file_re.search(line)
613 self.mudela = Mudela_output(self.gen_basename())
615 full_path = find_file (fn)
617 sys.stderr.write("error: can't find file '%s'\n" % fn)
620 f = open (full_path, 'r')
621 lines =f.readlines ()
622 self.mudela.write ('%% This is a copy of file %s\n' % full_path)
624 self.mudela.write (x)
625 r = file_ext_re.search(fn)
627 if r.group(1) == 'fly':
628 self.mudela.optlist.append('multiline')
629 stat =self.mudela.close ()
631 sys.stdout.write("(File %s needs recompiling)\n" % full_path)
632 self.mudtex.write (self.mudela.insert_me_string())
633 self.deps.append (full_path)
636 self.fine_count = self.fine_count + 1
638 elif begin_mudela_re.search (line) and not latex_verbatim:
639 Props.clear_for_new_block()
641 if self.mode == 'mudela':
644 r = begin_mudela_opts_re.search (line)
646 o = r.group()[1:][:-1]
647 optlist = re.compile('[ ,]*').split(o)
648 m = intertext_re.search(r.group())
650 self.intertext = m.groups()[0]
652 self.intertext = None
655 if ('veryverbatim' in optlist):
657 elif ('verbatim' in optlist) or (Props.force_verbatim_b):
662 self.mudtex.open_verbatim (line, self.verbatim)
663 self.mudela = Mudela_output (self.gen_basename ())
664 self.mudela.write (line)
666 elif end_mudela_re.search (line) and not latex_verbatim:
668 if self.mode != 'mudela':
672 if self.verbatim == 2:
673 self.mudtex.write (line)
674 self.mudtex.close_verbatim ()
676 if self.verbatim and self.intertext:
677 self.mudtex.write(self.intertext)
678 self.mudtex.write (self.mudela.insert_me_string())
681 self.fine_count = self.fine_count + 1
686 if self.mode == 'mudela':
687 self.mudela.write (line)
689 self.mudtex.write (line)
691 self.mudtex.write (line)
692 self.set_sections(line)
693 self.mudtex.create_graphics()
694 self.mudtex.write_outfile()
699 sys.stdout.write("""Usage: mudela-book [options] FILE\n
700 Generate hybrid LaTeX input from Latex + mudela
702 -h, --help print this help
703 -d, --outdir=DIR directory to put generated files
704 -o, --outname=FILE prefix for filenames
705 --default-mudela-fontsize=??pt default fontsize for music
706 --force-mudela-fontsize=??pt force fontsize for all inline mudela
707 --force-verbatim make all mudela verbatim\n
708 --dependencies write dependencies
709 --include include path
710 --init mudela-book initfile
716 def write_deps (fn, out, deps):
717 out_fn = os.path.join (outdir, fn)
719 sys.stdout.write('writing `%s\'\n' % out_fn)
721 f = open (out_fn, 'w')
722 target = re.sub (os.sep + os.sep, os.sep, os.path.join (outdir, out + '.latex'))
723 f.write ('%s: %s\n'% (target,
724 reduce (lambda x,y: x + ' '+ y, deps)))
728 sys.stderr.write ('This is %s version %s\n' % ('mudela-book', program_version))
731 global outdir, initfile, defined_mudela_cmd, defined_mudela_cmd_re
734 (options, files) = getopt.getopt(
735 sys.argv[1:], 'hd:o:I:', ['outdir=', 'outname=',
736 'default-mudela-fontsize=',
737 'force-mudela-fontsize=',
738 'help', 'dependencies', 'include=',
739 'force-verbatim', 'init='])
740 except getopt.error, msg:
741 sys.stderr.write("error: %s" % msg)
748 if o == '--include' or o == '-I':
749 include_path.append (a)
750 elif o == '--outname' or o == '-o':
753 sys.stderr.write("Mudela-book is confused by --outname on multiple files")
756 elif o == '--outdir' or o == '-d':
758 elif o == '--help' or o == '-h':
760 elif o == '--dependencies':
762 elif o == '--default-mudela-fontsize':
763 if not fontsize_pt2i.has_key(a):
764 sys.stderr.write("Error: invalid fontsize: %s" % a)
765 sys.stderr.write(" accepted fontsizes are: 11pt, 13pt, 16pt, 20pt, 26pt")
767 Props.setMudelaFontsize(fontsize_pt2i[a], 'init')
768 elif o == '--force-mudela-fontsize':
769 if not fontsize_pt2i.has_key(a):
770 sys.stderr.write("Error: invalid fontsize: %s" % a)
771 sys.stderr.write(" accepted fontsizes are: 11pt, 13pt, 16pt, 20pt, 26pt")
773 Props.force_mudela_fontsize = fontsize_pt2i[a]
774 elif o == '--force-verbatim':
775 Props.force_verbatim_b = 1
778 if outdir[-1:] != os.sep:
779 outdir = outdir + os.sep
781 # r""" ... """ means: leave escape seqs alone.
782 defined_mudela_cmd = {'mudela': r"""
783 \begin{mudela}[eps, singleline \fontoptions]
797 defined_mudela_cmd[i] = d[i]
800 c = string.join (defined_mudela_cmd.keys(), '|')
802 defined_mudela_cmd_re = re.compile("\\\\(%s)(\[(\d*pt)\])*{([^}]*)}" %c)
804 if not os.path.isdir(outdir):
805 os.system('mkdir %s' % outdir)
807 for input_filename in files:
808 Props.clear_for_new_file()
812 my_outname = os.path.basename(os.path.splitext(input_filename)[0])
813 my_depname = my_outname + '.dep'
814 inp = Main_tex_input (input_filename, my_outname)
817 write_deps (my_depname, my_outname, inp.deps)