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