1 # -*- coding: utf-8 -*-
3 import book_base
as BookBase
9 from subprocess
import Popen
, PIPE
11 progress
= ly
.progress
19 ####################################################################
20 # Snippet option handling
21 ####################################################################
25 # Is this pythonic? Personally, I find this rather #define-nesque. --hwn
28 ADDVERSION
= 'addversion'
33 EXAMPLEINDENT
= 'exampleindent'
37 LANG
= 'lang' ## TODO: This is handled nowhere!
39 LILYQUOTE
= 'lilyquote'
40 LINE_WIDTH
= 'line-width'
41 NOFRAGMENT
= 'nofragment'
42 NOGETTEXT
= 'nogettext'
46 NORAGGED_RIGHT
= 'noragged-right'
50 OUTPUTIMAGE
= 'outputimage'
53 PRINTFILENAME
= 'printfilename'
55 RAGGED_RIGHT
= 'ragged-right'
57 STAFFSIZE
= 'staffsize'
60 VERSION
= 'lilypondversion'
64 # NOTIME and NOGETTEXT have no opposite so they aren't part of this
66 # NOQUOTE is used internally only.
72 # Options that have no impact on processing by lilypond (or --process
74 PROCESSING_INDEPENDENT_OPTIONS
= (
75 ALT
, NOGETTEXT
, VERBATIM
, ADDVERSION
,
76 TEXIDOC
, DOCTITLE
, VERSION
, PRINTFILENAME
)
80 # Options without a pattern in snippet_options.
99 ####################################################################
100 # LilyPond templates for the snippets
101 ####################################################################
106 RELATIVE
: r
'''\relative c%(relative_quotes)s''',
111 INDENT
: r
'''indent = %(indent)s''',
112 LINE_WIDTH
: r
'''line-width = %(line-width)s''',
113 QUOTE
: r
'''line-width = %(line-width)s - 2.0 * %(exampleindent)s''',
114 LILYQUOTE
: r
'''line-width = %(line-width)s - 2.0 * %(exampleindent)s''',
115 RAGGED_RIGHT
: r
'''ragged-right = ##t''',
116 NORAGGED_RIGHT
: r
'''ragged-right = ##f''',
128 \remove "Time_signature_engraver"
134 STAFFSIZE
: r
'''#(set-global-staff-size %(staffsize)s)''',
147 %% ****************************************************************
148 %% ly snippet contents follows:
149 %% ****************************************************************
153 %% ****************************************************************
155 %% ****************************************************************
159 def classic_lilypond_book_compatibility (key
, value
):
160 if key
== 'singleline' and value
== None:
161 return (RAGGED_RIGHT
, None)
163 m
= re
.search ('relative\s*([-0-9])', key
)
165 return ('relative', m
.group (1))
167 m
= re
.match ('([0-9]+)pt', key
)
169 return ('staffsize', m
.group (1))
171 if key
== 'indent' or key
== 'line-width':
172 m
= re
.match ('([-.0-9]+)(cm|in|mm|pt|staffspace)', value
)
174 f
= float (m
.group (1))
175 return (key
, '%f\\%s' % (f
, m
.group (2)))
180 PREAMBLE_LY
= '''%%%% Generated by %(program_name)s
181 %%%% Options: [%(option_string)s]
182 \\include "lilypond-book-preamble.ly"
185 %% ****************************************************************
186 %% Start cut-&-pastable-section
187 %% ****************************************************************
193 force-assignment = #""
194 line-width = #(- line-width (* mm %(padding_mm)f))
206 %% ****************************************************************
208 %% ****************************************************************
212 %% ****************************************************************
214 %% ****************************************************************
224 ####################################################################
226 ####################################################################
228 def ps_page_count (ps_name
):
229 header
= file (ps_name
).read (1024)
230 m
= re
.search ('\n%%Pages: ([0-9]+)', header
)
232 return int (m
.group (1))
235 ly_var_def_re
= re
.compile (r
'^([a-zA-Z]+)[\t ]*=', re
.M
)
236 ly_comment_re
= re
.compile (r
'(%+[\t ]*)(.*)$', re
.M
)
237 ly_context_id_re
= re
.compile ('\\\\(?:new|context)\\s+(?:[a-zA-Z]*?(?:Staff\
238 (?:Group)?|Voice|FiguredBass|FretBoards|Names|Devnull))\\s+=\\s+"?([a-zA-Z]+)"?\\s+')
240 def ly_comment_gettext (t
, m
):
241 return m
.group (1) + t (m
.group (2))
245 class CompileError(Exception):
250 ####################################################################
252 ####################################################################
255 def replacement_text (self
):
258 def filter_text (self
):
259 return self
.replacement_text ()
264 class Substring (Chunk
):
265 """A string that does not require extra memory."""
266 def __init__ (self
, source
, start
, end
, line_number
):
270 self
.line_number
= line_number
271 self
.override_text
= None
276 def replacement_text (self
):
277 if self
.override_text
:
278 return self
.override_text
280 return self
.source
[self
.start
:self
.end
]
284 class Snippet (Chunk
):
285 def __init__ (self
, type, match
, formatter
, line_number
, global_options
):
289 self
.option_dict
= {}
290 self
.formatter
= formatter
291 self
.line_number
= line_number
292 self
.global_options
= global_options
293 self
.replacements
= {'program_version': ly
.program_version
,
294 'program_name': ly
.program_name
}
296 # return a shallow copy of the replacements, so the caller can modify
297 # it locally without interfering with other snippet operations
298 def get_replacements (self
):
299 return copy
.copy (self
.replacements
)
301 def replacement_text (self
):
302 return self
.match
.group ('match')
304 def substring (self
, s
):
305 return self
.match
.group (s
)
308 return `self
.__class
__`
+ ' type = ' + self
.type
312 class IncludeSnippet (Snippet
):
313 def processed_filename (self
):
314 f
= self
.substring ('filename')
315 return os
.path
.splitext (f
)[0] + self
.formatter
.default_extension
317 def replacement_text (self
):
318 s
= self
.match
.group ('match')
319 f
= self
.substring ('filename')
320 return re
.sub (f
, self
.processed_filename (), s
)
324 class LilypondSnippet (Snippet
):
325 def __init__ (self
, type, match
, formatter
, line_number
, global_options
):
326 Snippet
.__init
__ (self
, type, match
, formatter
, line_number
, global_options
)
327 os
= match
.group ('options')
328 self
.do_options (os
, self
.type)
331 def snippet_options (self
):
334 def verb_ly_gettext (self
, s
):
335 lang
= self
.formatter
.document_language
339 t
= langdefs
.translation
[lang
]
343 s
= ly_comment_re
.sub (lambda m
: ly_comment_gettext (t
, m
), s
)
345 if langdefs
.LANGDICT
[lang
].enable_ly_identifier_l10n
:
346 for v
in ly_var_def_re
.findall (s
):
347 s
= re
.sub (r
"(?m)(?<!\\clef)(^|[' \\#])%s([^a-zA-Z])" % v
,
348 "\\1" + t (v
) + "\\2",
350 for id in ly_context_id_re
.findall (s
):
351 s
= re
.sub (r
'(\s+|")%s(\s+|")' % id,
352 "\\1" + t (id) + "\\2",
357 verb_text
= self
.substring ('code')
358 if not NOGETTEXT
in self
.option_dict
:
359 verb_text
= self
.verb_ly_gettext (verb_text
)
360 if not verb_text
.endswith ('\n'):
365 contents
= self
.substring ('code')
366 return ('\\sourcefileline %d\n%s'
367 % (self
.line_number
- 1, contents
))
372 return self
.compose_ly (s
)
375 def split_options (self
, option_string
):
376 return self
.formatter
.split_snippet_options (option_string
);
378 def do_options (self
, option_string
, type):
379 self
.option_dict
= {}
381 options
= self
.split_options (option_string
)
383 for option
in options
:
385 (key
, value
) = re
.split ('\s*=\s*', option
)
386 self
.option_dict
[key
] = value
388 if option
in no_options
:
389 if no_options
[option
] in self
.option_dict
:
390 del self
.option_dict
[no_options
[option
]]
392 self
.option_dict
[option
] = None
395 # If LINE_WIDTH is used without parameter, set it to default.
396 has_line_width
= self
.option_dict
.has_key (LINE_WIDTH
)
397 if has_line_width
and self
.option_dict
[LINE_WIDTH
] == None:
398 has_line_width
= False
399 del self
.option_dict
[LINE_WIDTH
]
401 # TODO: Can't we do that more efficiently (built-in python func?)
402 for k
in self
.formatter
.default_snippet_options
:
403 if k
not in self
.option_dict
:
404 self
.option_dict
[k
] = self
.formatter
.default_snippet_options
[k
]
406 # RELATIVE does not work without FRAGMENT;
407 # make RELATIVE imply FRAGMENT
408 has_relative
= self
.option_dict
.has_key (RELATIVE
)
409 if has_relative
and not self
.option_dict
.has_key (FRAGMENT
):
410 self
.option_dict
[FRAGMENT
] = None
412 if not has_line_width
:
413 if type == 'lilypond' or FRAGMENT
in self
.option_dict
:
414 self
.option_dict
[RAGGED_RIGHT
] = None
416 if type == 'lilypond':
417 if LINE_WIDTH
in self
.option_dict
:
418 del self
.option_dict
[LINE_WIDTH
]
420 if RAGGED_RIGHT
in self
.option_dict
:
421 if LINE_WIDTH
in self
.option_dict
:
422 del self
.option_dict
[LINE_WIDTH
]
424 if QUOTE
in self
.option_dict
or type == 'lilypond':
425 if LINE_WIDTH
in self
.option_dict
:
426 del self
.option_dict
[LINE_WIDTH
]
428 if not INDENT
in self
.option_dict
:
429 self
.option_dict
[INDENT
] = '0\\mm'
431 # Set a default line-width if there is none. We need this, because
432 # lilypond-book has set left-padding by default and therefore does
433 # #(define line-width (- line-width (* 3 mm)))
434 # TODO: Junk this ugly hack if the code gets rewritten to concatenate
435 # all settings before writing them in the \paper block.
436 if not LINE_WIDTH
in self
.option_dict
:
437 if not QUOTE
in self
.option_dict
:
438 if not LILYQUOTE
in self
.option_dict
:
439 self
.option_dict
[LINE_WIDTH
] = "#(- paper-width \
440 left-margin-default right-margin-default)"
442 def get_option_list (self
):
443 if not 'option_list' in self
.__dict
__:
445 for (key
, value
) in self
.option_dict
.items ():
447 option_list
.append (key
)
449 option_list
.append (key
+ '=' + value
)
451 self
.option_list
= option_list
452 return self
.option_list
454 def compose_ly (self
, code
):
455 if FRAGMENT
in self
.option_dict
:
463 # The original concept of the `exampleindent' option is broken.
464 # It is not possible to get a sane value for @exampleindent at all
465 # without processing the document itself. Saying
473 # causes ugly results with the DVI backend of texinfo since the
474 # default value for @exampleindent isn't 5em but 0.4in (or a smaller
475 # value). Executing the above code changes the environment
476 # indentation to an unknown value because we don't know the amount
477 # of 1em in advance since it is font-dependent. Modifying
478 # @exampleindent in the middle of a document is simply not
479 # supported within texinfo.
481 # As a consequence, the only function of @exampleindent is now to
482 # specify the amount of indentation for the `quote' option.
484 # To set @exampleindent locally to zero, we use the @format
485 # environment for non-quoted snippets.
486 override
[EXAMPLEINDENT
] = r
'0.4\in'
487 override
[LINE_WIDTH
] = '5\\in' # = texinfo_line_widths['@smallbook']
488 override
.update (self
.formatter
.default_snippet_options
)
491 for option
in self
.get_option_list ():
492 for name
in PROCESSING_INDEPENDENT_OPTIONS
:
493 if option
.startswith (name
):
496 option_list
.append (option
)
497 option_string
= ','.join (option_list
)
499 compose_types
= [NOTES
, PREAMBLE
, LAYOUT
, PAPER
]
500 for a
in compose_types
:
503 option_names
= self
.option_dict
.keys ()
505 for key
in option_names
:
506 value
= self
.option_dict
[key
]
507 (c_key
, c_value
) = classic_lilypond_book_compatibility (key
, value
)
511 _ ("deprecated ly-option used: %s=%s") % (key
, value
))
513 _ ("compatibility mode translation: %s=%s") % (c_key
, c_value
))
516 _ ("deprecated ly-option used: %s") % key
)
518 _ ("compatibility mode translation: %s") % c_key
)
520 (key
, value
) = (c_key
, c_value
)
523 override
[key
] = value
525 if not override
.has_key (key
):
529 for typ
in compose_types
:
530 if snippet_options
[typ
].has_key (key
):
531 compose_dict
[typ
].append (snippet_options
[typ
][key
])
535 if not found
and key
not in simple_options
and key
not in self
.snippet_options ():
536 warning (_ ("ignoring unknown ly option: %s") % key
)
539 if RELATIVE
in override
and override
[RELATIVE
]:
540 relative
= int (override
[RELATIVE
])
546 relative_quotes
+= ',' * (- relative
)
548 relative_quotes
+= "'" * relative
550 paper_string
= '\n '.join (compose_dict
[PAPER
]) % override
551 layout_string
= '\n '.join (compose_dict
[LAYOUT
]) % override
552 notes_string
= '\n '.join (compose_dict
[NOTES
]) % vars ()
553 preamble_string
= '\n '.join (compose_dict
[PREAMBLE
]) % override
554 padding_mm
= self
.global_options
.padding_mm
558 d
.update (self
.global_options
.information
)
559 return (PREAMBLE_LY
+ body
) % d
561 def get_checksum (self
):
562 if not self
.checksum
:
563 # Work-around for md5 module deprecation warning in python 2.5+:
565 from hashlib
import md5
569 # We only want to calculate the hash based on the snippet
570 # code plus fragment options relevant to processing by
571 # lilypond, not the snippet + preamble
572 hash = md5 (self
.relevant_contents (self
.ly ()))
573 for option
in self
.get_option_list ():
574 for name
in PROCESSING_INDEPENDENT_OPTIONS
:
575 if option
.startswith (name
):
580 ## let's not create too long names.
581 self
.checksum
= hash.hexdigest ()[:10]
586 cs
= self
.get_checksum ()
587 name
= '%s/lily-%s' % (cs
[:2], cs
[2:])
590 final_basename
= basename
593 base
= self
.basename ()
594 path
= os
.path
.join (self
.global_options
.lily_output_dir
, base
)
595 directory
= os
.path
.split(path
)[0]
596 if not os
.path
.isdir (directory
):
597 os
.makedirs (directory
)
598 filename
= path
+ '.ly'
599 if os
.path
.exists (filename
):
600 diff_against_existing
= self
.filter_pipe (self
.full_ly (), 'diff -u %s -' % filename
)
601 if diff_against_existing
:
602 warning ("%s: duplicate filename but different contents of orginal file,\n\
603 printing diff against existing file." % filename
)
604 ly
.stderr_write (diff_against_existing
)
606 out
= file (filename
, 'w')
607 out
.write (self
.full_ly ())
608 file (path
+ '.txt', 'w').write ('image of music')
610 def relevant_contents (self
, ly
):
611 return re
.sub (r
'\\(version|sourcefileline|sourcefilename)[^\n]*\n', '', ly
)
613 def link_all_output_files (self
, output_dir
, output_dir_files
, destination
):
614 existing
, missing
= self
.all_output_files (output_dir
, output_dir_files
)
616 print '\nMissing', missing
617 raise CompileError(self
.basename())
618 for name
in existing
:
619 if (self
.global_options
.use_source_file_names
620 and isinstance (self
, LilypondFileSnippet
)):
621 base
, ext
= os
.path
.splitext (name
)
622 components
= base
.split ('-')
623 # ugh, assume filenames with prefix with one dash (lily-xxxx)
624 if len (components
) > 2:
625 base_suffix
= '-' + components
[-1]
628 final_name
= self
.final_basename () + base_suffix
+ ext
632 os
.unlink (os
.path
.join (destination
, final_name
))
636 src
= os
.path
.join (output_dir
, name
)
637 dst
= os
.path
.join (destination
, final_name
)
638 dst_path
= os
.path
.split(dst
)[0]
639 if not os
.path
.isdir (dst_path
):
640 os
.makedirs (dst_path
)
644 def all_output_files (self
, output_dir
, output_dir_files
):
645 """Return all files generated in lily_output_dir, a set.
647 output_dir_files is the list of files in the output directory.
651 base
= self
.basename()
652 full
= os
.path
.join (output_dir
, base
)
653 def consider_file (name
):
654 if name
in output_dir_files
:
657 def require_file (name
):
658 if name
in output_dir_files
:
663 # UGH - junk self.global_options
664 skip_lily
= self
.global_options
.skip_lilypond_run
665 for required
in [base
+ '.ly',
667 require_file (required
)
669 require_file (base
+ '-systems.count')
671 if 'ddump-profile' in self
.global_options
.process_cmd
:
672 require_file (base
+ '.profile')
673 if 'dseparate-log-file' in self
.global_options
.process_cmd
:
674 require_file (base
+ '.log')
676 map (consider_file
, [base
+ '.tex',
680 base
+ '-systems.texi',
681 base
+ '-systems.tex',
682 base
+ '-systems.pdftexi'])
683 if self
.formatter
.document_language
:
685 [base
+ '.texidoc' + self
.formatter
.document_language
,
686 base
+ '.doctitle' + self
.formatter
.document_language
])
688 required_files
= self
.formatter
.required_files (self
, base
, full
, result
)
689 for f
in required_files
:
693 if not skip_lily
and not missing
:
694 system_count
= int(file (full
+ '-systems.count').read())
696 for number
in range(1, system_count
+ 1):
697 systemfile
= '%s-%d' % (base
, number
)
698 require_file (systemfile
+ '.eps')
699 consider_file (systemfile
+ '.pdf')
701 # We can't require signatures, since books and toplevel
702 # markups do not output a signature.
703 if 'ddump-signature' in self
.global_options
.process_cmd
:
704 consider_file (systemfile
+ '.signature')
707 return (result
, missing
)
709 def is_outdated (self
, output_dir
, current_files
):
710 found
, missing
= self
.all_output_files (output_dir
, current_files
)
713 def filter_pipe (self
, input, cmd
):
714 """Pass input through cmd, and return the result."""
716 if self
.global_options
.verbose
:
717 progress (_ ("Opening filter `%s'\n") % cmd
)
719 #(stdin, stdout, stderr) = os.popen3 (cmd)
721 p
= Popen(cmd
, shell
=True, stdin
=PIPE
, stdout
=PIPE
, stderr
=PIPE
, close_fds
=True)
722 (stdin
, stdout
, stderr
) = (p
.stdin
, p
.stdout
, p
.stderr
)
724 status
= stdin
.close ()
728 output
= stdout
.read ()
729 status
= stdout
.close ()
730 error
= stderr
.read ()
734 signal
= 0x0f & status
735 if status
or (not output
and error
):
736 exit_status
= status
>> 8
737 error (_ ("`%s' failed (%d)") % (cmd
, exit_status
))
738 error (_ ("The error log is as follows:"))
739 ly
.stderr_write (error
)
740 ly
.stderr_write (stderr
.read ())
743 if self
.global_options
.verbose
:
748 def get_snippet_code (self
):
749 return self
.substring ('code');
751 def filter_text (self
):
752 """Run snippet bodies through a command (say: convert-ly).
754 This functionality is rarely used, and this code must have bitrot.
756 code
= self
.get_snippet_code ();
757 s
= self
.filter_pipe (code
, self
.global_options
.filter_cmd
)
760 'options': self
.match
.group ('options')
762 return self
.formatter
.output_simple_replacements (FILTER
, d
)
764 def replacement_text (self
):
765 base
= self
.final_basename ()
766 return self
.formatter
.snippet_output (base
, self
)
768 def get_images (self
):
769 rep
= {'base': self
.final_basename ()}
771 single
= '%(base)s.png' % rep
772 multiple
= '%(base)s-page1.png' % rep
774 if (os
.path
.exists (multiple
)
775 and (not os
.path
.exists (single
)
776 or (os
.stat (multiple
)[stat
.ST_MTIME
]
777 > os
.stat (single
)[stat
.ST_MTIME
]))):
778 count
= ps_page_count ('%(base)s.eps' % rep
)
779 images
= ['%s-page%d.png' % (rep
['base'], page
) for page
in range (1, count
+1)]
780 images
= tuple (images
)
786 re_begin_verbatim
= re
.compile (r
'\s+%.*?begin verbatim.*\n*', re
.M
)
787 re_end_verbatim
= re
.compile (r
'\s+%.*?end verbatim.*$', re
.M
)
789 class LilypondFileSnippet (LilypondSnippet
):
790 def __init__ (self
, type, match
, formatter
, line_number
, global_options
):
791 LilypondSnippet
.__init
__ (self
, type, match
, formatter
, line_number
, global_options
)
792 self
.contents
= file (BookBase
.find_file (self
.substring ('filename'), global_options
.include_path
)).read ()
794 def get_snippet_code (self
):
795 return self
.contents
;
799 s
= re_begin_verbatim
.split (s
)[-1]
800 s
= re_end_verbatim
.split (s
)[0]
801 if not NOGETTEXT
in self
.option_dict
:
802 s
= self
.verb_ly_gettext (s
)
803 if not s
.endswith ('\n'):
808 name
= self
.substring ('filename')
809 return ('\\sourcefilename \"%s\"\n\\sourcefileline 0\n%s'
810 % (name
, self
.contents
))
812 def final_basename (self
):
813 if self
.global_options
.use_source_file_names
:
814 base
= os
.path
.splitext (os
.path
.basename (self
.substring ('filename')))[0]
817 return self
.basename ()
820 class LilyPondVersionString (Snippet
):
821 """A string that does not require extra memory."""
822 def __init__ (self
, type, match
, formatter
, line_number
, global_options
):
823 Snippet
.__init
__ (self
, type, match
, formatter
, line_number
, global_options
)
825 def replacement_text (self
):
826 return self
.formatter
.output_simple (self
.type, self
)
829 snippet_type_to_class
= {
830 'lilypond_file': LilypondFileSnippet
,
831 'lilypond_block': LilypondSnippet
,
832 'lilypond': LilypondSnippet
,
833 'include': IncludeSnippet
,
834 'lilypondversion': LilyPondVersionString
,