]> git.lyx.org Git - lyx.git/blob - lib/scripts/docbook_copy.py
d2e4fe005857f05986564268d19cee83b8aed3ba
[lyx.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         # https://lists.gnu.org/archive/html/bug-lilypond/2021-09/msg00039.html
65         # Typical transformation:
66         #     FROM:  language='lilypond' role='fragment verbatim staffsize=16 ragged-right relative=2'
67         #     TO:    language="lilypond" role="fragment verbatim staffsize=16 ragged-right relative=2"
68         with open(self.in_file, 'r', encoding='utf-8') as f, open(self.in_lily_file, 'w', encoding='utf-8') as f_lily:
69             for line in f:
70                 if "language='lilypond'" in line:
71                     line = re.sub(
72                         '<programlisting\\s+language=\'lilypond\'.*?(role=\'(?P<options>.*?)\')?>',
73                         '<programlisting language="lilypond" role="\\g<options>">',
74                         line
75                     )
76                 f_lily.write(line)
77         os.unlink(self.in_file)
78
79     def postprocess_output_for_lilypond(self):
80         pass
81         # # Erase the <programlisting> that LilyPond left behind in the XML.
82         # in_file_before = self.in_file + '.tmp'
83         # shutil.move(self.in_file, in_file_before)
84         # with open(in_file_before, 'r', encoding='utf-8') as f_before, open(self.in_file, 'w', encoding='utf-8') as f_after:
85         #     looking_for_end_programlisting = False
86         #     for line in f_before:
87         #         # TODO: find an efficient way to distinguish those left-overs.
88         # https://lists.gnu.org/archive/html/bug-lilypond/2021-09/msg00040.html
89
90     def call_lilypond(self):
91         # LilyPond requires that its input file has the .lyxml extension (plus bugs in LilyPond).
92         print('>> Rewriting ' + self.in_file)
93         print('>> as ' + self.in_lily_file + '.')
94         self.preprocess_input_for_lilypond()
95
96         # Add LilyPond to the PATH. lilypond-book uses a direct call to lilypond from the PATH.
97         if os.path.isdir(self.lilypond_folder):
98             os.environ['PATH'] += os.pathsep + self.lilypond_folder
99
100         # Make LilyPond believe it is working from the temporary LyX directory. Otherwise, it tries to find files
101         # starting from LyX's working directory... LilyPond bug, most likely.
102         # https://lists.gnu.org/archive/html/bug-lilypond/2021-09/msg00041.html
103         os.chdir(self.in_folder)
104
105         # Start LilyPond on the copied file. First test the binary, then check if adding Python helps.
106         command_args = ['--format=docbook', '--output=' + self.in_folder, self.in_lily_file]
107         command_raw = [self.lilypond_command] + command_args
108         command_python = ['python', self.lilypond_command] + command_args
109
110         print('>> Running LilyPond.')
111         sys.stdout.flush()  # So that the LilyPond output is at the right place in the logs.
112
113         failed = True
114         exceptions = []
115         for cmd in [command_raw, command_python]:
116             try:
117                 subprocess.check_call(cmd, stdout=sys.stdout.fileno(), stderr=sys.stdout.fileno())
118                 print('>> Success running LilyPond with ')
119                 print('>> ' + str(cmd))
120                 failed = False
121             except (subprocess.CalledProcessError, OSError) as e:
122                 exceptions.append((cmd, e))
123
124         if failed:
125             print('>> Error from LilyPond. The successive calls were:')
126             for (i, pair) in enumerate(exceptions):
127                 exc = pair[0]
128                 cmd = pair[1]
129
130                 print('>> (' + i + ') Error from trying ' + str(cmd) + ':')
131                 print('>> (' + i + ') ' + str(exc))
132
133         if failed:
134             sys.exit(1)
135
136         # LilyPond has a distressing tendency to leave the raw LilyPond code in the new file.
137         self.postprocess_output_for_lilypond()
138
139         # Now, in_file should have the clean LilyPond-processed contents.
140
141     def copy_lilypond_generated_images(self):
142         # LilyPond generates a lot of files in LyX' temporary folder, within the ff folder: source LilyPond files
143         # for each snippet to render, images in several formats.
144         in_generated_images_folder = os.path.join(self.in_folder, 'ff')
145         out_generated_images_folder = os.path.join(self.out_folder, 'ff')
146
147         if not os.path.isdir(out_generated_images_folder):
148             os.mkdir(out_generated_images_folder)
149
150         for img in os.listdir(in_generated_images_folder):
151             if not img.endswith('.png') and not img.endswith('.pdf'):
152                 continue
153
154             shutil.copyfile(
155                 os.path.join(in_generated_images_folder, img),
156                 os.path.join(out_generated_images_folder, img),
157             )
158
159     def copy(self):
160         # Apply LilyPond to the original file if available and needed.
161         if self.do_lilypond_processing:
162             print('>> The input file needs a LilyPond pass and LilyPond is available.')
163             self.call_lilypond()
164
165         # Perform the actual copy: both the modified XML file and the generated images, if LilyPond is used.
166         shutil.copyfile(self.in_file, self.out_file)
167         if self.do_lilypond_processing:
168             self.copy_lilypond_generated_images()
169
170
171 if __name__ == '__main__':
172     if len(sys.argv) != 4:
173         print('Exactly four arguments are expected, only %s found: %s.' % (len(sys.argv), sys.argv))
174         sys.exit(1)
175
176     DocBookCopier(sys.argv).copy()