]> git.lyx.org Git - features.git/blob - lib/scripts/docbook_copy.py
DocBook: make LilyPond work more reliably.
[features.git] / lib / scripts / docbook_copy.py
1 # -*- coding: utf-8 -*-
2
3 # file docbook_copy.py
4 # This file is part of LyX, the document processor.
5 # Licence details can be found in the file COPYING.
6 #
7 # \author Thibaut Cuvelier
8 #
9 # Full author contact details are available in file CREDITS
10
11 # Usage:
12 #   python docbook_copy.py lilypond_book_command in.docbook out.docbook
13 # This script copies the original DocBook file (directly produced by LyX) to the output DocBook file,
14 # potentially applying a post-processing step. For now, the only implemented post-processing step is
15 # LilyPond.
16 # lilypond_book_command is either directly the binary to call OR the equivalent Python script that is
17 # not directly executable.
18 # /!\ The original file may be modified by this script!
19
20
21 import subprocess
22 import os
23 import os.path
24 import re
25 import shutil
26 import sys
27
28
29 def need_lilypond(file):
30     # Really tailored to the kind of output lilypond.module makes (in lib/layouts).
31     with open(file, 'r') as f:
32         return "language='lilypond'" in f.read()
33
34
35 def copy_docbook(args):
36     if len(args) != 4:
37         print('Exactly four arguments are expected, only %s found: %s.' % (len(args), args))
38         sys.exit(1)
39
40     # Parse the command line.
41     lilypond_command = args[1]
42     in_file = args[2]
43     out_file = args[3]
44
45     has_lilypond = lilypond_command not in {'', 'none'}
46     in_folder = os.path.split(in_file)[0]
47
48     # Guess the path for LilyPond.
49     lilypond_folder = os.path.split(lilypond_command)[0] if has_lilypond else ''
50
51     # Help debugging.
52     print('>> Given arguments:')
53     print('>> LilyPond: ' + ('present' if has_lilypond else 'not found') + '.')
54     print('>> LilyPond callable as: ' + lilypond_command + '.')
55     print('>> LilyPond path: ' + lilypond_folder + '.')
56     print('>> Input file: ' + in_file + '.')
57     print('>> Input folder: ' + in_folder + '.')
58     print('>> Output file: ' + out_file + '.')
59
60     # Apply LilyPond to the original file if available and needed.
61     if has_lilypond and need_lilypond(in_file):
62         in_lily_file = in_file.replace(".xml", ".lyxml")
63         print('>> The input file needs a LilyPond pass and LilyPond is available.')
64         print('>> Rewriting ' + in_file)
65         print('>> as ' + in_lily_file + '.')
66
67         # LilyPond requires that its input file has the .lyxml extension. Due to a bug in LilyPond,
68         # use " instead of ' to encode XML attributes.
69         # https://lists.gnu.org/archive/html/bug-lilypond/2021-09/msg00039.html
70         # Typical transformation:
71         #     FROM:  language='lilypond' role='fragment verbatim staffsize=16 ragged-right relative=2'
72         #     TO:    language="lilypond" role="fragment verbatim staffsize=16 ragged-right relative=2"
73         with open(in_file, 'r', encoding='utf-8') as f, open(in_lily_file, 'w', encoding='utf-8') as f_lily:
74             for line in f:
75                 if "language='lilypond'" in line:
76                     line = re.sub(
77                         '<programlisting\\s+language=\'lilypond\'.*?(role=\'(?P<options>.*?)\')?>',
78                         '<programlisting language="lilypond" role="\\g<options>">',
79                         line
80                     )
81                 f_lily.write(line)
82         os.unlink(in_file)
83
84         # Add LilyPond to the PATH. lilypond-book uses a direct call to lilypond from the PATH.
85         if os.path.isdir(lilypond_folder):
86             os.environ['PATH'] += os.pathsep + lilypond_folder
87
88         # Make LilyPond believe it is working from the temporary LyX directory. Otherwise, it tries to find files
89         # starting from LyX's working directory...
90         os.chdir(in_folder)
91
92         # Start LilyPond on the copied file. First test the binary, then check if adding Python helps.
93         command_args = ['--format=docbook', '--output=' + in_folder, in_lily_file]
94         command_raw = [lilypond_command] + command_args
95         command_python = ['python', lilypond_command] + command_args
96
97         print('>> Running LilyPond.')
98         sys.stdout.flush()  # So that the LilyPond output is at the right place in the logs.
99
100         failed = False
101         try:
102             subprocess.check_call(command_raw, stdout=sys.stdout.fileno(), stderr=sys.stdout.fileno())
103             print('>> Success running LilyPond with ')
104             print('>> ' + str(command_raw))
105         except (subprocess.CalledProcessError, OSError) as e1:
106             try:
107                 subprocess.check_call(command_python, stdout=sys.stdout.fileno(), stderr=sys.stdout.fileno())
108                 print('>> Success running LilyPond with ')
109                 print('>> ' + str(command_python) + '.')
110             except (subprocess.CalledProcessError, OSError) as e2:
111                 print('>> Error from LilyPond. The successive calls were:')
112                 print('>> (1) Error from trying ' + str(command_raw) + ':')
113                 print('>> (1) ' + str(e1))
114                 print('>> (2) Error from trying ' + str(command_python) + ':')
115                 print('>> (2) ' + str(e2))
116                 failed = True
117
118         if failed:
119             sys.exit(1)
120
121         # Now, in_file should have the LilyPond-processed contents.
122
123     # Perform the final copy.
124     shutil.copyfile(in_file, out_file, follow_symlinks=False)
125
126
127 if __name__ == '__main__':
128     copy_docbook(sys.argv)