]> git.lyx.org Git - lyx.git/blob - lib/scripts/docbook_copy.py
DocBook copy: don't error if the file was already copied.
[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
89     def call_lilypond(self):
90         # LilyPond requires that its input file has the .lyxml extension (plus bugs in LilyPond).
91         print('>> Rewriting ' + self.in_file)
92         print('>> as ' + self.in_lily_file + '.')
93         self.preprocess_input_for_lilypond()
94
95         # Add LilyPond to the PATH. lilypond-book uses a direct call to lilypond from the PATH.
96         if os.path.isdir(self.lilypond_folder):
97             os.environ['PATH'] += os.pathsep + self.lilypond_folder
98
99         # Make LilyPond believe it is working from the temporary LyX directory. Otherwise, it tries to find files
100         # starting from LyX's working directory... LilyPond bug.
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                 follow_symlinks=False,
156             )
157
158     def copy(self):
159         # Apply LilyPond to the original file if available and needed.
160         if self.do_lilypond_processing:
161             print('>> The input file needs a LilyPond pass and LilyPond is available.')
162             self.call_lilypond()
163
164         # Perform the actual copy: both the modified XML file and the generated images, if LilyPond is used.
165         shutil.copyfile(self.in_file, self.out_file, follow_symlinks=False)
166         if self.do_lilypond_processing:
167             self.copy_lilypond_generated_images()
168
169
170 if __name__ == '__main__':
171     if len(sys.argv) != 4:
172         print('Exactly four arguments are expected, only %s found: %s.' % (len(sys.argv), sys.argv))
173         sys.exit(1)
174
175     DocBookCopier(sys.argv).copy()