1 # -*- coding: utf-8 -*-
3 import book_base
as BookBase
9 # TODO: We are using os.popen3, which has been deprecated since python 2.6. The
10 # suggested replacement is the Popen function of the subprocess module.
11 # Unfortunately, on windows this needs the msvcrt module, which doesn't seem
12 # to be available in GUB?!?!?!
13 # from subprocess import Popen, PIPE
15 progress
= ly
.progress
23 ####################################################################
24 # Snippet option handling
25 ####################################################################
29 # Is this pythonic? Personally, I find this rather #define-nesque. --hwn
32 ADDVERSION
= 'addversion'
37 EXAMPLEINDENT
= 'exampleindent'
41 LANG
= 'lang' ## TODO: This is handled nowhere!
43 LILYQUOTE
= 'lilyquote'
44 LINE_WIDTH
= 'line-width'
45 NOFRAGMENT
= 'nofragment'
46 NOGETTEXT
= 'nogettext'
50 NORAGGED_RIGHT
= 'noragged-right'
54 OUTPUTIMAGE
= 'outputimage'
56 PAPERSIZE
= 'papersize'
58 PRINTFILENAME
= 'printfilename'
60 RAGGED_RIGHT
= 'ragged-right'
62 STAFFSIZE
= 'staffsize'
65 VERSION
= 'lilypondversion'
69 # NOTIME and NOGETTEXT have no opposite so they aren't part of this
71 # NOQUOTE is used internally only.
77 # Options that have no impact on processing by lilypond (or --process
79 PROCESSING_INDEPENDENT_OPTIONS
= (
80 ALT
, NOGETTEXT
, VERBATIM
, ADDVERSION
,
81 TEXIDOC
, DOCTITLE
, VERSION
, PRINTFILENAME
)
85 # Options without a pattern in snippet_options.
104 ####################################################################
105 # LilyPond templates for the snippets
106 ####################################################################
111 RELATIVE
: r
'''\relative c%(relative_quotes)s''',
116 PAPERSIZE
: r
'''#(set-paper-size "%(papersize)s")''',
117 INDENT
: r
'''indent = %(indent)s''',
118 LINE_WIDTH
: r
'''line-width = %(line-width)s''',
119 QUOTE
: r
'''line-width = %(line-width)s - 2.0 * %(exampleindent)s''',
120 LILYQUOTE
: r
'''line-width = %(line-width)s - 2.0 * %(exampleindent)s''',
121 RAGGED_RIGHT
: r
'''ragged-right = ##t''',
122 NORAGGED_RIGHT
: r
'''ragged-right = ##f''',
134 \remove "Time_signature_engraver"
140 STAFFSIZE
: r
'''#(set-global-staff-size %(staffsize)s)''',
153 %% ****************************************************************
154 %% ly snippet contents follows:
155 %% ****************************************************************
159 %% ****************************************************************
161 %% ****************************************************************
165 def classic_lilypond_book_compatibility (key
, value
):
166 if key
== 'singleline' and value
== None:
167 return (RAGGED_RIGHT
, None)
169 m
= re
.search ('relative\s*([-0-9])', key
)
171 return ('relative', m
.group (1))
173 m
= re
.match ('([0-9]+)pt', key
)
175 return ('staffsize', m
.group (1))
177 if key
== 'indent' or key
== 'line-width':
178 m
= re
.match ('([-.0-9]+)(cm|in|mm|pt|staffspace)', value
)
180 f
= float (m
.group (1))
181 return (key
, '%f\\%s' % (f
, m
.group (2)))
186 PREAMBLE_LY
= '''%%%% Generated by %(program_name)s
187 %%%% Options: [%(option_string)s]
188 \\include "lilypond-book-preamble.ly"
191 %% ****************************************************************
192 %% Start cut-&-pastable-section
193 %% ****************************************************************
199 force-assignment = #""
200 line-width = #(- line-width (* mm %(padding_mm)f))
214 %% ****************************************************************
216 %% ****************************************************************
220 %% ****************************************************************
222 %% ****************************************************************
232 ####################################################################
234 ####################################################################
236 def ps_page_count (ps_name
):
237 header
= file (ps_name
).read (1024)
238 m
= re
.search ('\n%%Pages: ([0-9]+)', header
)
240 return int (m
.group (1))
243 ly_var_def_re
= re
.compile (r
'^([a-zA-Z]+)[\t ]*=', re
.M
)
244 ly_comment_re
= re
.compile (r
'(%+[\t ]*)(.*)$', re
.M
)
245 ly_context_id_re
= re
.compile ('\\\\(?:new|context)\\s+(?:[a-zA-Z]*?(?:Staff\
246 (?:Group)?|Voice|FiguredBass|FretBoards|Names|Devnull))\\s+=\\s+"?([a-zA-Z]+)"?\\s+')
248 def ly_comment_gettext (t
, m
):
249 return m
.group (1) + t (m
.group (2))
253 class CompileError(Exception):
258 ####################################################################
260 ####################################################################
263 def replacement_text (self
):
266 def filter_text (self
):
267 return self
.replacement_text ()
272 class Substring (Chunk
):
273 """A string that does not require extra memory."""
274 def __init__ (self
, source
, start
, end
, line_number
):
278 self
.line_number
= line_number
279 self
.override_text
= None
284 def replacement_text (self
):
285 if self
.override_text
:
286 return self
.override_text
288 return self
.source
[self
.start
:self
.end
]
292 class Snippet (Chunk
):
293 def __init__ (self
, type, match
, formatter
, line_number
, global_options
):
297 self
.option_dict
= {}
298 self
.formatter
= formatter
299 self
.line_number
= line_number
300 self
.global_options
= global_options
301 self
.replacements
= {'program_version': ly
.program_version
,
302 'program_name': ly
.program_name
}
304 # return a shallow copy of the replacements, so the caller can modify
305 # it locally without interfering with other snippet operations
306 def get_replacements (self
):
307 return copy
.copy (self
.replacements
)
309 def replacement_text (self
):
310 return self
.match
.group ('match')
312 def substring (self
, s
):
313 return self
.match
.group (s
)
316 return `self
.__class
__`
+ ' type = ' + self
.type
320 class IncludeSnippet (Snippet
):
321 def processed_filename (self
):
322 f
= self
.substring ('filename')
323 return os
.path
.splitext (f
)[0] + self
.formatter
.default_extension
325 def replacement_text (self
):
326 s
= self
.match
.group ('match')
327 f
= self
.substring ('filename')
328 return re
.sub (f
, self
.processed_filename (), s
)
332 class LilypondSnippet (Snippet
):
333 def __init__ (self
, type, match
, formatter
, line_number
, global_options
):
334 Snippet
.__init
__ (self
, type, match
, formatter
, line_number
, global_options
)
335 os
= match
.group ('options')
336 self
.do_options (os
, self
.type)
339 def snippet_options (self
):
342 def verb_ly_gettext (self
, s
):
343 lang
= self
.formatter
.document_language
347 t
= langdefs
.translation
[lang
]
351 s
= ly_comment_re
.sub (lambda m
: ly_comment_gettext (t
, m
), s
)
353 if langdefs
.LANGDICT
[lang
].enable_ly_identifier_l10n
:
354 for v
in ly_var_def_re
.findall (s
):
355 s
= re
.sub (r
"(?m)(?<!\\clef)(^|[' \\#])%s([^a-zA-Z])" % v
,
356 "\\1" + t (v
) + "\\2",
358 for id in ly_context_id_re
.findall (s
):
359 s
= re
.sub (r
'(\s+|")%s(\s+|")' % id,
360 "\\1" + t (id) + "\\2",
365 verb_text
= self
.substring ('code')
366 if not NOGETTEXT
in self
.option_dict
:
367 verb_text
= self
.verb_ly_gettext (verb_text
)
368 if not verb_text
.endswith ('\n'):
373 contents
= self
.substring ('code')
374 return ('\\sourcefileline %d\n%s'
375 % (self
.line_number
- 1, contents
))
380 return self
.compose_ly (s
)
383 def split_options (self
, option_string
):
384 return self
.formatter
.split_snippet_options (option_string
);
386 def do_options (self
, option_string
, type):
387 self
.option_dict
= {}
389 options
= self
.split_options (option_string
)
391 for option
in options
:
393 (key
, value
) = re
.split ('\s*=\s*', option
)
394 self
.option_dict
[key
] = value
396 if option
in no_options
:
397 if no_options
[option
] in self
.option_dict
:
398 del self
.option_dict
[no_options
[option
]]
400 self
.option_dict
[option
] = None
403 # If LINE_WIDTH is used without parameter, set it to default.
404 has_line_width
= self
.option_dict
.has_key (LINE_WIDTH
)
405 if has_line_width
and self
.option_dict
[LINE_WIDTH
] == None:
406 has_line_width
= False
407 del self
.option_dict
[LINE_WIDTH
]
409 # TODO: Can't we do that more efficiently (built-in python func?)
410 for k
in self
.formatter
.default_snippet_options
:
411 if k
not in self
.option_dict
:
412 self
.option_dict
[k
] = self
.formatter
.default_snippet_options
[k
]
414 # RELATIVE does not work without FRAGMENT;
415 # make RELATIVE imply FRAGMENT
416 has_relative
= self
.option_dict
.has_key (RELATIVE
)
417 if has_relative
and not self
.option_dict
.has_key (FRAGMENT
):
418 self
.option_dict
[FRAGMENT
] = None
420 if not has_line_width
:
421 if type == 'lilypond' or FRAGMENT
in self
.option_dict
:
422 self
.option_dict
[RAGGED_RIGHT
] = None
424 if type == 'lilypond':
425 if LINE_WIDTH
in self
.option_dict
:
426 del self
.option_dict
[LINE_WIDTH
]
428 if RAGGED_RIGHT
in self
.option_dict
:
429 if LINE_WIDTH
in self
.option_dict
:
430 del self
.option_dict
[LINE_WIDTH
]
432 if QUOTE
in self
.option_dict
or type == 'lilypond':
433 if LINE_WIDTH
in self
.option_dict
:
434 del self
.option_dict
[LINE_WIDTH
]
436 if not INDENT
in self
.option_dict
:
437 self
.option_dict
[INDENT
] = '0\\mm'
439 # Set a default line-width if there is none. We need this, because
440 # lilypond-book has set left-padding by default and therefore does
441 # #(define line-width (- line-width (* 3 mm)))
442 # TODO: Junk this ugly hack if the code gets rewritten to concatenate
443 # all settings before writing them in the \paper block.
444 if not LINE_WIDTH
in self
.option_dict
:
445 if not QUOTE
in self
.option_dict
:
446 if not LILYQUOTE
in self
.option_dict
:
447 self
.option_dict
[LINE_WIDTH
] = "#(- paper-width \
448 left-margin-default right-margin-default)"
450 def get_option_list (self
):
451 if not 'option_list' in self
.__dict
__:
453 for (key
, value
) in self
.option_dict
.items ():
455 option_list
.append (key
)
457 option_list
.append (key
+ '=' + value
)
459 self
.option_list
= option_list
460 return self
.option_list
462 def compose_ly (self
, code
):
463 if FRAGMENT
in self
.option_dict
:
471 # The original concept of the `exampleindent' option is broken.
472 # It is not possible to get a sane value for @exampleindent at all
473 # without processing the document itself. Saying
481 # causes ugly results with the DVI backend of texinfo since the
482 # default value for @exampleindent isn't 5em but 0.4in (or a smaller
483 # value). Executing the above code changes the environment
484 # indentation to an unknown value because we don't know the amount
485 # of 1em in advance since it is font-dependent. Modifying
486 # @exampleindent in the middle of a document is simply not
487 # supported within texinfo.
489 # As a consequence, the only function of @exampleindent is now to
490 # specify the amount of indentation for the `quote' option.
492 # To set @exampleindent locally to zero, we use the @format
493 # environment for non-quoted snippets.
494 override
[EXAMPLEINDENT
] = r
'0.4\in'
495 override
[LINE_WIDTH
] = '5\\in' # = texinfo_line_widths['@smallbook']
496 override
.update (self
.formatter
.default_snippet_options
)
499 for option
in self
.get_option_list ():
500 for name
in PROCESSING_INDEPENDENT_OPTIONS
:
501 if option
.startswith (name
):
504 option_list
.append (option
)
505 option_string
= ','.join (option_list
)
507 compose_types
= [NOTES
, PREAMBLE
, LAYOUT
, PAPER
]
508 for a
in compose_types
:
511 option_names
= self
.option_dict
.keys ()
513 for key
in option_names
:
514 value
= self
.option_dict
[key
]
515 (c_key
, c_value
) = classic_lilypond_book_compatibility (key
, value
)
519 _ ("deprecated ly-option used: %s=%s") % (key
, value
))
521 _ ("compatibility mode translation: %s=%s") % (c_key
, c_value
))
524 _ ("deprecated ly-option used: %s") % key
)
526 _ ("compatibility mode translation: %s") % c_key
)
528 (key
, value
) = (c_key
, c_value
)
531 override
[key
] = value
533 if not override
.has_key (key
):
537 for typ
in compose_types
:
538 if snippet_options
[typ
].has_key (key
):
539 compose_dict
[typ
].append (snippet_options
[typ
][key
])
543 if not found
and key
not in simple_options
and key
not in self
.snippet_options ():
544 warning (_ ("ignoring unknown ly option: %s") % key
)
547 if RELATIVE
in override
and override
[RELATIVE
]:
548 relative
= int (override
[RELATIVE
])
554 relative_quotes
+= ',' * (- relative
)
556 relative_quotes
+= "'" * relative
558 # put paper-size first, if it exists
559 for i
,elem
in enumerate(compose_dict
[PAPER
]):
560 if elem
.startswith("#(set-paper-size"):
561 compose_dict
[PAPER
].insert(0, compose_dict
[PAPER
].pop(i
))
564 paper_string
= '\n '.join (compose_dict
[PAPER
]) % override
565 layout_string
= '\n '.join (compose_dict
[LAYOUT
]) % override
566 notes_string
= '\n '.join (compose_dict
[NOTES
]) % vars ()
567 preamble_string
= '\n '.join (compose_dict
[PREAMBLE
]) % override
568 padding_mm
= self
.global_options
.padding_mm
569 if self
.global_options
.safe_mode
:
570 safe_mode_string
= "#(ly:set-option 'safe #t)"
572 safe_mode_string
= ""
576 d
.update (self
.global_options
.information
)
577 return (PREAMBLE_LY
+ body
) % d
579 def get_checksum (self
):
580 if not self
.checksum
:
581 # Work-around for md5 module deprecation warning in python 2.5+:
583 from hashlib
import md5
587 # We only want to calculate the hash based on the snippet
588 # code plus fragment options relevant to processing by
589 # lilypond, not the snippet + preamble
590 hash = md5 (self
.relevant_contents (self
.ly ()))
591 for option
in self
.get_option_list ():
592 for name
in PROCESSING_INDEPENDENT_OPTIONS
:
593 if option
.startswith (name
):
598 ## let's not create too long names.
599 self
.checksum
= hash.hexdigest ()[:10]
604 cs
= self
.get_checksum ()
605 name
= '%s/lily-%s' % (cs
[:2], cs
[2:])
608 final_basename
= basename
611 base
= self
.basename ()
612 path
= os
.path
.join (self
.global_options
.lily_output_dir
, base
)
613 directory
= os
.path
.split(path
)[0]
614 if not os
.path
.isdir (directory
):
615 os
.makedirs (directory
)
616 filename
= path
+ '.ly'
617 if os
.path
.exists (filename
):
618 existing
= open (filename
, 'r').read ()
620 if self
.relevant_contents (existing
) != self
.relevant_contents (self
.full_ly ()):
621 warning ("%s: duplicate filename but different contents of orginal file,\n\
622 printing diff against existing file." % filename
)
623 ly
.stderr_write (self
.filter_pipe (self
.full_ly (), 'diff -u %s -' % filename
))
625 out
= file (filename
, 'w')
626 out
.write (self
.full_ly ())
627 file (path
+ '.txt', 'w').write ('image of music')
629 def relevant_contents (self
, ly
):
630 return re
.sub (r
'\\(version|sourcefileline|sourcefilename)[^\n]*\n', '', ly
)
632 def link_all_output_files (self
, output_dir
, output_dir_files
, destination
):
633 existing
, missing
= self
.all_output_files (output_dir
, output_dir_files
)
635 print '\nMissing', missing
636 raise CompileError(self
.basename())
637 for name
in existing
:
638 if (self
.global_options
.use_source_file_names
639 and isinstance (self
, LilypondFileSnippet
)):
640 base
, ext
= os
.path
.splitext (name
)
641 components
= base
.split ('-')
642 # ugh, assume filenames with prefix with one dash (lily-xxxx)
643 if len (components
) > 2:
644 base_suffix
= '-' + components
[-1]
647 final_name
= self
.final_basename () + base_suffix
+ ext
651 os
.unlink (os
.path
.join (destination
, final_name
))
655 src
= os
.path
.join (output_dir
, name
)
656 dst
= os
.path
.join (destination
, final_name
)
657 dst_path
= os
.path
.split(dst
)[0]
658 if not os
.path
.isdir (dst_path
):
659 os
.makedirs (dst_path
)
663 def all_output_files (self
, output_dir
, output_dir_files
):
664 """Return all files generated in lily_output_dir, a set.
666 output_dir_files is the list of files in the output directory.
670 base
= self
.basename()
671 full
= os
.path
.join (output_dir
, base
)
672 def consider_file (name
):
673 if name
in output_dir_files
:
676 def require_file (name
):
677 if name
in output_dir_files
:
682 # UGH - junk self.global_options
683 skip_lily
= self
.global_options
.skip_lilypond_run
684 for required
in [base
+ '.ly',
686 require_file (required
)
688 require_file (base
+ '-systems.count')
690 if 'ddump-profile' in self
.global_options
.process_cmd
:
691 require_file (base
+ '.profile')
692 if 'dseparate-log-file' in self
.global_options
.process_cmd
:
693 require_file (base
+ '.log')
695 map (consider_file
, [base
+ '.tex',
699 base
+ '-systems.texi',
700 base
+ '-systems.tex',
701 base
+ '-systems.pdftexi'])
702 if self
.formatter
.document_language
:
704 [base
+ '.texidoc' + self
.formatter
.document_language
,
705 base
+ '.doctitle' + self
.formatter
.document_language
])
707 required_files
= self
.formatter
.required_files (self
, base
, full
, result
)
708 for f
in required_files
:
712 if not skip_lily
and not missing
:
713 system_count
= int(file (full
+ '-systems.count').read())
715 for number
in range(1, system_count
+ 1):
716 systemfile
= '%s-%d' % (base
, number
)
717 require_file (systemfile
+ '.eps')
718 consider_file (systemfile
+ '.pdf')
720 # We can't require signatures, since books and toplevel
721 # markups do not output a signature.
722 if 'ddump-signature' in self
.global_options
.process_cmd
:
723 consider_file (systemfile
+ '.signature')
726 return (result
, missing
)
728 def is_outdated (self
, output_dir
, current_files
):
729 found
, missing
= self
.all_output_files (output_dir
, current_files
)
732 def filter_pipe (self
, input, cmd
):
733 """Pass input through cmd, and return the result."""
735 if self
.global_options
.verbose
:
736 progress (_ ("Opening filter `%s'\n") % cmd
)
738 # TODO: Use Popen once we resolve the problem with msvcrt in Windows:
739 (stdin
, stdout
, stderr
) = os
.popen3 (cmd
)
740 # p = Popen(cmd, shell=True, stdin=PIPE, stdout=PIPE, stderr=PIPE, close_fds=True)
741 # (stdin, stdout, stderr) = (p.stdin, p.stdout, p.stderr)
743 status
= stdin
.close ()
747 output
= stdout
.read ()
748 status
= stdout
.close ()
749 error
= stderr
.read ()
753 signal
= 0x0f & status
754 if status
or (not output
and error
):
755 exit_status
= status
>> 8
756 ly
.error (_ ("`%s' failed (%d)") % (cmd
, exit_status
))
757 ly
.error (_ ("The error log is as follows:"))
758 ly
.stderr_write (error
)
759 ly
.stderr_write (stderr
.read ())
762 if self
.global_options
.verbose
:
767 def get_snippet_code (self
):
768 return self
.substring ('code');
770 def filter_text (self
):
771 """Run snippet bodies through a command (say: convert-ly).
773 This functionality is rarely used, and this code must have bitrot.
775 code
= self
.get_snippet_code ();
776 s
= self
.filter_pipe (code
, self
.global_options
.filter_cmd
)
779 'options': self
.match
.group ('options')
781 return self
.formatter
.output_simple_replacements (FILTER
, d
)
783 def replacement_text (self
):
784 base
= self
.final_basename ()
785 return self
.formatter
.snippet_output (base
, self
)
787 def get_images (self
):
788 rep
= {'base': self
.final_basename ()}
790 single
= '%(base)s.png' % rep
791 multiple
= '%(base)s-page1.png' % rep
793 if (os
.path
.exists (multiple
)
794 and (not os
.path
.exists (single
)
795 or (os
.stat (multiple
)[stat
.ST_MTIME
]
796 > os
.stat (single
)[stat
.ST_MTIME
]))):
797 count
= ps_page_count ('%(base)s.eps' % rep
)
798 images
= ['%s-page%d.png' % (rep
['base'], page
) for page
in range (1, count
+1)]
799 images
= tuple (images
)
805 re_begin_verbatim
= re
.compile (r
'\s+%.*?begin verbatim.*\n*', re
.M
)
806 re_end_verbatim
= re
.compile (r
'\s+%.*?end verbatim.*$', re
.M
)
808 class LilypondFileSnippet (LilypondSnippet
):
809 def __init__ (self
, type, match
, formatter
, line_number
, global_options
):
810 LilypondSnippet
.__init
__ (self
, type, match
, formatter
, line_number
, global_options
)
811 self
.contents
= file (BookBase
.find_file (self
.substring ('filename'), global_options
.include_path
)).read ()
813 def get_snippet_code (self
):
814 return self
.contents
;
818 s
= re_begin_verbatim
.split (s
)[-1]
819 s
= re_end_verbatim
.split (s
)[0]
820 if not NOGETTEXT
in self
.option_dict
:
821 s
= self
.verb_ly_gettext (s
)
822 if not s
.endswith ('\n'):
827 name
= self
.substring ('filename')
828 return ('\\sourcefilename \"%s\"\n\\sourcefileline 0\n%s'
829 % (name
, self
.contents
))
831 def final_basename (self
):
832 if self
.global_options
.use_source_file_names
:
833 base
= os
.path
.splitext (os
.path
.basename (self
.substring ('filename')))[0]
836 return self
.basename ()
839 class LilyPondVersionString (Snippet
):
840 """A string that does not require extra memory."""
841 def __init__ (self
, type, match
, formatter
, line_number
, global_options
):
842 Snippet
.__init
__ (self
, type, match
, formatter
, line_number
, global_options
)
844 def replacement_text (self
):
845 return self
.formatter
.output_simple (self
.type, self
)
848 snippet_type_to_class
= {
849 'lilypond_file': LilypondFileSnippet
,
850 'lilypond_block': LilypondSnippet
,
851 'lilypond': LilypondSnippet
,
852 'include': IncludeSnippet
,
853 'lilypondversion': LilyPondVersionString
,