]> git.lyx.org Git - lyx.git/blobdiff - lib/scripts/docbook_copy.py
Create Chapter 6 Bullets in Additional.lyx and move the bullet section into it; this...
[lyx.git] / lib / scripts / docbook_copy.py
index 1346a9042eea596311abd9fa48c18432002fc938..186bbe554f78e6bcb1949495c43a87d80a9ae4e0 100644 (file)
@@ -26,48 +26,58 @@ import shutil
 import sys
 
 
-def need_lilypond(file):
-    # Really tailored to the kind of output lilypond.module makes (in lib/layouts).
-    with open(file, 'r') as f:
-        return "language='lilypond'" in f.read()
-
-
-def copy_docbook(args):
-    print(args)
-    if len(args) != 4:
-        print('Exactly four arguments are expected, only %s found: %s.' % (len(args), args))
-        sys.exit(1)
-
-    # Parse the command line.
-    lilypond_command = args[1]
-    in_file = args[2]
-    out_file = args[3]
-
-    has_lilypond = lilypond_command != "" and lilypond_command != "none"
-
-    # Guess the path for LilyPond.
-    lilypond_folder = os.path.split(lilypond_command)[0] if has_lilypond else ''
-
-    # Help debugging.
-    print(">> Given arguments:")
-    print(">> LilyPond: " + ("present" if has_lilypond else "not found") + " " + lilypond_command)
-    print(">> LilyPond path: " + lilypond_folder)
-    print(">> Input file: " + in_file)
-    print(">> Output file: " + out_file)
-
-    # Apply LilyPond to the original file if available and needed.
-    if has_lilypond and need_lilypond(in_file):
-        in_lily_file = in_file.replace(".xml", ".lyxml")
-        print(">> The input file needs a LilyPond pass and LilyPond is available.")
-        print(">> Rewriting " + in_file + " as " + in_lily_file)
-
+class DocBookCopier:
+    def __init__(self, args):
+
+        # Parse the command line.
+        self.lilypond_command = args[1]
+        self.in_file = args[2]
+        self.out_file = args[3]
+
+        # Compute a few things from the raw parameters.
+        self.in_folder = os.path.split(self.in_file)[0]
+        self.out_folder = os.path.split(self.out_file)[0]
+
+        self.in_lily_file = self.in_file.replace('.xml', '.lyxml')
+        self.has_lilypond = self.lilypond_command not in {'', 'none'}
+        self.lilypond_folder = os.path.split(self.lilypond_command)[0] if self.has_lilypond else ''
+        self.do_lilypond_processing = self.has_lilypond and self.in_file_needs_lilypond()
+
+        # Help debugging.
+        print('>> Given arguments:')
+        print('>> LilyPond: ' + ('present' if self.has_lilypond else 'not found') + '.')
+        print('>> LilyPond callable as: ' + self.lilypond_command + '.')
+        print('>> LilyPond path: ' + self.lilypond_folder + '.')
+        print('>> Input file: ' + self.in_file + '.')
+        print('>> Output file: ' + self.out_file + '.')
+        print('>> Input folder: ' + self.in_folder + '.')
+        print('>> Output folder: ' + self.out_folder + '.')
+
+    def in_file_needs_lilypond(self):
+        # Really tailored to the kind of output lilypond.module makes (in lib/layouts).
+        with open(self.in_file, 'r') as f:
+            return "language='lilypond'" in f.read()
+
+    def preprocess_input_for_lilypond(self):
         # LilyPond requires that its input file has the .lyxml extension. Due to a bug in LilyPond,
         # use " instead of ' to encode XML attributes.
-        # https://lists.gnu.org/archive/html/bug-lilypond/2021-09/msg00039.html
+        # Bug report: https://gitlab.com/lilypond/lilypond/-/issues/6185
         # Typical transformation:
+        # Fixed by 2.23.4.
         #     FROM:  language='lilypond' role='fragment verbatim staffsize=16 ragged-right relative=2'
         #     TO:    language="lilypond" role="fragment verbatim staffsize=16 ragged-right relative=2"
-        with open(in_file, 'r', encoding='utf-8') as f, open(in_lily_file, 'w', encoding='utf-8') as f_lily:
+
+        # Another problem to fix: the output is in XML, with some characters encoded as XML
+        # entities. For instance, this could be in a LilyPond snippet:
+        #     \new PianoStaff <<
+        # instead of:
+        #     \new PianoStaff <<
+        # (More complete example:
+        # https://lilypond.org/doc/v2.23/Documentation/learning/piano-centered-lyrics.)
+        # This issue must be fixed by LilyPond, as any change in this part would make the XML
+        # file invalid.
+        # Bug report: https://gitlab.com/lilypond/lilypond/-/issues/6204
+        with open(self.in_file, 'r', encoding='utf-8') as f, open(self.in_lily_file, 'w', encoding='utf-8') as f_lily:
             for line in f:
                 if "language='lilypond'" in line:
                     line = re.sub(
@@ -76,40 +86,100 @@ def copy_docbook(args):
                         line
                     )
                 f_lily.write(line)
