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