]> git.lyx.org Git - features.git/blob - lib/scripts/docbook_copy.py
Update layouts (run layout2layout.py)
[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 class DocBookCopier:
30     def __init__(self, args):
31
32         # Parse the command line.
33         self.lilypond_command = args[1]
34         self.in_file = args[2]
35         self.out_file = args[3]
36
37         # Compute a few things from the raw parameters.
38         self.in_folder = os.path.split(self.in_file)[0]
39         self.out_folder = os.path.split(self.out_file)[0]
40
41         self.in_lily_file = self.in_file.replace('.xml', '.lyxml')
42         self.has_lilypond = self.lilypond_command not in {'', 'none'}
43         self.lilypond_folder = os.path.split(self.lilypond_command)[0] if self.has_lilypond else ''
44         self.do_lilypond_processing = self.has_lilypond and self.in_file_needs_lilypond()
45
46         # Help debugging.
47         print('>> Given arguments:')
48         print('>> LilyPond: ' + ('present' if self.has_lilypond else 'not found') + '.')
49         print('>> LilyPond callable as: ' + self.lilypond_command + '.')
50         print('>> LilyPond path: ' + self.lilypond_folder + '.')
51         print('>> Input file: ' + self.in_file + '.')
52         print('>> Output file: ' + self.out_file + '.')
53         print('>> Input folder: ' + self.in_folder + '.')
54         print('>> Output folder: ' + self.out_folder + '.')
55
56     def in_file_needs_lilypond(self):
57         # Really tailored to the kind of output lilypond.module makes (in lib/layouts).
58         with open(self.in_file, 'r') as f:
59             return "language='lilypond'" in f.read()
60
61     def preprocess_input_for_lilypond(self):
62         # LilyPond requires that its input file has the .lyxml extension. Due to a bug in LilyPond,
63         # use " instead of ' to encode XML attributes.
64         # Bug report: https://gitlab.com/lilypond/lilypond/-/issues/6185
65         # Typical transformation:
66         # Fixed by 2.23.4.
67         #     FROM:  language='lilypond' role='fragment verbatim staffsize=16 ragged-right relative=2'
68         #     TO:    language="lilypond" role="fragment verbatim staffsize=16 ragged-right relative=2"
69         with open(self.in_file, 'r', encoding='utf-8') as f, open(self.in_lily_file, 'w', encoding='utf-8') as f_lily:
70             for line in f:
71                 if "language='lilypond'" in line:
72                     line = re.sub(
73                         '<programlisting\\s+language=\'lilypond\'.*?(role=\'(?P<options>.*?)\')?>',
74                         '<programlisting language="lilypond" role="\\g<options>">',
75                         line
76                     )
77                 f_lily.write(line)
78         os.unlink(self.in_file)
79
80     def postprocess_output_for_lilypond(self):
81         # Major problem: LilyPond used to output the LilyPond code outside the image, which is then always displayed
82         # before the image (instead of only the generated image).
83         # Bug report: https://gitlab.com/lilypond/lilypond/-/issues/6186
84         # No more necessary with the new version of LilyPond (2.23.4). No efficient way to decide how to post-process
85         # for previous versions of LilyPond. Basically, it does not make sense to post-process.
86         pass
87
88     def call_lilypond(self):
89         # LilyPond requires that its input file has the .lyxml extension (plus bugs in LilyPond).
90         print('>> Rewriting ' + self.in_file)
91         print('>> as ' + self.in_lily_file + '.')
92         self.preprocess_input_for_lilypond()
93
94         # Add LilyPond to the PATH. lilypond-book uses a direct call to lilypond from the PATH.
95         if os.path.isdir(self.lilypond_folder):
96             os.environ['PATH'] += os.pathsep + self.lilypond_folder
97
98         # Make LilyPond believe it is working from the temporary LyX directory. Otherwise, it tries to find files
99         # starting from LyX's working directory... LilyPond bug, most likely.
100         # https://lists.gnu.org/archive/html/bug-lilypond/2021-09/msg00041.html
101         os.chdir(self.in_folder)
102
103         # Start LilyPond on the copied file. First test the binary, then check if adding Python helps.
104         command_args = ['--format=docbook', '--output=' + self.in_folder, self.in_lily_file]
105         command_raw = [self.lilypond_command] + command_args
106         command_python = ['python', self.lilypond_command] + command_args
107
108         print('>> Running LilyPond.')
109         sys.stdout.flush()  # So that the LilyPond output is at the right place in the logs.
110
111         failed = True
112         exceptions = []
113         for cmd in [command_raw, command_python]:
114             try:
115                 subprocess.check_call(cmd, stdout=sys.stdout.fileno(), stderr=sys.stdout.fileno())
116                 print('>> Success running LilyPond with ')
117                 print('>> ' + str(cmd))
118                 failed = False
119             except (subprocess.CalledProcessError, OSError) as e:
120                 exceptions.append((cmd, e))
121
122         if failed:
123             print('>> Error from LilyPond. The successive calls were:')
124             for (i, pair) in enumerate(exceptions):
125                 exc = pair[0]
126                 cmd = pair[1]
127
128                 print('>> (' + i + ') Error from trying ' + str(cmd) + ':')
129                 print('>> (' + i + ') ' + str(exc))
130
131         if failed:
132             sys.exit(1)
133
134         # LilyPond has a distressing tendency to leave the raw LilyPond code in the new file.
135         self.postprocess_output_for_lilypond()
136
137         # Now, in_file should have the clean LilyPond-processed contents.
138
139     def copy_lilypond_generated_images(self):
140         # LilyPond generates a lot of files in LyX' temporary folder, within the ff folder: source LilyPond files
141         # for each snippet to render, images in several formats.
142         in_generated_images_folder = os.path.join(self.in_folder, 'ff')
143         out_generated_images_folder = os.path.join(self.out_folder, 'ff')
144
145         if not os.path.isdir(out_generated_images_folder):
146             os.mkdir(out_generated_images_folder)
147
148         for img in os.listdir(in_generated_images_folder):
149             if not img.endswith('.png') and not img.endswith('.pdf'):
150                 continue
151
152             shutil.copyfile(
153                 os.path.join(in_generated_images_folder, img),
154                 os.path.join(out_generated_images_folder, img),
155             )
156
157     def copy(self):
158         # Apply LilyPond to the original file if available and needed.
159         if self.do_lilypond_processing:
160             print('>> The input file needs a LilyPond pass and LilyPond is available.')
161             self.call_lilypond()
162
163         # Perform the actual copy: both the modified XML file and the generated images, if LilyPond is used.
164         shutil.copyfile(self.in_file, self.out_file)
165         if self.do_lilypond_processing:
166             self.copy_lilypond_generated_images()
167
168
169 if __name__ == '__main__':
170     if len(sys.argv) != 4:
171         print('Exactly four arguments are expected, only %s found: %s.' % (len(sys.argv), sys.argv))
172         sys.exit(1)
173
174     DocBookCopier(sys.argv).copy()