-        os.unlink(in_file)
-
-        # Add LilyPond to the PATH.
-        if os.path.isdir(lilypond_folder):
-            os.environ['PATH'] += os.pathsep + lilypond_folder
+        os.unlink(self.in_file)
+
+    def postprocess_output_for_lilypond(self):
+        # Major problem: LilyPond used to output the LilyPond code outside the image, which is then always displayed
+        # before the image (instead of only the generated image).
+        # Bug report: https://gitlab.com/lilypond/lilypond/-/issues/6186
+        # No more necessary with the new version of LilyPond (2.23.4). No efficient way to decide how to post-process
+        # for previous versions of LilyPond. Basically, it does not make sense to post-process.
+        pass
+
+    def call_lilypond(self):
+        # LilyPond requires that its input file has the .lyxml extension (plus bugs in LilyPond).
+        print('>> Rewriting ' + self.in_file)
+        print('>> as ' + self.in_lily_file + '.')
+        self.preprocess_input_for_lilypond()
+
+        # Add LilyPond to the PATH. lilypond-book uses a direct call to lilypond from the PATH.
+        if os.path.isdir(self.lilypond_folder):
+            os.environ['PATH'] += os.pathsep + self.lilypond_folder
+
+        # Make LilyPond believe it is working from the temporary LyX directory. Otherwise, it tries to find files
+        # starting from LyX's working directory... LilyPond bug, most likely.
+        # https://lists.gnu.org/archive/html/bug-lilypond/2021-09/msg00041.html
+        os.chdir(self.in_folder)
 
         # Start LilyPond on the copied file. First test the binary, then check if adding Python helps.
-        command_raw = [lilypond_command, '--format=docbook', in_lily_file]
-        command_python = ['python', lilypond_command, '--format=docbook', in_lily_file]
-
-        failed = False
-        try:
-            subprocess.check_call(command_raw, stdout=sys.stdout.fileno(), stderr=sys.stdout.fileno())
-            print(">> Success running LilyPond with " + str(command_raw))
-        except (subprocess.CalledProcessError, OSError) as e1:
+        command_args = ['--format=docbook', '--output=' + self.in_folder, self.in_lily_file]
+        command_raw = [self.lilypond_command] + command_args
+        command_python = ['python', self.lilypond_command] + command_args
+
+        print('>> Running LilyPond.')
+        sys.stdout.flush()  # So that the LilyPond output is at the right place in the logs.
+
+        failed = True
+        exceptions = []
+        for cmd in [command_raw, command_python]:
             try:
-                subprocess.check_call(command_python, stdout=sys.stdout.fileno(), stderr=sys.stdout.fileno())
-                print(">> Success running LilyPond with " + str(command_python))
-            except (subprocess.CalledProcessError, OSError) as e2:
-                print('>> Error from LilyPond')
-                print('>> Error from trying ' + str(command_raw) + ':')
-                print(e1)
-                print('>> Error from trying ' + str(command_python) + ':')
-                print(e2)
-                failed = True
+                subprocess.check_call(cmd, stdout=sys.stdout.fileno(), stderr=sys.stdout.fileno())
+                print('>> Success running LilyPond with ')
+                print('>> ' + str(cmd))
+                failed = False
+            except (subprocess.CalledProcessError, OSError) as e:
+                exceptions.append((cmd, e))
+
+        if failed:
+            print('>> Error from LilyPond. The successive calls were:')
+            for (i, pair) in enumerate(exceptions):
+                exc = pair[0]
+                cmd = pair[1]
+
+                print('>> (' + i + ') Error from trying ' + str(cmd) + ':')
+                print('>> (' + i + ') ' + str(exc))
 
         if failed:
             sys.exit(1)
 
-        # Now, in_file should have the LilyPond-processed contents.
+        # LilyPond has a distressing tendency to leave the raw LilyPond code in the new file.
+        self.postprocess_output_for_lilypond()
+
+        # Now, in_file should have the clean LilyPond-processed contents.
+
+    def copy_lilypond_generated_images(self):
+        # LilyPond generates a lot of files in LyX' temporary folder, within the ff folder: source LilyPond files
+        # for each snippet to render, images in several formats.
+        in_generated_images_folder = os.path.join(self.in_folder, 'ff')
+        out_generated_images_folder = os.path.join(self.out_folder, 'ff')
 
-    # Perform the final copy.
-    shutil.copyfile(in_file, out_file, follow_symlinks=False)
+        if not os.path.isdir(out_generated_images_folder):
+            os.mkdir(out_generated_images_folder)
+
+        for img in os.listdir(in_generated_images_folder):
+            if not img.endswith('.png') and not img.endswith('.pdf'):
+                continue
+
+            shutil.copyfile(
+                os.path.join(in_generated_images_folder, img),
+                os.path.join(out_generated_images_folder, img),
+            )
+
+    def copy(self):
+        # Apply LilyPond to the original file if available and needed.
+        if self.do_lilypond_processing:
+            print('>> The input file needs a LilyPond pass and LilyPond is available.')
+            self.call_lilypond()
+
+        # Perform the actual copy: both the modified XML file and the generated images, if LilyPond is used.
+        shutil.copyfile(self.in_file, self.out_file)
+        if self.do_lilypond_processing:
+            self.copy_lilypond_generated_images()
 
 
 if __name__ == '__main__':
-    copy_docbook(sys.argv)
+    if len(sys.argv) != 4:
+        print('Exactly four arguments are expected, only %s found: %s.' % (len(sys.argv), sys.argv))
+        sys.exit(1)
+
+    DocBookCopier(sys.argv).copy()