Fix #1194.
[lilypond/mpolesky.git] / python / book_snippets.py
blobfa4d97ccec6bb4d82db7ae96f7db30dbafdc2862
1 # -*- coding: utf-8 -*-
3 import book_base as BookBase
4 import lilylib as ly
5 global _;_=ly._
6 import re
7 import os
8 import copy
9 from subprocess import Popen, PIPE
11 progress = ly.progress
12 warning = ly.warning
13 error = ly.error
19 ####################################################################
20 # Snippet option handling
21 ####################################################################
25 # Is this pythonic? Personally, I find this rather #define-nesque. --hwn
27 # Global definitions:
28 ADDVERSION = 'addversion'
29 AFTER = 'after'
30 ALT = 'alt'
31 BEFORE = 'before'
32 DOCTITLE = 'doctitle'
33 EXAMPLEINDENT = 'exampleindent'
34 FILENAME = 'filename'
35 FILTER = 'filter'
36 FRAGMENT = 'fragment'
37 LANG = 'lang' ## TODO: This is handled nowhere!
38 LAYOUT = 'layout'
39 LILYQUOTE = 'lilyquote'
40 LINE_WIDTH = 'line-width'
41 NOFRAGMENT = 'nofragment'
42 NOGETTEXT = 'nogettext'
43 NOINDENT = 'noindent'
44 NOQUOTE = 'noquote'
45 INDENT = 'indent'
46 NORAGGED_RIGHT = 'noragged-right'
47 NOTES = 'body'
48 NOTIME = 'notime'
49 OUTPUT = 'output'
50 OUTPUTIMAGE = 'outputimage'
51 PAPER = 'paper'
52 PREAMBLE = 'preamble'
53 PRINTFILENAME = 'printfilename'
54 QUOTE = 'quote'
55 RAGGED_RIGHT = 'ragged-right'
56 RELATIVE = 'relative'
57 STAFFSIZE = 'staffsize'
58 TEXIDOC = 'texidoc'
59 VERBATIM = 'verbatim'
60 VERSION = 'lilypondversion'
64 # NOTIME and NOGETTEXT have no opposite so they aren't part of this
65 # dictionary.
66 # NOQUOTE is used internally only.
67 no_options = {
68 NOFRAGMENT: FRAGMENT,
69 NOINDENT: INDENT,
72 # Options that have no impact on processing by lilypond (or --process
73 # argument)
74 PROCESSING_INDEPENDENT_OPTIONS = (
75 ALT, NOGETTEXT, VERBATIM, ADDVERSION,
76 TEXIDOC, DOCTITLE, VERSION, PRINTFILENAME)
80 # Options without a pattern in snippet_options.
81 simple_options = [
82 EXAMPLEINDENT,
83 FRAGMENT,
84 NOFRAGMENT,
85 NOGETTEXT,
86 NOINDENT,
87 PRINTFILENAME,
88 DOCTITLE,
89 TEXIDOC,
90 LANG,
91 VERBATIM,
92 FILENAME,
93 ALT,
94 ADDVERSION
99 ####################################################################
100 # LilyPond templates for the snippets
101 ####################################################################
103 snippet_options = {
105 NOTES: {
106 RELATIVE: r'''\relative c%(relative_quotes)s''',
110 PAPER: {
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''',
120 LAYOUT: {
121 NOTIME: r'''
122 \context {
123 \Score
124 timing = ##f
126 \context {
127 \Staff
128 \remove "Time_signature_engraver"
129 }''',
133 PREAMBLE: {
134 STAFFSIZE: r'''#(set-global-staff-size %(staffsize)s)''',
142 FRAGMENT_LY = r'''
143 %(notes_string)s
147 %% ****************************************************************
148 %% ly snippet contents follows:
149 %% ****************************************************************
150 %(code)s
153 %% ****************************************************************
154 %% end ly snippet
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)
164 if m:
165 return ('relative', m.group (1))
167 m = re.match ('([0-9]+)pt', key)
168 if m:
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)
173 if m:
174 f = float (m.group (1))
175 return (key, '%f\\%s' % (f, m.group (2)))
177 return (None, None)
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 %% ****************************************************************
189 %(preamble_string)s
191 \paper {
192 %(paper_string)s
193 force-assignment = #""
194 line-width = #(- line-width (* mm %(padding_mm)f))
197 \layout {
198 %(layout_string)s
203 FULL_LY = '''
206 %% ****************************************************************
207 %% ly snippet:
208 %% ****************************************************************
209 %(code)s
212 %% ****************************************************************
213 %% end ly snippet
214 %% ****************************************************************
224 ####################################################################
225 # Helper functions
226 ####################################################################
228 def ps_page_count (ps_name):
229 header = file (ps_name).read (1024)
230 m = re.search ('\n%%Pages: ([0-9]+)', header)
231 if m:
232 return int (m.group (1))
233 return 0
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):
246 pass
250 ####################################################################
251 # Snippet classes
252 ####################################################################
254 class Chunk:
255 def replacement_text (self):
256 return ''
258 def filter_text (self):
259 return self.replacement_text ()
261 def is_plain (self):
262 return False
264 class Substring (Chunk):
265 """A string that does not require extra memory."""
266 def __init__ (self, source, start, end, line_number):
267 self.source = source
268 self.start = start
269 self.end = end
270 self.line_number = line_number
271 self.override_text = None
273 def is_plain (self):
274 return True
276 def replacement_text (self):
277 if self.override_text:
278 return self.override_text
279 else:
280 return self.source[self.start:self.end]
284 class Snippet (Chunk):
285 def __init__ (self, type, match, formatter, line_number, global_options):
286 self.type = type
287 self.match = match
288 self.checksum = 0
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)
307 def __repr__ (self):
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):
332 return [];
334 def verb_ly_gettext (self, s):
335 lang = self.formatter.document_language
336 if not lang:
337 return s
338 try:
339 t = langdefs.translation[lang]
340 except:
341 return s
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",
354 return s
356 def verb_ly (self):
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'):
361 verb_text += '\n'
362 return verb_text
364 def ly (self):
365 contents = self.substring ('code')
366 return ('\\sourcefileline %d\n%s'
367 % (self.line_number - 1, contents))
369 def full_ly (self):
370 s = self.ly ()
371 if s:
372 return self.compose_ly (s)
373 return ''
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:
384 if '=' in option:
385 (key, value) = re.split ('\s*=\s*', option)
386 self.option_dict[key] = value
387 else:
388 if option in no_options:
389 if no_options[option] in self.option_dict:
390 del self.option_dict[no_options[option]]
391 else:
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]
419 else:
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__:
444 option_list = []
445 for (key, value) in self.option_dict.items ():
446 if value == None:
447 option_list.append (key)
448 else:
449 option_list.append (key + '=' + value)
450 option_list.sort ()
451 self.option_list = option_list
452 return self.option_list
454 def compose_ly (self, code):
455 if FRAGMENT in self.option_dict:
456 body = FRAGMENT_LY
457 else:
458 body = FULL_LY
460 # Defaults.
461 relative = 1
462 override = {}
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
467 # @exampleindent 0
468 # @example
469 # ...
470 # @end example
471 # @exampleindent 5
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)
490 option_list = []
491 for option in self.get_option_list ():
492 for name in PROCESSING_INDEPENDENT_OPTIONS:
493 if option.startswith (name):
494 break
495 else:
496 option_list.append (option)
497 option_string = ','.join (option_list)
498 compose_dict = {}
499 compose_types = [NOTES, PREAMBLE, LAYOUT, PAPER]
500 for a in compose_types:
501 compose_dict[a] = []
503 option_names = self.option_dict.keys ()
504 option_names.sort ()
505 for key in option_names:
506 value = self.option_dict[key]
507 (c_key, c_value) = classic_lilypond_book_compatibility (key, value)
508 if c_key:
509 if c_value:
510 warning (
511 _ ("deprecated ly-option used: %s=%s") % (key, value))
512 warning (
513 _ ("compatibility mode translation: %s=%s") % (c_key, c_value))
514 else:
515 warning (
516 _ ("deprecated ly-option used: %s") % key)
517 warning (
518 _ ("compatibility mode translation: %s") % c_key)
520 (key, value) = (c_key, c_value)
522 if value:
523 override[key] = value
524 else:
525 if not override.has_key (key):
526 override[key] = None
528 found = 0
529 for typ in compose_types:
530 if snippet_options[typ].has_key (key):
531 compose_dict[typ].append (snippet_options[typ][key])
532 found = 1
533 break
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)
538 # URGS
539 if RELATIVE in override and override[RELATIVE]:
540 relative = int (override[RELATIVE])
542 relative_quotes = ''
544 # 1 = central C
545 if relative < 0:
546 relative_quotes += ',' * (- relative)
547 elif relative > 0:
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
556 d = globals().copy()
557 d.update (locals())
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+:
564 try:
565 from hashlib import md5
566 except ImportError:
567 from md5 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):
576 break
577 else:
578 hash.update (option)
580 ## let's not create too long names.
581 self.checksum = hash.hexdigest ()[:10]
583 return self.checksum
585 def basename (self):
586 cs = self.get_checksum ()
587 name = '%s/lily-%s' % (cs[:2], cs[2:])
588 return name
590 final_basename = basename
592 def write_ly (self):
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)
605 else:
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)
615 if missing:
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]
626 else:
627 base_suffix = ''
628 final_name = self.final_basename () + base_suffix + ext
629 else:
630 final_name = name
631 try:
632 os.unlink (os.path.join (destination, final_name))
633 except OSError:
634 pass
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)
641 os.link (src, dst)
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.
649 result = set ()
650 missing = set ()
651 base = self.basename()
652 full = os.path.join (output_dir, base)
653 def consider_file (name):
654 if name in output_dir_files:
655 result.add (name)
657 def require_file (name):
658 if name in output_dir_files:
659 result.add (name)
660 else:
661 missing.add (name)
663 # UGH - junk self.global_options
664 skip_lily = self.global_options.skip_lilypond_run
665 for required in [base + '.ly',
666 base + '.txt']:
667 require_file (required)
668 if not skip_lily:
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',
677 base + '.eps',
678 base + '.texidoc',
679 base + '.doctitle',
680 base + '-systems.texi',
681 base + '-systems.tex',
682 base + '-systems.pdftexi'])
683 if self.formatter.document_language:
684 map (consider_file,
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:
690 require_file (f)
692 system_count = 0
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)
711 return missing
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)
723 stdin.write (input)
724 status = stdin.close ()
726 if not status:
727 status = 0
728 output = stdout.read ()
729 status = stdout.close ()
730 error = stderr.read ()
732 if not status:
733 status = 0
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 ())
741 exit (status)
743 if self.global_options.verbose:
744 progress ('\n')
746 return output
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)
758 d = {
759 'code': s,
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
773 images = (single,)
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)
782 return 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;
797 def verb_ly (self):
798 s = 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'):
804 s += '\n'
805 return s
807 def ly (self):
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]
815 return base
816 else:
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,