]> git.donarmstrong.com Git - lilypond.git/blob - scripts/lilypond-book.py
Change Snippets compilation
[lilypond.git] / scripts / lilypond-book.py
1 #!@TARGET_PYTHON@
2
3 '''
4 Example usage:
5
6 test:
7   lilypond-book --filter="tr '[a-z]' '[A-Z]'" BOOK
8
9 convert-ly on book:
10   lilypond-book --filter="convert-ly --no-version --from=1.6.11 -" BOOK
11
12 classic lilypond-book:
13   lilypond-book --process="lilypond" BOOK.tely
14
15 TODO:
16
17   *  this script is too complex. Modularize.
18   
19   *  ly-options: intertext?
20   *  --line-width?
21   *  eps in latex / eps by lilypond -b ps?
22   *  check latex parameters, twocolumn, multicolumn?
23   *  use --png --ps --pdf for making images?
24
25   *  Converting from lilypond-book source, substitute:
26    @mbinclude foo.itely -> @include foo.itely
27    \mbinput -> \input
28
29 '''
30
31 import stat
32 import tempfile
33 import commands
34 import os
35 import sys
36 import re
37 import md5
38 import operator
39
40 """
41 @relocate-preamble@
42 """
43
44 import lilylib as ly
45 import fontextract
46 global _;_=ly._
47
48
49 # Lilylib globals.
50 program_version = '@TOPLEVEL_VERSION@'
51 program_name = os.path.basename (sys.argv[0])
52
53 original_dir = os.getcwd ()
54 backend = 'ps'
55
56 help_summary = (
57 _ ("Process LilyPond snippets in hybrid HTML, LaTeX, texinfo or DocBook document.")
58 + '\n\n'
59 + _ ("Examples:")
60 + '''
61  lilypond-book --filter="tr '[a-z]' '[A-Z]'" %(BOOK)s
62  lilypond-book --filter="convert-ly --no-version --from=2.0.0 -" %(BOOK)s
63  lilypond-book --process='lilypond -I include' %(BOOK)s
64 ''' % {'BOOK': _ ("BOOK")})
65
66 authors = ('Jan Nieuwenhuizen <janneke@gnu.org>',
67       'Han-Wen Nienhuys <hanwen@xs4all.nl>')
68
69 ################################################################
70 def exit (i):
71     if global_options.verbose:
72         raise Exception (_ ('Exiting (%d)...') % i)
73     else:
74         sys.exit (i)
75
76 def identify ():
77     ly.encoded_write (sys.stdout, '%s (GNU LilyPond) %s\n' % (program_name, program_version))
78
79 progress = ly.progress
80
81 def warning (s):
82     ly.stderr_write (program_name + ": " + _ ("warning: %s") % s + '\n')
83
84 def error (s):
85     ly.stderr_write (program_name + ": " + _ ("error: %s") % s + '\n')
86
87 def ps_page_count (ps_name):
88     header = open (ps_name).read (1024)
89     m = re.search ('\n%%Pages: ([0-9]+)', header)
90     if m:
91         return int (m.group (1))
92     return 0
93
94 def warranty ():
95     identify ()
96     ly.encoded_write (sys.stdout, '''
97 %s
98
99 %s
100
101 %s
102 %s
103 ''' % ( _ ('Copyright (c) %s by') % '2001--2007',
104     ' '.join (authors),
105    _ ("Distributed under terms of the GNU General Public License."),
106    _ ("It comes with NO WARRANTY.")))
107
108 def get_option_parser ():
109     p = ly.get_option_parser (usage=_ ("%s [OPTION]... FILE") % 'lilypond-book',
110                               description=help_summary,
111                               add_help_option=False)
112
113     p.add_option ('-F', '--filter', metavar=_ ("FILTER"),
114                   action="store",
115                   dest="filter_cmd",
116                   help=_ ("pipe snippets through FILTER [convert-ly -n -]"),
117                   default=None)
118
119     p.add_option ('-f', '--format',
120                   help=_ ("use output format FORMAT (texi [default], texi-html, latex, html, docbook)"),
121                   action='store')
122
123     p.add_option("-h", "--help",
124                  action="help",
125                  help=_ ("show this help and exit"))
126
127     p.add_option ("-I", '--include', help=_ ("add DIR to include path"),
128                   metavar=_ ("DIR"),
129                   action='append', dest='include_path',
130                   default=[os.path.abspath (os.getcwd ())])
131
132     p.add_option ('--info-images-dir',
133                   help=_ ("format Texinfo output so that Info will "
134                           "look for images of music in DIR"),
135                   metavar=_ ("DIR"),
136                   action='store', dest='info_images_dir',
137                   default='')
138
139     p.add_option ('--left-padding', 
140                   metavar=_ ("PAD"),
141                   dest="padding_mm",
142                   help=_ ("pad left side of music to align music inspite of uneven bar numbers (in mm)"),
143                   type="float",
144                   default=3.0)
145     
146     p.add_option ("-o", '--output', help=_ ("write output to DIR"),
147                   metavar=_ ("DIR"),
148                   action='store', dest='output_name',
149                   default='')
150     
151     p.add_option ('-P', '--process', metavar=_ ("COMMAND"),
152                   help = _ ("process ly_files using COMMAND FILE..."),
153                   action='store', 
154                   dest='process_cmd', default='lilypond -dbackend=eps')
155
156     p.add_option ('--pdf',
157                   action="store_true",
158                   dest="create_pdf",
159                   help=_ ("create PDF files for use with PDFTeX"),
160                   default=False)
161
162     p.add_option ('', '--psfonts', action="store_true", dest="psfonts",
163                   help=_ ('''extract all PostScript fonts into INPUT.psfonts for LaTeX
164 must use this with dvips -h INPUT.psfonts'''),
165                   default=None)
166
167     p.add_option ('-V', '--verbose', help=_ ("be verbose"),
168                   action="store_true",
169                   default=False,
170                   dest="verbose")
171
172     p.version = "@TOPLEVEL_VERSION@"
173     p.add_option("--version",
174                  action="version",
175                  help=_ ("show version number and exit"))
176
177     p.add_option ('-w', '--warranty',
178                   help=_ ("show warranty and copyright"),
179                   action='store_true')
180     p.add_option_group (ly.display_encode (_ ('Bugs')),
181                         description=(_ ("Report bugs via")
182                                      + ''' http://post.gmane.org/post.php'''
183                                      '''?group=gmane.comp.gnu.lilypond.bugs\n'''))
184     return p
185
186 lilypond_binary = os.path.join ('@bindir@', 'lilypond')
187
188 # Only use installed binary when we are installed too.
189 if '@bindir@' == ('@' + 'bindir@') or not os.path.exists (lilypond_binary):
190     lilypond_binary = 'lilypond'
191
192 global_options = None
193
194
195 default_ly_options = { 'alt': "[image of music]" }
196
197 #
198 # Is this pythonic?  Personally, I find this rather #define-nesque. --hwn
199 #
200 AFTER = 'after'
201 BEFORE = 'before'
202 DOCBOOK = 'docbook'
203 EXAMPLEINDENT = 'exampleindent'
204 FILTER = 'filter'
205 FRAGMENT = 'fragment'
206 HTML = 'html'
207 INDENT = 'indent'
208 LATEX = 'latex'
209 LAYOUT = 'layout'
210 LINE_WIDTH = 'line-width'
211 LILYQUOTE = 'lilyquote'
212 NOFRAGMENT = 'nofragment'
213 NOINDENT = 'noindent'
214 NOQUOTE = 'noquote'
215 NOTES = 'body'
216 NOTIME = 'notime'
217 OUTPUT = 'output'
218 OUTPUTIMAGE = 'outputimage'
219 PACKED = 'packed'
220 PAPER = 'paper'
221 PREAMBLE = 'preamble'
222 PRINTFILENAME = 'printfilename'
223 QUOTE = 'quote'
224 RAGGED_RIGHT = 'ragged-right'
225 RELATIVE = 'relative'
226 STAFFSIZE = 'staffsize'
227 TEXIDOC = 'texidoc'
228 TEXINFO = 'texinfo'
229 VERBATIM = 'verbatim'
230 FONTLOAD = 'fontload'
231 FILENAME = 'filename'
232 ALT = 'alt'
233
234
235 # NOTIME has no opposite so it isn't part of this dictionary.
236 # NOQUOTE is used internally only.
237 no_options = {
238     NOFRAGMENT: FRAGMENT,
239     NOINDENT: INDENT,
240 }
241
242
243 # Recognize special sequences in the input.
244 #
245 #   (?P<name>regex) -- Assign result of REGEX to NAME.
246 #   *? -- Match non-greedily.
247 #   (?m) -- Multiline regex: Make ^ and $ match at each line.
248 #   (?s) -- Make the dot match all characters including newline.
249 #   (?x) -- Ignore whitespace in patterns.
250 no_match = 'a\ba'
251 snippet_res = {
252  ##
253     DOCBOOK: {
254         'include':
255          no_match,
256
257         'lilypond':
258          r'''(?smx)
259           (?P<match>
260           <(?P<inline>(inline)?)mediaobject>\s*<textobject.*?>\s*<programlisting\s+language="lilypond".*?(role="(?P<options>.*?)")?>(?P<code>.*?)</programlisting\s*>\s*</textobject\s*>\s*</(inline)?mediaobject>)''',
261
262         'lilypond_block':
263          r'''(?smx)
264           (?P<match>
265           <(?P<inline>(inline)?)mediaobject>\s*<textobject.*?>\s*<programlisting\s+language="lilypond".*?(role="(?P<options>.*?)")?>(?P<code>.*?)</programlisting\s*>\s*</textobject\s*>\s*</(inline)?mediaobject>)''',
266
267         'lilypond_file':
268          r'''(?smx)
269           (?P<match>
270           <(?P<inline>(inline)?)mediaobject>\s*<imageobject.*?>\s*<imagedata\s+fileref="(?P<filename>.*?\.ly)"\s*(role="(?P<options>.*?)")?\s*(/>|>\s*</imagedata>)\s*</imageobject>\s*</(inline)?mediaobject>)''',
271
272         'multiline_comment':
273          r'''(?smx)
274           (?P<match>
275           \s*(?!@c\s+)
276           (?P<code><!--\s.*?!-->)
277           \s)''',
278
279         'singleline_comment':
280          no_match,
281
282         'verb':
283          no_match,
284
285         'verbatim':
286         no_match,
287         
288     }, 
289     ##
290     HTML: {
291         'include':
292          no_match,
293
294         'lilypond':
295          r'''(?mx)
296           (?P<match>
297           <lilypond
298            (\s*(?P<options>.*?)\s*:)?\s*
299            (?P<code>.*?)
300           />)''',
301
302         'lilypond_block':
303          r'''(?msx)
304           (?P<match>
305           <lilypond
306            \s*(?P<options>.*?)\s*
307           >
308           (?P<code>.*?)
309           </lilypond>)''',
310
311         'lilypond_file':
312          r'''(?mx)
313           (?P<match>
314           <lilypondfile
315            \s*(?P<options>.*?)\s*
316           >
317           \s*(?P<filename>.*?)\s*
318           </lilypondfile>)''',
319
320         'multiline_comment':
321          r'''(?smx)
322           (?P<match>
323           \s*(?!@c\s+)
324           (?P<code><!--\s.*?!-->)
325           \s)''',
326
327         'singleline_comment':
328          no_match,
329
330         'verb':
331          r'''(?x)
332           (?P<match>
333            (?P<code><pre>.*?</pre>))''',
334
335         'verbatim':
336          r'''(?x)
337           (?s)
338           (?P<match>
339            (?P<code><pre>\s.*?</pre>\s))''',
340     },
341
342     ##
343     LATEX: {
344         'include':
345          r'''(?smx)
346           ^[^%\n]*?
347           (?P<match>
348           \\input\s*{
349            (?P<filename>\S+?)
350           })''',
351
352         'lilypond':
353          r'''(?smx)
354           ^[^%\n]*?
355           (?P<match>
356           \\lilypond\s*(
357           \[
358            \s*(?P<options>.*?)\s*
359           \])?\s*{
360            (?P<code>.*?)
361           })''',
362
363         'lilypond_block':
364          r'''(?smx)
365           ^[^%\n]*?
366           (?P<match>
367           \\begin\s*(
368           \[
369            \s*(?P<options>.*?)\s*
370           \])?\s*{lilypond}
371            (?P<code>.*?)
372           ^[^%\n]*?
373           \\end\s*{lilypond})''',
374
375         'lilypond_file':
376          r'''(?smx)
377           ^[^%\n]*?
378           (?P<match>
379           \\lilypondfile\s*(
380           \[
381            \s*(?P<options>.*?)\s*
382           \])?\s*\{
383            (?P<filename>\S+?)
384           })''',
385
386         'multiline_comment':
387          no_match,
388
389         'singleline_comment':
390          r'''(?mx)
391           ^.*?
392           (?P<match>
393            (?P<code>
394            %.*$\n+))''',
395
396         'verb':
397          r'''(?mx)
398           ^[^%\n]*?
399           (?P<match>
400            (?P<code>
401            \\verb(?P<del>.)
402             .*?
403            (?P=del)))''',
404
405         'verbatim':
406          r'''(?msx)
407           ^[^%\n]*?
408           (?P<match>
409            (?P<code>
410            \\begin\s*{verbatim}
411             .*?
412            \\end\s*{verbatim}))''',
413     },
414
415     ##
416     TEXINFO: {
417         'include':
418          r'''(?mx)
419           ^(?P<match>
420           @include\s+
421            (?P<filename>\S+))''',
422
423         'lilypond':
424          r'''(?smx)
425           ^[^\n]*?(?!@c\s+)[^\n]*?
426           (?P<match>
427           @lilypond\s*(
428           \[
429            \s*(?P<options>.*?)\s*
430           \])?\s*{
431            (?P<code>.*?)
432           })''',
433
434         'lilypond_block':
435          r'''(?msx)
436           ^(?P<match>
437           @lilypond\s*(
438           \[
439            \s*(?P<options>.*?)\s*
440           \])?\s+?
441           ^(?P<code>.*?)
442           ^@end\s+lilypond)\s''',
443
444         'lilypond_file':
445          r'''(?mx)
446           ^(?P<match>
447           @lilypondfile\s*(
448           \[
449            \s*(?P<options>.*?)\s*
450           \])?\s*{
451            (?P<filename>\S+)
452           })''',
453
454         'multiline_comment':
455          r'''(?smx)
456           ^(?P<match>
457            (?P<code>
458            @ignore\s
459             .*?
460            @end\s+ignore))\s''',
461
462         'singleline_comment':
463          r'''(?mx)
464           ^.*
465           (?P<match>
466            (?P<code>
467            @c([ \t][^\n]*|)\n))''',
468
469     # Don't do this: It interferes with @code{@{}.
470     #        'verb': r'''(?P<code>@code{.*?})''',
471
472         'verbatim':
473          r'''(?sx)
474           (?P<match>
475            (?P<code>
476            @example
477             \s.*?
478            @end\s+example\s))''',
479     },
480 }
481
482
483
484
485 format_res = {
486     DOCBOOK: {        
487         'intertext': r',?\s*intertext=\".*?\"',
488         'option_sep': '\s*',
489     }, 
490     HTML: {
491         'intertext': r',?\s*intertext=\".*?\"',
492         'option_sep': '\s*',
493     },
494
495     LATEX: {
496         'intertext': r',?\s*intertext=\".*?\"',
497         'option_sep': '\s*,\s*',
498     },
499
500     TEXINFO: {
501         'intertext': r',?\s*intertext=\".*?\"',
502         'option_sep': '\s*,\s*',
503     },
504 }
505
506 # Options without a pattern in ly_options.
507 simple_options = [
508     EXAMPLEINDENT,
509     FRAGMENT,
510     NOFRAGMENT,
511     NOINDENT,
512     PRINTFILENAME,
513     TEXIDOC,
514     VERBATIM,
515     FONTLOAD,
516     FILENAME,
517     ALT
518 ]
519
520 ly_options = {
521     ##
522     NOTES: {
523         RELATIVE: r'''\relative c%(relative_quotes)s''',
524     },
525
526     ##
527     PAPER: {
528         INDENT: r'''indent = %(indent)s''',
529
530         LINE_WIDTH: r'''line-width = %(line-width)s''',
531
532         QUOTE: r'''line-width = %(line-width)s - 2.0 * %(exampleindent)s''',
533
534         LILYQUOTE: r'''line-width = %(line-width)s - 2.0 * %(exampleindent)s''',
535
536         RAGGED_RIGHT: r'''ragged-right = ##t''',
537
538         PACKED: r'''packed = ##t''',
539     },
540
541     ##
542     LAYOUT: {
543         NOTIME: r'''
544  \context {
545   \Score
546   timing = ##f
547  }
548  \context {
549   \Staff
550   \remove Time_signature_engraver
551  }''',
552     },
553
554     ##
555     PREAMBLE: {
556         STAFFSIZE: r'''#(set-global-staff-size %(staffsize)s)''',
557     },
558 }
559
560 output = {
561     ##
562     DOCBOOK: {                 
563         FILTER: r'''<mediaobject><textobject><programlisting language="lilypond" role="%(options)s">%(code)s</programlisting></textobject></mediaobject>''', 
564     
565         OUTPUT: r'''
566         <imageobject role="latex">
567                 <imagedata fileref="%(base)s.pdf" format="PDF"/>
568                 </imageobject>
569                 <imageobject role="html">
570                 <imagedata fileref="%(base)s.png" format="PNG"/></imageobject>''',
571     
572         VERBATIM: r'''<programlisting>%(verb)s</programlisting>''',
573     
574         PRINTFILENAME: '<textobject><simpara><ulink url="%(base)s.ly"><filename>%(filename)s</filename></ulink></simpara></textobject>'
575     },
576     ##
577     HTML: {
578         FILTER: r'''<lilypond %(options)s>
579 %(code)s
580 </lilypond>
581 ''',
582
583         AFTER: r'''
584  </a>
585 </p>''',
586
587         BEFORE: r'''<p>
588  <a href="%(base)s.ly">''',
589
590         OUTPUT: r'''
591   <img align="center" valign="center"
592     border="0" src="%(image)s" alt="%(alt)s">''',
593
594         PRINTFILENAME: '<p><tt><a href="%(base)s.ly">%(filename)s</a></tt></p>',
595
596         QUOTE: r'''<blockquote>
597 %(str)s
598 </blockquote>
599 ''',
600
601         VERBATIM: r'''<pre>
602 %(verb)s</pre>''',
603     },
604
605     ##
606     LATEX: {
607         OUTPUT: r'''{%%
608 \parindent 0pt%%
609 \ifx\preLilyPondExample \undefined%%
610  \relax%%
611 \else%%
612  \preLilyPondExample%%
613 \fi%%
614 \def\lilypondbook{}%%
615 \input %(base)s-systems.tex%%
616 \ifx\postLilyPondExample \undefined%%
617  \relax%%
618 \else%%
619  \postLilyPondExample%%
620 \fi%%
621 }''',
622
623         PRINTFILENAME: '''\\texttt{%(filename)s}
624     ''',
625
626         QUOTE: r'''\begin{quotation}%(str)s
627 \end{quotation}''',
628
629         VERBATIM: r'''\noindent
630 \begin{verbatim}%(verb)s\end{verbatim}''',
631
632         FILTER: r'''\begin{lilypond}[%(options)s]
633 %(code)s
634 \end{lilypond}''',
635     },
636
637     ##
638     TEXINFO: {
639         FILTER: r'''@lilypond[%(options)s]
640 %(code)s
641 @lilypond''',
642
643         OUTPUT: r'''
644 @iftex
645 @include %(base)s-systems.texi
646 @end iftex
647 ''',
648
649         OUTPUTIMAGE: r'''@noindent
650 @ifinfo
651 @image{%(info_image_path)s,,,%(alt)s,%(ext)s}
652 @end ifinfo
653 @html
654 <p>
655  <a href="%(base)s.ly">
656   <img align="center" valign="center"
657     border="0" src="%(image)s" alt="%(alt)s">
658  </a>
659 </p>
660 @end html
661 ''',
662
663         PRINTFILENAME: '''
664 @html
665 <a href="%(base)s.ly">
666 @end html
667 @file{%(filename)s}
668 @html
669 </a>
670 @end html
671     ''',
672
673         QUOTE: r'''@quotation
674 %(str)s@end quotation
675 ''',
676
677         NOQUOTE: r'''@format
678 %(str)s@end format
679 ''',
680
681         VERBATIM: r'''@exampleindent 0
682 @verbatim
683 %(verb)s@end verbatim
684 ''',
685     },
686 }
687
688 #
689 # Maintain line numbers.
690 #
691
692 ## TODO
693 if 0:
694     for f in [HTML, LATEX]:
695         for s in (QUOTE, VERBATIM):
696             output[f][s] = output[f][s].replace("\n"," ")
697
698
699 PREAMBLE_LY = '''%%%% Generated by %(program_name)s
700 %%%% Options: [%(option_string)s]
701 \\include "lilypond-book-preamble.ly"
702
703
704 %% ****************************************************************
705 %% Start cut-&-pastable-section 
706 %% ****************************************************************
707
708 %(preamble_string)s
709
710 \paper {
711   #(define dump-extents #t)
712   %(font_dump_setting)s
713   %(paper_string)s
714   force-assignment = #""
715   line-width = #(- line-width (* mm  %(padding_mm)f))
716 }
717
718 \layout {
719   %(layout_string)s
720 }
721 '''
722
723 FRAGMENT_LY = r'''
724 %(notes_string)s
725 {
726
727
728 %% ****************************************************************
729 %% ly snippet contents follows:
730 %% ****************************************************************
731 %(code)s
732
733
734 %% ****************************************************************
735 %% end ly snippet
736 %% ****************************************************************
737 }
738 '''
739
740 FULL_LY = '''
741
742
743 %% ****************************************************************
744 %% ly snippet:
745 %% ****************************************************************
746 %(code)s
747
748
749 %% ****************************************************************
750 %% end ly snippet
751 %% ****************************************************************
752 '''
753
754 texinfo_line_widths = {
755     '@afourpaper': '160\\mm',
756     '@afourwide': '6.5\\in',
757     '@afourlatex': '150\\mm',
758     '@smallbook': '5\\in',
759     '@letterpaper': '6\\in',
760 }
761
762 def classic_lilypond_book_compatibility (key, value):
763     if key == 'singleline' and value == None:
764         return (RAGGED_RIGHT, None)
765
766     m = re.search ('relative\s*([-0-9])', key)
767     if m:
768         return ('relative', m.group (1))
769
770     m = re.match ('([0-9]+)pt', key)
771     if m:
772         return ('staffsize', m.group (1))
773
774     if key == 'indent' or key == 'line-width':
775         m = re.match ('([-.0-9]+)(cm|in|mm|pt|staffspace)', value)
776         if m:
777             f = float (m.group (1))
778             return (key, '%f\\%s' % (f, m.group (2)))
779
780     return (None, None)
781
782 def find_file (name, raise_error=True):
783     for i in global_options.include_path:
784         full = os.path.join (i, name)
785         if os.path.exists (full):
786             return full
787         
788     if raise_error:
789         error (_ ("file not found: %s") % name + '\n')
790         exit (1)
791     return ''
792
793 def verbatim_html (s):
794     return re.sub ('>', '&gt;',
795            re.sub ('<', '&lt;',
796                re.sub ('&', '&amp;', s)))
797
798 def split_options (option_string):
799     if option_string:
800         if global_options.format == HTML:
801             options = re.findall('[\w\.-:]+(?:\s*=\s*(?:"[^"]*"|\'[^\']*\'|\S+))?',option_string)
802             for i in range(len(options)):
803                 options[i] = re.sub('^([^=]+=\s*)(?P<q>["\'])(.*)(?P=q)','\g<1>\g<3>',options[i])
804             return options
805         else:
806             return re.split (format_res[global_options.format]['option_sep'],
807                     option_string)
808     return []
809
810 def set_default_options (source):
811     global default_ly_options
812     if not default_ly_options.has_key (LINE_WIDTH):
813         if global_options.format == LATEX:
814             textwidth = get_latex_textwidth (source)
815             default_ly_options[LINE_WIDTH] = \
816              '''%.0f\\pt''' % textwidth
817         elif global_options.format == TEXINFO:
818             for (k, v) in texinfo_line_widths.items ():
819                 # FIXME: @layout is usually not in
820                 # chunk #0:
821                 #
822                 #  \input texinfo @c -*-texinfo-*-
823                 #
824                 # Bluntly search first K items of
825                 # source.
826                 # s = chunks[0].replacement_text ()
827                 if re.search (k, source[:1024]):
828                     default_ly_options[LINE_WIDTH] = v
829                     break
830
831 class Chunk:
832     def replacement_text (self):
833         return ''
834
835     def filter_text (self):
836         return self.replacement_text ()
837
838     def ly_is_outdated (self):
839         return 0
840
841     def png_is_outdated (self):
842         return 0
843
844     def is_plain (self):
845         return False
846     
847 class Substring (Chunk):
848     def __init__ (self, source, start, end, line_number):
849         self.source = source
850         self.start = start
851         self.end = end
852         self.line_number = line_number
853         self.override_text = None
854         
855     def is_plain (self):
856         return True
857
858     def replacement_text (self):
859         if self.override_text:
860             return self.override_text
861         else:
862             return self.source[self.start:self.end]
863
864 class Snippet (Chunk):
865     def __init__ (self, type, match, format, line_number):
866         self.type = type
867         self.match = match
868         self.hash = 0
869         self.option_dict = {}
870         self.format = format
871         self.line_number = line_number
872
873     def replacement_text (self):
874         return self.match.group ('match')
875
876     def substring (self, s):
877         return self.match.group (s)
878
879     def __repr__ (self):
880         return `self.__class__` + ' type = ' + self.type
881
882 class Include_snippet (Snippet):
883     def processed_filename (self):
884         f = self.substring ('filename')
885         return os.path.splitext (f)[0] + format2ext[global_options.format]
886
887     def replacement_text (self):
888         s = self.match.group ('match')
889         f = self.substring ('filename')
890
891         return re.sub (f, self.processed_filename (), s)
892
893 class Lilypond_snippet (Snippet):
894     def __init__ (self, type, match, format, line_number):
895         Snippet.__init__ (self, type, match, format, line_number)
896         os = match.group ('options')
897         self.do_options (os, self.type)
898
899     def verb_ly (self):
900         return self.substring ('code')
901
902     def ly (self):
903         contents = self.substring ('code')
904         return ('\\sourcefileline %d\n%s'
905                 % (self.line_number - 1, contents))
906
907     def full_ly (self):
908         s = self.ly ()
909         if s:
910             return self.compose_ly (s)
911         return ''
912
913     def do_options (self, option_string, type):
914         self.option_dict = {}
915
916         options = split_options (option_string)
917
918         for i in options:
919             if '=' in i:
920                 (key, value) = re.split ('\s*=\s*', i)
921                 self.option_dict[key] = value
922             else:
923                 if i in no_options.keys ():
924                     if no_options[i] in self.option_dict.keys ():
925                         del self.option_dict[no_options[i]]
926                 else:
927                     self.option_dict[i] = None
928
929         has_line_width = self.option_dict.has_key (LINE_WIDTH)
930         no_line_width_value = 0
931
932         # If LINE_WIDTH is used without parameter, set it to default.
933         if has_line_width and self.option_dict[LINE_WIDTH] == None:
934             no_line_width_value = 1
935             del self.option_dict[LINE_WIDTH]
936
937         for i in default_ly_options.keys ():
938             if i not in self.option_dict.keys ():
939                 self.option_dict[i] = default_ly_options[i]
940
941         if not has_line_width:
942             if type == 'lilypond' or FRAGMENT in self.option_dict.keys ():
943                 self.option_dict[RAGGED_RIGHT] = None
944
945             if type == 'lilypond':
946                 if LINE_WIDTH in self.option_dict.keys ():
947                     del self.option_dict[LINE_WIDTH]
948             else:
949                 if RAGGED_RIGHT in self.option_dict.keys ():
950                     if LINE_WIDTH in self.option_dict.keys ():
951                         del self.option_dict[LINE_WIDTH]
952
953             if QUOTE in self.option_dict.keys () or type == 'lilypond':
954                 if LINE_WIDTH in self.option_dict.keys ():
955                     del self.option_dict[LINE_WIDTH]
956
957         if not INDENT in self.option_dict.keys ():
958             self.option_dict[INDENT] = '0\\mm'
959
960         # The QUOTE pattern from ly_options only emits the `line-width'
961         # keyword.
962         if has_line_width and QUOTE in self.option_dict.keys ():
963             if no_line_width_value:
964                 del self.option_dict[LINE_WIDTH]
965             else:
966                 del self.option_dict[QUOTE]
967
968     def compose_ly (self, code):
969         if FRAGMENT in self.option_dict.keys ():
970             body = FRAGMENT_LY
971         else:
972             body = FULL_LY
973
974         # Defaults.
975         relative = 1
976         override = {}
977         # The original concept of the `exampleindent' option is broken.
978         # It is not possible to get a sane value for @exampleindent at all
979         # without processing the document itself.  Saying
980         #
981         #   @exampleindent 0
982         #   @example
983         #   ...
984         #   @end example
985         #   @exampleindent 5
986         #
987         # causes ugly results with the DVI backend of texinfo since the
988         # default value for @exampleindent isn't 5em but 0.4in (or a smaller
989         # value).  Executing the above code changes the environment
990         # indentation to an unknown value because we don't know the amount
991         # of 1em in advance since it is font-dependent.  Modifying
992         # @exampleindent in the middle of a document is simply not
993         # supported within texinfo.
994         #
995         # As a consequence, the only function of @exampleindent is now to
996         # specify the amount of indentation for the `quote' option.
997         #
998         # To set @exampleindent locally to zero, we use the @format
999         # environment for non-quoted snippets.
1000         override[EXAMPLEINDENT] = r'0.4\in'
1001         override[LINE_WIDTH] = texinfo_line_widths['@smallbook']
1002         override.update (default_ly_options)
1003
1004         option_list = []
1005         for (key, value) in self.option_dict.items ():
1006             if value == None:
1007                 option_list.append (key)
1008             else:
1009                 option_list.append (key + '=' + value)
1010         option_string = ','.join (option_list)
1011
1012         compose_dict = {}
1013         compose_types = [NOTES, PREAMBLE, LAYOUT, PAPER]
1014         for a in compose_types:
1015             compose_dict[a] = []
1016
1017         for (key, value) in self.option_dict.items ():
1018             (c_key, c_value) = \
1019              classic_lilypond_book_compatibility (key, value)
1020             if c_key:
1021                 if c_value:
1022                     warning \
1023                      (_ ("deprecated ly-option used: %s=%s" \
1024                       % (key, value)))
1025                     warning \
1026                      (_ ("compatibility mode translation: %s=%s" \
1027                       % (c_key, c_value)))
1028                 else:
1029                     warning \
1030                      (_ ("deprecated ly-option used: %s" \
1031                       % key))
1032                     warning \
1033                      (_ ("compatibility mode translation: %s" \
1034                       % c_key))
1035
1036                 (key, value) = (c_key, c_value)
1037
1038             if value:
1039                 override[key] = value
1040             else:
1041                 if not override.has_key (key):
1042                     override[key] = None
1043
1044             found = 0
1045             for type in compose_types:
1046                 if ly_options[type].has_key (key):
1047                     compose_dict[type].append (ly_options[type][key])
1048                     found = 1
1049                     break
1050
1051             if not found and key not in simple_options:
1052                 warning (_ ("ignoring unknown ly option: %s") % key)
1053
1054         # URGS
1055         if RELATIVE in override.keys () and override[RELATIVE]:
1056             relative = int (override[RELATIVE])
1057
1058         relative_quotes = ''
1059
1060         # 1 = central C
1061         if relative < 0:
1062             relative_quotes += ',' * (- relative)
1063         elif relative > 0:
1064             relative_quotes += "'" * relative
1065
1066         paper_string = '\n  '.join (compose_dict[PAPER]) % override
1067         layout_string = '\n  '.join (compose_dict[LAYOUT]) % override
1068         notes_string = '\n  '.join (compose_dict[NOTES]) % vars ()
1069         preamble_string = '\n  '.join (compose_dict[PREAMBLE]) % override
1070         padding_mm = global_options.padding_mm
1071         font_dump_setting = ''
1072         if FONTLOAD in self.option_dict:
1073             font_dump_setting = '#(define-public force-eps-font-include #t)\n'
1074
1075         d = globals().copy()
1076         d.update (locals())
1077         return (PREAMBLE_LY + body) % d
1078
1079     def get_hash (self):
1080         if not self.hash:
1081             hash = md5.md5 (self.relevant_contents (self.full_ly ()))
1082
1083             ## let's not create too long names.
1084             self.hash = hash.hexdigest ()[:10]
1085             
1086         return self.hash
1087
1088     def basename (self):
1089         if FILENAME in self.option_dict:
1090             return self.option_dict[FILENAME]
1091         if global_options.use_hash:
1092             return 'lily-%s' % self.get_hash ()
1093         raise 'to be done'
1094
1095     def write_ly (self):
1096         outf = open (self.basename () + '.ly', 'w')
1097         outf.write (self.full_ly ())
1098         open (self.basename () + '.txt', 'w').write ('image of music')
1099
1100     def relevant_contents (self, ly):
1101         return re.sub (r'\\(version|sourcefileline|sourcefilename)[^\n]*\n', '', ly)
1102              
1103     def ly_is_outdated (self):
1104         base = self.basename ()
1105         ly_file = find_file (base + '.ly', raise_error=False)
1106         tex_file = find_file (base + '.tex', raise_error=False)
1107         systems_file = find_file (base + '-systems.tex', raise_error=False)
1108
1109         if (os.path.exists (ly_file)
1110             and os.path.exists (systems_file)
1111             and os.stat (systems_file)[stat.ST_SIZE]
1112             and re.match ('% eof', open (systems_file).readlines ()[-1])
1113             and (global_options.use_hash or FILENAME in self.option_dict)
1114             and (self.relevant_contents (self.full_ly ())
1115                  == self.relevant_contents (open (ly_file).read ()))):
1116             return None
1117
1118         return self
1119
1120     def png_is_outdated (self):
1121         base = self.basename ()
1122         eps_file = find_file (base + '.eps', raise_error=False)
1123         png_file = find_file (base + '.png', raise_error=False)
1124         if not self.ly_is_outdated () and global_options.format in (HTML, TEXINFO):
1125             if os.path.exists (eps_file):
1126                 page_count = ps_page_count (eps_file)
1127                 if page_count <= 1:
1128                     return not os.path.exists (png_file)
1129                 else:
1130                     return not reduce (operator.or_,
1131                                        [find_file (base + '-page%d.png' % a, raise_error=False)
1132                                         for a in range (1, page_count + 1)])
1133         return True
1134     
1135     def texstr_is_outdated (self):
1136         if backend == 'ps':
1137             return 0
1138
1139         base = self.basename ()
1140         return not (self.ly_is_outdated ()
1141                     and find_file (base + '.texstr', raise_error=False))
1142
1143     def filter_text (self):
1144         code = self.substring ('code')
1145         s = run_filter (code)
1146         d = {
1147             'code': s,
1148             'options': self.match.group ('options')
1149         }
1150         # TODO
1151         return output[self.format][FILTER] % d
1152
1153     def replacement_text (self):
1154         func = Lilypond_snippet.__dict__['output_' + self.format]
1155         return func (self)
1156
1157     def get_images (self):
1158         base = self.basename ()
1159         # URGUGHUGHUGUGH
1160         single = '%(base)s.png' % vars ()
1161         multiple = '%(base)s-page1.png' % vars ()
1162         images = (single,)
1163         if os.path.exists (multiple) \
1164          and (not os.path.exists (single) \
1165             or (os.stat (multiple)[stat.ST_MTIME] \
1166               > os.stat (single)[stat.ST_MTIME])):
1167             count = ps_page_count ('%(base)s.eps' % vars ())
1168             images = ['%s-page%d.png' % (base, a) for a in range (1, count+1)]
1169             images = tuple (images)
1170         return images
1171
1172     def output_docbook (self):
1173         str = ''
1174         base = self.basename ()
1175         for image in self.get_images ():
1176             (base, ext) = os.path.splitext (image)
1177             str += output[DOCBOOK][OUTPUT] % vars ()
1178             str += self.output_print_filename (DOCBOOK)
1179             if (self.substring('inline') == 'inline'): 
1180                 str = '<inlinemediaobject>' + str + '</inlinemediaobject>'
1181             else:
1182                 str = '<mediaobject>' + str + '</mediaobject>'
1183         if VERBATIM in self.option_dict:
1184                 verb = verbatim_html (self.verb_ly ())
1185                 str = output[DOCBOOK][VERBATIM] % vars () + str
1186         return str
1187         
1188     def output_html (self):
1189         str = ''
1190         base = self.basename ()
1191         if global_options.format == HTML:
1192             str += self.output_print_filename (HTML)
1193             if VERBATIM in self.option_dict:
1194                 verb = verbatim_html (self.verb_ly ())
1195                 str += output[HTML][VERBATIM] % vars ()
1196             if QUOTE in self.option_dict:
1197                 str = output[HTML][QUOTE] % vars ()
1198
1199         str += output[HTML][BEFORE] % vars ()
1200         for image in self.get_images ():
1201             (base, ext) = os.path.splitext (image)
1202             alt = self.option_dict[ALT]
1203             str += output[HTML][OUTPUT] % vars ()
1204         str += output[HTML][AFTER] % vars ()
1205         return str
1206
1207     def output_info (self):
1208         str = ''
1209         for image in self.get_images ():
1210             (base, ext) = os.path.splitext (image)
1211
1212             # URG, makeinfo implicitly prepends dot to extension.
1213             # Specifying no extension is most robust.
1214             ext = ''
1215             alt = self.option_dict[ALT]
1216             info_image_path = os.path.join (global_options.info_images_dir, base)
1217             str += output[TEXINFO][OUTPUTIMAGE] % vars ()
1218
1219         base = self.basename ()
1220         str += output[global_options.format][OUTPUT] % vars ()
1221         return str
1222
1223     def output_latex (self):
1224         str = ''
1225         base = self.basename ()
1226         if global_options.format == LATEX:
1227             str += self.output_print_filename (LATEX)
1228             if VERBATIM in self.option_dict:
1229                 verb = self.verb_ly ()
1230                 str += (output[LATEX][VERBATIM] % vars ())
1231
1232         str += (output[LATEX][OUTPUT] % vars ())
1233
1234         ## todo: maintain breaks
1235         if 0:
1236             breaks = self.ly ().count ("\n")
1237             str += "".ljust (breaks, "\n").replace ("\n","%\n")
1238         
1239         if QUOTE in self.option_dict:
1240             str = output[LATEX][QUOTE] % vars ()
1241         return str
1242
1243     def output_print_filename (self, format):
1244         str = ''
1245         if PRINTFILENAME in self.option_dict:
1246             base = self.basename ()
1247             filename = os.path.basename (self.substring ('filename'))
1248             str = output[format][PRINTFILENAME] % vars ()
1249
1250         return str
1251
1252     def output_texinfo (self):
1253         str = self.output_print_filename (TEXINFO)
1254         base = self.basename ()
1255         if TEXIDOC in self.option_dict:
1256             texidoc = base + '.texidoc'
1257             if os.path.exists (texidoc):
1258                 str += '@include %(texidoc)s\n\n' % vars ()
1259
1260         substr = ''
1261         if VERBATIM in self.option_dict:
1262             verb = self.verb_ly ()
1263             substr += output[TEXINFO][VERBATIM] % vars ()
1264             if not QUOTE in self.option_dict:
1265                 substr = output[TEXINFO][NOQUOTE] % {'str':substr}
1266         substr += self.output_info ()
1267         if LILYQUOTE in self.option_dict:
1268             substr = output[TEXINFO][QUOTE] % {'str':substr}
1269         str += substr
1270
1271 #                str += ('@ifinfo\n' + self.output_info () + '\n@end ifinfo\n')
1272 #                str += ('@tex\n' + self.output_latex () + '\n@end tex\n')
1273 #                str += ('@html\n' + self.output_html () + '\n@end html\n')
1274
1275         if QUOTE in self.option_dict:
1276             str = output[TEXINFO][QUOTE] % vars ()
1277
1278         # need par after image
1279         str += '\n'
1280
1281         return str
1282
1283 re_begin_verbatim = re.compile (r'\s+%.*?begin verbatim.*\n*', re.M)
1284 re_end_verbatim = re.compile (r'\s+%.*?end verbatim.*$', re.M)
1285
1286 class Lilypond_file_snippet (Lilypond_snippet):
1287     def __init__ (self, type, match, format, line_number):
1288         Lilypond_snippet.__init__ (self, type, match, format, line_number)
1289         self.contents = open (find_file (self.substring ('filename'))).read ()
1290
1291     def verb_ly (self):
1292         s = self.contents
1293         s = re_begin_verbatim.split (s)[-1]
1294         s = re_end_verbatim.split (s)[0]
1295         return s
1296
1297     def ly (self):
1298         name = self.substring ('filename')
1299         return ('\\sourcefilename \"%s\"\n\\sourcefileline 0\n%s'
1300                 % (name, self.contents))
1301
1302
1303 snippet_type_to_class = {
1304     'lilypond_file': Lilypond_file_snippet,
1305     'lilypond_block': Lilypond_snippet,
1306     'lilypond': Lilypond_snippet,
1307     'include': Include_snippet,
1308 }
1309
1310 def find_linestarts (s):
1311     nls = [0]
1312     start = 0
1313     end = len (s)
1314     while 1:
1315         i = s.find ('\n', start)
1316         if i < 0:
1317             break
1318
1319         i = i + 1
1320         nls.append (i)
1321         start = i
1322
1323     nls.append (len (s))
1324     return nls
1325
1326 def find_toplevel_snippets (s, types):
1327     res = {}
1328     for i in types:
1329         res[i] = ly.re.compile (snippet_res[global_options.format][i])
1330
1331     snippets = []
1332     index = 0
1333     found = dict ([(t, None) for t in types])
1334
1335     line_starts = find_linestarts (s)
1336     line_start_idx = 0
1337     # We want to search for multiple regexes, without searching
1338     # the string multiple times for one regex.
1339     # Hence, we use earlier results to limit the string portion
1340     # where we search.
1341     # Since every part of the string is traversed at most once for
1342     # every type of snippet, this is linear.
1343
1344     while 1:
1345         first = None
1346         endex = 1 << 30
1347         for type in types:
1348             if not found[type] or found[type][0] < index:
1349                 found[type] = None
1350                 
1351                 m = res[type].search (s[index:endex])
1352                 if not m:
1353                     continue
1354
1355                 cl = Snippet
1356                 if snippet_type_to_class.has_key (type):
1357                     cl = snippet_type_to_class[type]
1358
1359
1360                 start = index + m.start ('match')
1361                 line_number = line_start_idx
1362                 while (line_starts[line_number] < start):
1363                     line_number += 1
1364
1365                 line_number += 1
1366                 snip = cl (type, m, global_options.format, line_number)
1367
1368                 found[type] = (start, snip)
1369
1370             if found[type] \
1371              and (not first \
1372                 or found[type][0] < found[first][0]):
1373                 first = type
1374
1375                 # FIXME.
1376
1377                 # Limiting the search space is a cute
1378                 # idea, but this *requires* to search
1379                 # for possible containing blocks
1380                 # first, at least as long as we do not
1381                 # search for the start of blocks, but
1382                 # always/directly for the entire
1383                 # @block ... @end block.
1384
1385                 endex = found[first][0]
1386
1387         if not first:
1388             snippets.append (Substring (s, index, len (s), line_start_idx))
1389             break
1390
1391         while (start > line_starts[line_start_idx+1]):
1392             line_start_idx += 1
1393
1394         (start, snip) = found[first]
1395         snippets.append (Substring (s, index, start, line_start_idx + 1))
1396         snippets.append (snip)
1397         found[first] = None
1398         index = start + len (snip.match.group ('match'))
1399
1400     return snippets
1401
1402 def filter_pipe (input, cmd):
1403     if global_options.verbose:
1404         progress (_ ("Opening filter `%s'") % cmd)
1405
1406     (stdin, stdout, stderr) = os.popen3 (cmd)
1407     stdin.write (input)
1408     status = stdin.close ()
1409
1410     if not status:
1411         status = 0
1412         output = stdout.read ()
1413         status = stdout.close ()
1414         error = stderr.read ()
1415
1416     if not status:
1417         status = 0
1418     signal = 0x0f & status
1419     if status or (not output and error):
1420         exit_status = status >> 8
1421         error (_ ("`%s' failed (%d)") % (cmd, exit_status))
1422         error (_ ("The error log is as follows:"))
1423         ly.stderr_write (error)
1424         ly.stderr_write (stderr.read ())
1425         exit (status)
1426
1427     if global_options.verbose:
1428         progress ('\n')
1429
1430     return output
1431
1432 def run_filter (s):
1433     return filter_pipe (s, global_options.filter_cmd)
1434
1435 def is_derived_class (cl, baseclass):
1436     if cl == baseclass:
1437         return 1
1438     for b in cl.__bases__:
1439         if is_derived_class (b, baseclass):
1440             return 1
1441     return 0
1442
1443 def process_snippets (cmd, ly_snippets, texstr_snippets, png_snippets):
1444     ly_names = filter (lambda x: x,
1445                        map (Lilypond_snippet.basename, ly_snippets))
1446     texstr_names = filter (lambda x: x,
1447                            map (Lilypond_snippet.basename, texstr_snippets))
1448     
1449     png_names = filter (lambda x: x,
1450                         map (Lilypond_snippet.basename, png_snippets))
1451
1452     status = 0
1453     def my_system (cmd):
1454         status = ly.system (cmd,
1455                             be_verbose=global_options.verbose, 
1456                             progress_p=1)
1457
1458     if global_options.format in (HTML, TEXINFO) and '--formats' not in cmd:
1459         cmd += ' --formats=png '
1460     elif global_options.format in (DOCBOOK) and '--formats' not in cmd:
1461         cmd += ' --formats=png,pdf '
1462
1463         
1464     # UGH
1465     # the --process=CMD switch is a bad idea
1466     # it is too generic for lilypond-book.
1467     if texstr_names:
1468         my_system (' '.join ([cmd, '--backend texstr',
1469                               'snippet-map.ly'] + texstr_names))
1470         for l in texstr_names:
1471             my_system ('latex %s.texstr' % l)
1472
1473     if ly_names:
1474         open ('snippet-names', 'wb').write ('\n'.join (['snippet-map.ly']
1475                                                        + ly_names))
1476         
1477         my_system (' '.join ([cmd, 'snippet-names']))
1478
1479
1480 LATEX_INSPECTION_DOCUMENT = r'''
1481 \nonstopmode
1482 %(preamble)s
1483 \begin{document}
1484 \typeout{textwidth=\the\textwidth}
1485 \typeout{columnsep=\the\columnsep}
1486 \makeatletter\if@twocolumn\typeout{columns=2}\fi\makeatother
1487 \end{document}
1488 '''
1489
1490 # Do we need anything else besides `textwidth'?
1491 def get_latex_textwidth (source):
1492     m = re.search (r'''(?P<preamble>\\begin\s*{document})''', source)
1493     if m == None:
1494         warning (_ ("cannot find \\begin{document} in LaTeX document"))
1495         
1496         ## what's a sensible default?
1497         return 550.0
1498     
1499     preamble = source[:m.start (0)]
1500     latex_document = LATEX_INSPECTION_DOCUMENT % vars ()
1501     
1502     (handle, tmpfile) = tempfile.mkstemp('.tex')
1503     logfile = os.path.splitext (tmpfile)[0] + '.log'
1504     logfile = os.path.split (logfile)[1]
1505
1506     tmp_handle = os.fdopen (handle,'w')
1507     tmp_handle.write (latex_document)
1508     tmp_handle.close ()
1509     
1510     ly.system ('latex %s' % tmpfile, be_verbose=global_options.verbose)
1511     parameter_string = open (logfile).read()
1512     
1513     os.unlink (tmpfile)
1514     os.unlink (logfile)
1515
1516     columns = 0
1517     m = re.search ('columns=([0-9.]*)', parameter_string)
1518     if m:
1519         columns = int (m.group (1))
1520
1521     columnsep = 0
1522     m = re.search ('columnsep=([0-9.]*)pt', parameter_string)
1523     if m:
1524         columnsep = float (m.group (1))
1525
1526     textwidth = 0
1527     m = re.search ('textwidth=([0-9.]*)pt', parameter_string)
1528     if m:
1529         textwidth = float (m.group (1))
1530         if columns:
1531             textwidth = (textwidth - columnsep) / columns
1532
1533     return textwidth
1534
1535 def modify_preamble (chunk):
1536     str = chunk.replacement_text ()
1537     if (re.search (r"\\begin *{document}", str)
1538       and not re.search ("{graphic[sx]", str)):
1539         str = re.sub (r"\\begin{document}",
1540                r"\\usepackage{graphics}" + '\n'
1541                + r"\\begin{document}",
1542                str)
1543         chunk.override_text = str 
1544         
1545     
1546
1547 ext2format = {
1548     '.html': HTML,
1549     '.itely': TEXINFO,
1550     '.latex': LATEX,
1551     '.lytex': LATEX,
1552     '.tely': TEXINFO,
1553     '.tex': LATEX,
1554     '.texi': TEXINFO,
1555     '.texinfo': TEXINFO,
1556     '.xml': HTML,
1557     '.lyxml': DOCBOOK
1558 }
1559
1560 format2ext = {
1561     HTML: '.html',
1562     # TEXINFO: '.texinfo',
1563     TEXINFO: '.texi',
1564     LATEX: '.tex',
1565     DOCBOOK: '.xml'
1566 }
1567
1568 class Compile_error:
1569     pass
1570
1571 def write_file_map (lys, name):
1572     snippet_map = open ('snippet-map.ly', 'w')
1573     snippet_map.write ("""
1574 #(define version-seen #t)
1575 #(define output-empty-score-list #f)
1576 #(ly:add-file-name-alist '(
1577 """)
1578     for ly in lys:
1579         snippet_map.write ('("%s.ly" . "%s")\n'
1580                  % (ly.basename (),
1581                    name))
1582
1583     snippet_map.write ('))\n')
1584
1585 def do_process_cmd (chunks, input_name):
1586     all_lys = filter (lambda x: is_derived_class (x.__class__,
1587                            Lilypond_snippet),
1588                       chunks)
1589
1590     write_file_map (all_lys, input_name)
1591     ly_outdated = filter (lambda x: is_derived_class (x.__class__,
1592                                                       Lilypond_snippet)
1593                           and x.ly_is_outdated (), chunks)
1594     texstr_outdated = filter (lambda x: is_derived_class (x.__class__,
1595                                                           Lilypond_snippet)
1596                               and x.texstr_is_outdated (),
1597                               chunks)
1598     png_outdated = filter (lambda x: is_derived_class (x.__class__,
1599                                                         Lilypond_snippet)
1600                            and x.png_is_outdated (),
1601                            chunks)
1602
1603     outdated = png_outdated + texstr_outdated + ly_outdated
1604     
1605     progress (_ ("Writing snippets..."))
1606     map (Lilypond_snippet.write_ly, ly_outdated)
1607     progress ('\n')
1608
1609     if outdated:
1610         progress (_ ("Processing..."))
1611         progress ('\n')
1612         process_snippets (global_options.process_cmd, ly_outdated, texstr_outdated, png_outdated)
1613     else:
1614         progress (_ ("All snippets are up to date..."))
1615     progress ('\n')
1616
1617 def guess_format (input_filename):
1618     format = None
1619     e = os.path.splitext (input_filename)[1]
1620     if e in ext2format.keys ():
1621         # FIXME
1622         format = ext2format[e]
1623     else:
1624         error (_ ("cannot determine format for: %s" \
1625               % input_filename))
1626         exit (1)
1627     return format
1628
1629 def write_if_updated (file_name, lines):
1630     try:
1631         f = open (file_name)
1632         oldstr = f.read ()
1633         new_str = ''.join (lines)
1634         if oldstr == new_str:
1635             progress (_ ("%s is up to date.") % file_name)
1636             progress ('\n')
1637
1638             # this prevents make from always rerunning lilypond-book:
1639             # .texi target must be touched in order to be up to date
1640             if global_options.format == 'texinfo':
1641                 os.utime (file_name, None)
1642             return
1643     except:
1644         pass
1645
1646     progress (_ ("Writing `%s'...") % file_name)
1647     open (file_name, 'w').writelines (lines)
1648     progress ('\n')
1649
1650 def note_input_file (name, inputs=[]):
1651     ## hack: inputs is mutable!
1652     inputs.append (name)
1653     return inputs
1654
1655 def samefile (f1, f2):
1656     try:
1657         return os.path.samefile (f1, f2)
1658     except AttributeError:                # Windoze
1659         f1 = re.sub ("//*", "/", f1)
1660         f2 = re.sub ("//*", "/", f2)
1661         return f1 == f2
1662
1663 def do_file (input_filename):
1664     # Ugh.
1665     if not input_filename or input_filename == '-':
1666         in_handle = sys.stdin
1667         input_fullname = '<stdin>'
1668     else:
1669         if os.path.exists (input_filename):
1670             input_fullname = input_filename
1671         elif global_options.format == LATEX and ly.search_exe_path ('kpsewhich'):
1672             input_fullname = os.popen ('kpsewhich ' + input_filename).read()[:-1]
1673         else:
1674             input_fullname = find_file (input_filename)
1675
1676         note_input_file (input_fullname)
1677         in_handle = open (input_fullname)
1678
1679     if input_filename == '-':
1680         input_base = 'stdin'
1681     else:
1682         input_base = os.path.basename \
1683                      (os.path.splitext (input_filename)[0])
1684
1685     # Only default to stdout when filtering.
1686     if global_options.output_name == '-' or (not global_options.output_name and global_options.filter_cmd):
1687         output_filename = '-'
1688         output_file = sys.stdout
1689     else:
1690         # don't complain when global_options.output_name is existing
1691         output_filename = input_base + format2ext[global_options.format]
1692         if global_options.output_name:
1693             if not os.path.isdir (global_options.output_name):
1694                 os.mkdir (global_options.output_name, 0777)
1695             os.chdir (global_options.output_name)
1696         else: 
1697             if (os.path.exists (input_filename) 
1698                 and os.path.exists (output_filename) 
1699                 and samefile (output_filename, input_fullname)):
1700              error (
1701              _ ("Output would overwrite input file; use --output."))
1702              exit (2)
1703
1704     try:
1705         progress (_ ("Reading %s...") % input_fullname)
1706         source = in_handle.read ()
1707         progress ('\n')
1708
1709         set_default_options (source)
1710
1711
1712         # FIXME: Containing blocks must be first, see
1713         #        find_toplevel_snippets.
1714         snippet_types = (
1715             'multiline_comment',
1716             'verbatim',
1717             'lilypond_block',
1718     #                'verb',
1719             'singleline_comment',
1720             'lilypond_file',
1721             'include',
1722             'lilypond',
1723         )
1724         progress (_ ("Dissecting..."))
1725         chunks = find_toplevel_snippets (source, snippet_types)
1726
1727         if global_options.format == LATEX:
1728             for c in chunks:
1729                 if (c.is_plain () and
1730                   re.search (r"\\begin *{document}", c.replacement_text())):
1731                     modify_preamble (c)
1732                     break
1733         progress ('\n')
1734
1735         if global_options.filter_cmd:
1736             write_if_updated (output_filename,
1737                      [c.filter_text () for c in chunks])
1738         elif global_options.process_cmd:
1739             do_process_cmd (chunks, input_fullname)
1740             progress (_ ("Compiling %s...") % output_filename)
1741             progress ('\n')
1742             write_if_updated (output_filename,
1743                      [s.replacement_text ()
1744                      for s in chunks])
1745         
1746         def process_include (snippet):
1747             os.chdir (original_dir)
1748             name = snippet.substring ('filename')
1749             progress (_ ("Processing include: %s") % name)
1750             progress ('\n')
1751             return do_file (name)
1752
1753         include_chunks = map (process_include,
1754                    filter (lambda x: is_derived_class (x.__class__,
1755                                      Include_snippet),
1756                        chunks))
1757
1758
1759         return chunks + reduce (lambda x,y: x + y, include_chunks, [])
1760         
1761     except Compile_error:
1762         os.chdir (original_dir)
1763         progress (_ ("Removing `%s'") % output_filename)
1764         progress ('\n')
1765         raise Compile_error
1766
1767 def do_options ():
1768
1769     global global_options
1770
1771     opt_parser = get_option_parser()
1772     (global_options, args) = opt_parser.parse_args ()
1773
1774     if global_options.format in ('texi-html', 'texi'):
1775         global_options.format = TEXINFO
1776     global_options.use_hash = True
1777
1778     global_options.include_path =  map (os.path.abspath, global_options.include_path)
1779     
1780     if global_options.warranty:
1781         warranty ()
1782         exit (0)
1783     if not args or len (args) > 1:
1784         opt_parser.print_help ()
1785         exit (2)
1786         
1787     return args
1788
1789 def psfonts_warning (options, basename):
1790     if options.format in (TEXINFO, LATEX):
1791         psfonts_file = os.path.join (options.output_name, basename + '.psfonts')
1792         output = os.path.join (options.output_name, basename +  '.dvi' )
1793
1794         if not options.create_pdf:
1795             if not options.psfonts:
1796                 warning (_ ("option --psfonts not used"))
1797                 warning (_ ("processing with dvips will have no fonts"))
1798             else:
1799                 progress ('\n')
1800                 progress (_ ("DVIPS usage:"))
1801                 progress ('\n')
1802                 progress ("    dvips -h %(psfonts_file)s %(output)s" % vars ())
1803                 progress ('\n')
1804
1805 def main ():
1806     # FIXME: 85 lines of `main' macramee??
1807     files = do_options ()
1808
1809     file = files[0]
1810
1811     basename = os.path.splitext (file)[0]
1812     basename = os.path.split (basename)[1]
1813     
1814     if not global_options.format:
1815         global_options.format = guess_format (files[0])
1816
1817     formats = 'ps'
1818     if global_options.format in (TEXINFO, HTML, DOCBOOK):
1819         formats += ',png'
1820
1821         
1822     if global_options.process_cmd == '':
1823         global_options.process_cmd = (lilypond_binary 
1824                                       + ' --formats=%s --backend eps ' % formats)
1825
1826     if global_options.process_cmd:
1827         global_options.process_cmd += ' '.join ([(' -I %s' % ly.mkarg (p))
1828                               for p in global_options.include_path])
1829
1830     if global_options.format in (TEXINFO, LATEX):
1831         ## prevent PDF from being switched on by default.
1832         global_options.process_cmd += ' --formats=eps '
1833         if global_options.create_pdf:
1834             global_options.process_cmd += "--pdf -dinclude-eps-fonts -dgs-load-fonts "
1835     
1836     if global_options.verbose:
1837         global_options.process_cmd += " --verbose "
1838
1839     if global_options.padding_mm:
1840         global_options.process_cmd += " -deps-box-padding=%f " % global_options.padding_mm
1841         
1842     global_options.process_cmd += " -dread-file-list "
1843
1844     identify ()
1845
1846     try:
1847         chunks = do_file (file)
1848         if global_options.psfonts:
1849             fontextract.verbose = global_options.verbose
1850             snippet_chunks = filter (lambda x: is_derived_class (x.__class__,
1851                                        Lilypond_snippet),
1852                         chunks)
1853
1854             psfonts_file = basename + '.psfonts' 
1855             if not global_options.verbose:
1856                 progress (_ ("Writing fonts to %s...") % psfonts_file)
1857             fontextract.extract_fonts (psfonts_file,
1858                          [x.basename() + '.eps'
1859                           for x in snippet_chunks])
1860             if not global_options.verbose:
1861                 progress ('\n')
1862             
1863     except Compile_error:
1864         exit (1)
1865
1866     psfonts_warning (global_options, basename)
1867
1868     inputs = note_input_file ('')
1869     inputs.pop ()
1870
1871     base_file_name = os.path.splitext (os.path.basename (file))[0]
1872     dep_file = os.path.join (global_options.output_name, base_file_name + '.dep')
1873     final_output_file = os.path.join (global_options.output_name,
1874                      base_file_name
1875                      + '.%s' % global_options.format)
1876     
1877     os.chdir (original_dir)
1878     open (dep_file, 'w').write ('%s: %s' % (final_output_file, ' '.join (inputs)))
1879
1880 if __name__ == '__main__':
1881     main ()