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