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'
57 PRINTFILENAME
= 'printfilename'
59 RAGGED_RIGHT
= 'ragged-right'
61 STAFFSIZE
= 'staffsize'
64 VERSION
= 'lilypondversion'
68 # NOTIME and NOGETTEXT have no opposite so they aren't part of this
70 # NOQUOTE is used internally only.
76 # Options that have no impact on processing by lilypond (or --process
78 PROCESSING_INDEPENDENT_OPTIONS
= (
79 ALT
, NOGETTEXT
, VERBATIM
, ADDVERSION
,
80 TEXIDOC
, DOCTITLE
, VERSION
, PRINTFILENAME
)
84 # Options without a pattern in snippet_options.
103 ####################################################################
104 # LilyPond templates for the snippets
105 ####################################################################
110 RELATIVE
: r
'''\relative c%(relative_quotes)s''',
115 INDENT
: r
'''indent = %(indent)s''',
116 LINE_WIDTH
: r
'''line-width = %(line-width)s''',
117 QUOTE
: r
'''line-width = %(line-width)s - 2.0 * %(exampleindent)s''',
118 LILYQUOTE
: r
'''line-width = %(line-width)s - 2.0 * %(exampleindent)s''',
119 RAGGED_RIGHT
: r
'''ragged-right = ##t''',
120 NORAGGED_RIGHT
: r
'''ragged-right = ##f''',
132 \remove "Time_signature_engraver"
138 STAFFSIZE
: r
'''#(set-global-staff-size %(staffsize)s)''',
151 %% ****************************************************************
152 %% ly snippet contents follows:
153 %% ****************************************************************
157 %% ****************************************************************
159 %% ****************************************************************
163 def classic_lilypond_book_compatibility (key
, value
):
164 if key
== 'singleline' and value
== None:
165 return (RAGGED_RIGHT
, None)
167 m
= re
.search ('relative\s*([-0-9])', key
)
169 return ('relative', m
.group (1))
171 m
= re
.match ('([0-9]+)pt', key
)
173 return ('staffsize', m
.group (1))
175 if key
== 'indent' or key
== 'line-width':
176 m
= re
.match ('([-.0-9]+)(cm|in|mm|pt|staffspace)', value
)
178 f
= float (m
.group (1))
179 return (key
, '%f\\%s' % (f
, m
.group (2)))
184 PREAMBLE_LY
= '''%%%% Generated by %(program_name)s
185 %%%% Options: [%(option_string)s]
186 \\include "lilypond-book-preamble.ly"
189 %% ****************************************************************
190 %% Start cut-&-pastable-section
191 %% ****************************************************************
197 force-assignment = #""
198 line-width = #(- line-width (* mm %(padding_mm)f))
212 %% ****************************************************************
214 %% ****************************************************************
218 %% ****************************************************************
220 %% ****************************************************************
230 ####################################################################
232 ####################################################################
234 def ps_page_count (ps_name
):
235 header
= file (ps_name
).read (1024)
236 m
= re
.search ('\n%%Pages: ([0-9]+)', header
)
238 return int (m
.group (1))
241 ly_var_def_re
= re
.compile (r
'^([a-zA-Z]+)[\t ]*=', re
.M
)
242 ly_comment_re
= re
.compile (r
'(%+[\t ]*)(.*)$', re
.M
)
243 ly_context_id_re
= re
.compile ('\\\\(?:new|context)\\s+(?:[a-zA-Z]*?(?:Staff\
244 (?:Group)?|Voice|FiguredBass|FretBoards|Names|Devnull))\\s+=\\s+"?([a-zA-Z]+)"?\\s+')
246 def ly_comment_gettext (t
, m
):
247 return m
.group (1) + t (m
.group (2))
251 class CompileError(Exception):
256 ####################################################################
258 ####################################################################
261 def replacement_text (self
):
264 def filter_text (self
):
265 return self
.replacement_text ()
270 class Substring (Chunk
):
271 """A string that does not require extra memory."""
272 def __init__ (self
, source
, start
, end
, line_number
):
276 self
.line_number
= line_number
277 self
.override_text
= None
282 def replacement_text (self
):
283 if self
.override_text
:
284 return self
.override_text
286 return self
.source
[self
.start
:self
.end
]
290 class Snippet (Chunk
):
291 def __init__ (self
, type, match
, formatter
, line_number
, global_options
):
295 self
.option_dict
= {}
296 self
.formatter
= formatter
297 self
.line_number
= line_number
298 self
.global_options
= global_options
299 self
.replacements
= {'program_version': ly
.program_version
,
300 'program_name': ly
.program_name
}
302 # return a shallow copy of the replacements, so the caller can modify
303 # it locally without interfering with other snippet operations
304 def get_replacements (self
):
305 return copy
.copy (self
.replacements
)
307 def replacement_text (self
):
308 return self
.match
.group ('match')
310 def substring (self
, s
):
311 return self
.match
.group (s
)
314 return `self
.__class
__`
+ ' type = ' + self
.type
318 class IncludeSnippet (Snippet
):
319 def processed_filename (self
):
320 f
= self
.substring ('filename')
321 return os
.path
.splitext (f
)[0] + self
.formatter
.default_extension
323 def replacement_text (self
):
324 s
= self
.match
.group ('match')
325 f
= self
.substring ('filename')
326 return re
.sub (f
, self
.processed_filename (), s
)
330 class LilypondSnippet (Snippet
):
331 def __init__ (self
, type, match
, formatter
, line_number
, global_options
):
332 Snippet
.__init
__ (self
, type, match
, formatter
, line_number
, global_options
)
333 os
= match
.group ('options')
334 self
.do_options (os
, self
.type)
337 def snippet_options (self
):
340 def verb_ly_gettext (self
, s
):
341 lang
= self
.formatter
.document_language
345 t
= langdefs
.translation
[lang
]
349 s
= ly_comment_re
.sub (lambda m
: ly_comment_gettext (t
, m
), s
)
351 if langdefs
.LANGDICT
[lang
].enable_ly_identifier_l10n
:
352 for v
in ly_var_def_re
.findall (s
):
353 s
= re
.sub (r
"(?m)(?<!\\clef)(^|[' \\#])%s([^a-zA-Z])" % v
,
354 "\\1" + t (v
) + "\\2",
356 for id in ly_context_id_re
.findall (s
):
357 s
= re
.sub (r
'(\s+|")%s(\s+|")' % id,
358 "\\1" + t (id) + "\\2",
363 verb_text
= self
.substring ('code')
364 if not NOGETTEXT
in self
.option_dict
:
365 verb_text
= self
.verb_ly_gettext (verb_text
)
366 if not verb_text
.endswith ('\n'):
371 contents
= self
.substring ('code')
372 return ('\\sourcefileline %d\n%s'
373 % (self
.line_number
- 1, contents
))
378 return self
.compose_ly (s
)
381 def split_options (self
, option_string
):
382 return self
.formatter
.split_snippet_options (option_string
);
384 def do_options (self
, option_string
, type):
385 self
.option_dict
= {}
387 options
= self
.split_options (option_string
)
389 for option
in options
:
391 (key
, value
) = re
.split ('\s*=\s*', option
)
392 self
.option_dict
[key
] = value
394 if option
in no_options
:
395 if no_options
[option
] in self
.option_dict
:
396 del self
.option_dict
[no_options
[option
]]
398 self
.option_dict
[option
] = None
401 # If LINE_WIDTH is used without parameter, set it to default.
402 has_line_width
= self
.option_dict
.has_key (LINE_WIDTH
)
403 if has_line_width
and self
.option_dict
[LINE_WIDTH
] == None:
404 has_line_width
= False
405 del self
.option_dict
[LINE_WIDTH
]
407 # TODO: Can't we do that more efficiently (built-in python func?)
408 for k
in self
.formatter
.default_snippet_options
:
409 if k
not in self
.option_dict
:
410 self
.option_dict
[k
] = self
.formatter
.default_snippet_options
[k
]
412 # RELATIVE does not work without FRAGMENT;
413 # make RELATIVE imply FRAGMENT
414 has_relative
= self
.option_dict
.has_key (RELATIVE
)
415 if has_relative
and not self
.option_dict
.has_key (FRAGMENT
):
416 self
.option_dict
[FRAGMENT
] = None
418 if not has_line_width
:
419 if type == 'lilypond' or FRAGMENT
in self
.option_dict
:
420 self
.option_dict
[RAGGED_RIGHT
] = None
422 if type == 'lilypond':
423 if LINE_WIDTH
in self
.option_dict
:
424 del self
.option_dict
[LINE_WIDTH
]
426 if RAGGED_RIGHT
in self
.option_dict
:
427 if LINE_WIDTH
in self
.option_dict
:
428 del self
.option_dict
[LINE_WIDTH
]
430 if QUOTE
in self
.option_dict
or type == 'lilypond':
431 if LINE_WIDTH
in self
.option_dict
:
432 del self
.option_dict
[LINE_WIDTH
]
434 if not INDENT
in self
.option_dict
:
435 self
.option_dict
[INDENT
] = '0\\mm'
437 # Set a default line-width if there is none. We need this, because
438 # lilypond-book has set left-padding by default and therefore does
439 # #(define line-width (- line-width (* 3 mm)))
440 # TODO: Junk this ugly hack if the code gets rewritten to concatenate
441 # all settings before writing them in the \paper block.
442 if not LINE_WIDTH
in self
.option_dict
:
443 if not QUOTE
in self
.option_dict
:
444 if not LILYQUOTE
in self
.option_dict
:
445 self
.option_dict
[LINE_WIDTH
] = "#(- paper-width \
446 left-margin-default right-margin-default)"
448 def get_option_list (self
):
449 if not 'option_list' in self
.__dict
__:
451 for (key
, value
) in self
.option_dict
.items ():
453 option_list
.append (key
)
455 option_list
.append (key
+ '=' + value
)
457 self
.option_list
= option_list
458 return self
.option_list
460 def compose_ly (self
, code
):
461 if FRAGMENT
in self
.option_dict
:
469 # The original concept of the `exampleindent' option is broken.
470 # It is not possible to get a sane value for @exampleindent at all
471 # without processing the document itself. Saying
479 # causes ugly results with the DVI backend of texinfo since the
480 # default value for @exampleindent isn't 5em but 0.4in (or a smaller
481 # value). Executing the above code changes the environment
482 # indentation to an unknown value because we don't know the amount
483 # of 1em in advance since it is font-dependent. Modifying
484 # @exampleindent in the middle of a document is simply not
485 # supported within texinfo.
487 # As a consequence, the only function of @exampleindent is now to
488 # specify the amount of indentation for the `quote' option.
490 # To set @exampleindent locally to zero, we use the @format
491 # environment for non-quoted snippets.
492 override
[EXAMPLEINDENT
] = r
'0.4\in'
493 override
[LINE_WIDTH
] = '5\\in' # = texinfo_line_widths['@smallbook']
494 override
.update (self
.formatter
.default_snippet_options
)
497 for option
in self
.get_option_list ():
498 for name
in PROCESSING_INDEPENDENT_OPTIONS
:
499 if option
.startswith (name
):
502 option_list
.append (option
)
503 option_string
= ','.join (option_list
)
505 compose_types
= [NOTES
, PREAMBLE
, LAYOUT
, PAPER
]
506 for a
in compose_types
:
509 option_names
= self
.option_dict
.keys ()
511 for key
in option_names
:
512 value
= self
.option_dict
[key
]
513 (c_key
, c_value
) = classic_lilypond_book_compatibility (key
, value
)
517 _ ("deprecated ly-option used: %s=%s") % (key
, value
))
519 _ ("compatibility mode translation: %s=%s") % (c_key
, c_value
))
522 _ ("deprecated ly-option used: %s") % key
)
524 _ ("compatibility mode translation: %s") % c_key
)
526 (key
, value
) = (c_key
, c_value
)
529 override
[key
] = value
531 if not override
.has_key (key
):
535 for typ
in compose_types
:
536 if snippet_options
[typ
].has_key (key
):
537 compose_dict
[typ
].append (snippet_options
[typ
][key
])
541 if not found
and key
not in simple_options
and key
not in self
.snippet_options ():
542 warning (_ ("ignoring unknown ly option: %s") % key
)
545 if RELATIVE
in override
and override
[RELATIVE
]:
546 relative
= int (override
[RELATIVE
])
552 relative_quotes
+= ',' * (- relative
)
554 relative_quotes
+= "'" * relative
556 paper_string
= '\n '.join (compose_dict
[PAPER
]) % override
557 layout_string
= '\n '.join (compose_dict
[LAYOUT
]) % override
558 notes_string
= '\n '.join (compose_dict
[NOTES
]) % vars ()
559 preamble_string
= '\n '.join (compose_dict
[PREAMBLE
]) % override
560 padding_mm
= self
.global_options
.padding_mm
561 if self
.global_options
.safe_mode
:
562 safe_mode_string
= "#(ly:set-option 'safe #t)"
564 safe_mode_string
= ""
568 d
.update (self
.global_options
.information
)
569 return (PREAMBLE_LY
+ body
) % d
571 def get_checksum (self
):
572 if not self
.checksum
:
573 # Work-around for md5 module deprecation warning in python 2.5+:
575 from hashlib
import md5
579 # We only want to calculate the hash based on the snippet
580 # code plus fragment options relevant to processing by
581 # lilypond, not the snippet + preamble
582 hash = md5 (self
.relevant_contents (self
.ly ()))
583 for option
in self
.get_option_list ():
584 for name
in PROCESSING_INDEPENDENT_OPTIONS
:
585 if option
.startswith (name
):
590 ## let's not create too long names.
591 self
.checksum
= hash.hexdigest ()[:10]
596 cs
= self
.get_checksum ()
597 name
= '%s/lily-%s' % (cs
[:2], cs
[2:])
600 final_basename
= basename
603 base
= self
.basename ()
604 path
= os
.path
.join (self
.global_options
.lily_output_dir
, base
)
605 directory
= os
.path
.split(path
)[0]
606 if not os
.path
.isdir (directory
):
607 os
.makedirs (directory
)
608 filename
= path
+ '.ly'
609 if os
.path
.exists (filename
):
610 existing
= open (filename
, 'r').read ()
612 if self
.relevant_contents (existing
) != self
.relevant_contents (self
.full_ly ()):
613 warning ("%s: duplicate filename but different contents of orginal file,\n\
614 printing diff against existing file." % filename
)
615 ly
.stderr_write (self
.filter_pipe (self
.full_ly (), 'diff -u %s -' % filename
))
617 out
= file (filename
, 'w')
618 out
.write (self
.full_ly ())
619 file (path
+ '.txt', 'w').write ('image of music')
621 def relevant_contents (self
, ly
):
622 return re
.sub (r
'\\(version|sourcefileline|sourcefilename)[^\n]*\n', '', ly
)
624 def link_all_output_files (self
, output_dir
, output_dir_files
, destination
):
625 existing
, missing
= self
.all_output_files (output_dir
, output_dir_files
)
627 print '\nMissing', missing
628 raise CompileError(self
.basename())
629 for name
in existing
:
630 if (self
.global_options
.use_source_file_names
631 and isinstance (self
, LilypondFileSnippet
)):
632 base
, ext
= os
.path
.splitext (name
)
633 components
= base
.split ('-')
634 # ugh, assume filenames with prefix with one dash (lily-xxxx)
635 if len (components
) > 2:
636 base_suffix
= '-' + components
[-1]
639 final_name
= self
.final_basename () + base_suffix
+ ext
643 os
.unlink (os
.path
.join (destination
, final_name
))
647 src
= os
.path
.join (output_dir
, name
)
648 dst
= os
.path
.join (destination
, final_name
)
649 dst_path
= os
.path
.split(dst
)[0]
650 if not os
.path
.isdir (dst_path
):
651 os
.makedirs (dst_path
)
655 def all_output_files (self
, output_dir
, output_dir_files
):
656 """Return all files generated in lily_output_dir, a set.
658 output_dir_files is the list of files in the output directory.
662 base
= self
.basename()
663 full
= os
.path
.join (output_dir
, base
)
664 def consider_file (name
):
665 if name
in output_dir_files
:
668 def require_file (name
):
669 if name
in output_dir_files
:
674 # UGH - junk self.global_options
675 skip_lily
= self
.global_options
.skip_lilypond_run
676 for required
in [base
+ '.ly',
678 require_file (required
)
680 require_file (base
+ '-systems.count')
682 if 'ddump-profile' in self
.global_options
.process_cmd
:
683 require_file (base
+ '.profile')
684 if 'dseparate-log-file' in self
.global_options
.process_cmd
:
685 require_file (base
+ '.log')
687 map (consider_file
, [base
+ '.tex',
691 base
+ '-systems.texi',
692 base
+ '-systems.tex',
693 base
+ '-systems.pdftexi'])
694 if self
.formatter
.document_language
:
696 [base
+ '.texidoc' + self
.formatter
.document_language
,
697 base
+ '.doctitle' + self
.formatter
.document_language
])
699 required_files
= self
.formatter
.required_files (self
, base
, full
, result
)
700 for f
in required_files
:
704 if not skip_lily
and not missing
:
705 system_count
= int(file (full
+ '-systems.count').read())
707 for number
in range(1, system_count
+ 1):
708 systemfile
= '%s-%d' % (base
, number
)
709 require_file (systemfile
+ '.eps')
710 consider_file (systemfile
+ '.pdf')
712 # We can't require signatures, since books and toplevel
713 # markups do not output a signature.
714 if 'ddump-signature' in self
.global_options
.process_cmd
:
715 consider_file (systemfile
+ '.signature')
718 return (result
, missing
)
720 def is_outdated (self
, output_dir
, current_files
):
721 found
, missing
= self
.all_output_files (output_dir
, current_files
)
724 def filter_pipe (self
, input, cmd
):
725 """Pass input through cmd, and return the result."""
727 if self
.global_options
.verbose
:
728 progress (_ ("Opening filter `%s'\n") % cmd
)
730 # TODO: Use Popen once we resolve the problem with msvcrt in Windows:
731 (stdin
, stdout
, stderr
) = os
.popen3 (cmd
)
732 # p = Popen(cmd, shell=True, stdin=PIPE, stdout=PIPE, stderr=PIPE, close_fds=True)
733 # (stdin, stdout, stderr) = (p.stdin, p.stdout, p.stderr)
735 status
= stdin
.close ()
739 output
= stdout
.read ()
740 status
= stdout
.close ()
741 error
= stderr
.read ()
745 signal
= 0x0f & status
746 if status
or (not output
and error
):
747 exit_status
= status
>> 8
748 ly
.error (_ ("`%s' failed (%d)") % (cmd
, exit_status
))
749 ly
.error (_ ("The error log is as follows:"))
750 ly
.stderr_write (error
)
751 ly
.stderr_write (stderr
.read ())
754 if self
.global_options
.verbose
:
759 def get_snippet_code (self
):
760 return self
.substring ('code');
762 def filter_text (self
):
763 """Run snippet bodies through a command (say: convert-ly).
765 This functionality is rarely used, and this code must have bitrot.
767 code
= self
.get_snippet_code ();
768 s
= self
.filter_pipe (code
, self
.global_options
.filter_cmd
)
771 'options': self
.match
.group ('options')
773 return self
.formatter
.output_simple_replacements (FILTER
, d
)
775 def replacement_text (self
):
776 base
= self
.final_basename ()
777 return self
.formatter
.snippet_output (base
, self
)
779 def get_images (self
):
780 rep
= {'base': self
.final_basename ()}
782 single
= '%(base)s.png' % rep
783 multiple
= '%(base)s-page1.png' % rep
785 if (os
.path
.exists (multiple
)
786 and (not os
.path
.exists (single
)
787 or (os
.stat (multiple
)[stat
.ST_MTIME
]
788 > os
.stat (single
)[stat
.ST_MTIME
]))):
789 count
= ps_page_count ('%(base)s.eps' % rep
)
790 images
= ['%s-page%d.png' % (rep
['base'], page
) for page
in range (1, count
+1)]
791 images
= tuple (images
)
797 re_begin_verbatim
= re
.compile (r
'\s+%.*?begin verbatim.*\n*', re
.M
)
798 re_end_verbatim
= re
.compile (r
'\s+%.*?end verbatim.*$', re
.M
)
800 class LilypondFileSnippet (LilypondSnippet
):
801 def __init__ (self
, type, match
, formatter
, line_number
, global_options
):
802 LilypondSnippet
.__init
__ (self
, type, match
, formatter
, line_number
, global_options
)
803 self
.contents
= file (BookBase
.find_file (self
.substring ('filename'), global_options
.include_path
)).read ()
805 def get_snippet_code (self
):
806 return self
.contents
;
810 s
= re_begin_verbatim
.split (s
)[-1]
811 s
= re_end_verbatim
.split (s
)[0]
812 if not NOGETTEXT
in self
.option_dict
:
813 s
= self
.verb_ly_gettext (s
)
814 if not s
.endswith ('\n'):
819 name
= self
.substring ('filename')
820 return ('\\sourcefilename \"%s\"\n\\sourcefileline 0\n%s'
821 % (name
, self
.contents
))
823 def final_basename (self
):
824 if self
.global_options
.use_source_file_names
:
825 base
= os
.path
.splitext (os
.path
.basename (self
.substring ('filename')))[0]
828 return self
.basename ()
831 class LilyPondVersionString (Snippet
):
832 """A string that does not require extra memory."""
833 def __init__ (self
, type, match
, formatter
, line_number
, global_options
):
834 Snippet
.__init
__ (self
, type, match
, formatter
, line_number
, global_options
)
836 def replacement_text (self
):
837 return self
.formatter
.output_simple (self
.type, self
)
840 snippet_type_to_class
= {
841 'lilypond_file': LilypondFileSnippet
,
842 'lilypond_block': LilypondSnippet
,
843 'lilypond': LilypondSnippet
,
844 'include': IncludeSnippet
,
845 'lilypondversion': LilyPondVersionString
,