1 # This file is part of lyx2lyx
2 # Copyright (C) 2002-2024 The LyX Team
3 # Copyright (C) 2002-2004 Dekel Tsur <dekel@lyx.org>
4 # Copyright (C) 2002-2006 José Matos <jamatos@lyx.org>
6 # This program is free software; you can redistribute it and/or
7 # modify it under the terms of the GNU General Public License
8 # as published by the Free Software Foundation; either version 2
9 # of the License, or (at your option) any later version.
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
20 "The LyX module has all the rules related with different lyx file formats."
31 from parser_tools import (
40 import lyx2lyx_version
42 version__ = lyx2lyx_version.version
44 except ModuleNotFoundError:
45 # we are running from the build directory so assume the last version
47 stable_version = False
52 ####################################################################
53 # Private helper functions
56 def find_end_of_inset(lines, i):
57 "Find beginning of inset, where lines[i] is included."
58 return find_end_of(lines, i, "\\begin_inset", "\\end_inset")
61 def minor_versions(major, last_minor_version):
62 """Generate minor versions, using major as prefix and minor
63 versions from 0 until last_minor_version, plus the generic version.
67 minor_versions("1.2", 4) ->
68 [ "1.2", "1.2.0", "1.2.1", "1.2.2", "1.2.3"]
70 return [major] + [major + ".%d" % i for i in range(last_minor_version + 1)]
73 # End of helper functions
74 ####################################################################
77 # Regular expressions used
78 format_re = re.compile(r"(\d)[\.,]?(\d\d)")
79 fileformat = re.compile(r"\\lyxformat\s*(\S*)")
80 original_version = re.compile(b".*?LyX ([\\d.]*)")
81 original_tex2lyx_version = re.compile(b".*?tex2lyx ([\\d.]*)")
84 # file format information:
85 # file, supported formats, stable release versions
87 ("0_06", [200], minor_versions("0.6", 4)),
88 ("0_08", [210], minor_versions("0.8", 6) + ["0.7"]),
89 ("0_10", [210], minor_versions("0.10", 7) + ["0.9"]),
90 ("0_12", [215], minor_versions("0.12", 1) + ["0.11"]),
91 ("1_0", [215], minor_versions("1.0", 4)),
92 ("1_1", [215], minor_versions("1.1", 4)),
93 ("1_1_5", [216], ["1.1", "1.1.5", "1.1.5.1", "1.1.5.2"]),
94 ("1_1_6_0", [217], ["1.1", "1.1.6", "1.1.6.1", "1.1.6.2"]),
95 ("1_1_6_3", [218], ["1.1", "1.1.6.3", "1.1.6.4"]),
96 ("1_2", [220], minor_versions("1.2", 4)),
97 ("1_3", [221], minor_versions("1.3", 7)),
98 # Note that range(i,j) is up to j *excluded*.
99 ("1_4", list(range(222, 246)), minor_versions("1.4", 5)),
100 ("1_5", list(range(246, 277)), minor_versions("1.5", 7)),
101 ("1_6", list(range(277, 346)), minor_versions("1.6", 10)),
102 ("2_0", list(range(346, 414)), minor_versions("2.0", 8)),
103 ("2_1", list(range(414, 475)), minor_versions("2.1", 5)),
104 ("2_2", list(range(475, 509)), minor_versions("2.2", 4)),
105 ("2_3", list(range(509, 545)), minor_versions("2.3", 7)),
106 ("2_4", list(range(545, 621)), minor_versions("2.4", 0)),
107 ("2_5", (), minor_versions("2.5", 0)),
110 ####################################################################
111 # This is useful just for development versions #
112 # if the list of supported formats is empty get it from last step #
113 if not format_relation[-1][1]:
114 step, mode = format_relation[-1][0], "convert"
115 convert = getattr(__import__("lyx_" + step), mode)
116 format_relation[-1] = (step, [conv[0] for conv in convert], format_relation[-1][2])
118 ####################################################################
122 "Returns a list with supported file formats."
124 for version in format_relation:
125 for format in version[1]:
126 if format not in formats:
127 formats.append(format)
132 "Returns a list with the supported file formats."
134 %s\tstable format: %s
135 \tstable versions: %s
136 \tdevelopment formats: %s
139 out = "version: formats and versions"
140 for version in format_relation:
141 major = str(version[2][0])
142 versions = str(version[2][1:])
143 if len(version[1]) == 1:
144 formats = str(version[1][0])
145 stable_format = str(version[1][0])
146 elif not stable_version and major == version__:
147 stable_format = "-- not yet --"
148 versions = "-- not yet --"
149 formats = f"{version[1][0]} - {version[1][-1]}"
151 formats = f"{version[1][0]} - {version[1][-2]}"
152 stable_format = str(version[1][-1])
154 out += template % (major, stable_format, versions, formats)
158 def get_end_format():
159 "Returns the more recent file format available."
160 # this check will fail only when we have a new version
161 # and there is no format change yet.
162 if format_relation[-1][1]:
163 return format_relation[-1][1][-1]
164 return format_relation[-2][1][-1]
167 def get_backend(textclass):
168 "For _textclass_ returns its backend."
169 if textclass == "linuxdoc" or textclass == "manpage":
171 if textclass.startswith("docbook") or textclass.startswith("agu-"):
177 "Remove end of line char(s)."
178 if line[-1] != "\n" and line[-1] != "\r":
179 # May happen for the last line of a document
181 if line[-2:-1] == "\r":
187 def trim_eol_binary(line):
188 "Remove end of line char(s)."
189 if line[-1] != 10 and line[-1] != 13:
190 # May happen for the last line of a document
192 if line[-2:-1] == 13:
198 def get_encoding(language, inputencoding, format, cjk_encoding):
199 "Returns enconding of the lyx file"
202 # CJK-LyX encodes files using the current locale encoding.
203 # This means that files created by CJK-LyX can only be converted using
204 # the correct locale settings unless the encoding is given as commandline
206 if cjk_encoding == "auto":
207 return locale.getpreferredencoding()
210 from lyx2lyx_lang import lang
212 if inputencoding == "auto" or inputencoding == "default":
213 return lang[language][3]
214 if inputencoding == "":
216 if inputencoding == "utf8x":
218 # python does not know the alias latin9
219 if inputencoding == "latin9":
228 """This class carries all the information of the LyX file."""
236 debug=default_debug__,
245 end_format: final format that the file should be converted. (integer)
246 input: the name of the input source, if empty resort to standard input.
247 output: the name of the output file, if empty use the standard output.
248 error: the name of the error file, if empty use the standard error.
249 debug: debug level, O means no debug, as its value increases be more verbose.
251 self.choose_input(input)
255 self.err = open(error, "w")
257 self.err = sys.stderr
260 self.try_hard = try_hard
261 self.cjk_encoding = cjk_encoding
264 self.end_format = self.lyxformat(end_format)
266 # In case the target version and format are both specified
267 # verify that they are compatible. If not send a warning
268 # and ignore the version.
270 message = "Incompatible version %s for specified format %d" % (
274 for version in format_relation:
275 if self.end_format in version[1]:
276 if final_version not in version[2]:
277 self.warning(message)
280 for version in format_relation:
281 if final_version in version[2]:
282 # set the last format for that version
283 self.end_format = version[1][-1]
288 self.end_format = get_end_format()
290 if not final_version:
291 for step in format_relation:
292 if self.end_format in step[1]:
293 final_version = step[2][1]
294 self.final_version = final_version
295 self.warning("Final version: %s" % self.final_version, 10)
296 self.warning("Final format: %d" % self.end_format, 10)
298 self.backend = "latex"
299 self.textclass = "article"
300 # This is a hack: We use '' since we don't know the default
301 # layout of the text class. LyX will parse it as default layout.
302 # FIXME: Read the layout file and use the real default layout
303 self.default_layout = ""
308 self.encoding = encoding
309 self.language = language
310 self.systemlyxdir = systemlyxdir
312 def warning(self, message, debug_level=default_debug__):
313 """Emits warning to self.error, if the debug_level is less
314 than the self.debug."""
315 if debug_level <= self.debug:
316 self.err.write("lyx2lyx warning: " + message + "\n")
318 def error(self, message):
319 "Emits a warning and exits if not in try_hard mode."
320 self.warning(message)
321 if not self.try_hard:
322 self.warning("Quitting.")
328 """Reads a file into the self.header and
329 self.body parts, from self.input."""
331 # First pass: Read header to determine file encoding
332 # If we are running under python3 then all strings are binary in this
333 # pass. In some cases we need to convert binary to unicode in order to
334 # use our parser tools. Since we do not know the true encoding yet we
335 # use latin1. This works since a) the parts we are interested in are
336 # pure ASCII (subset of latin1) and b) in contrast to pure ascii or
337 # utf8, one can decode any 8byte string using latin1.
340 line = self.input.readline()
342 # eof found before end of header
343 self.error("Invalid LyX file: Missing body.")
346 # Remove UTF8 BOM marker if present
347 if line.startswith(codecs.BOM_UTF8):
348 line = line[len(codecs.BOM_UTF8) :]
352 line = trim_eol_binary(line)
353 decoded = line.decode("latin1")
354 if check_token(decoded, "\\begin_preamble"):
356 line = self.input.readline()
358 # eof found before end of header
359 self.error("Invalid LyX file: Missing body.")
361 line = trim_eol_binary(line)
362 decoded = line.decode("latin1")
363 if check_token(decoded, "\\end_preamble"):
366 if decoded.split()[:0] in (
372 "Malformed LyX file:"
373 "Missing '\\end_preamble'."
374 "\nAdding it now and hoping"
378 self.preamble.append(line)
380 if check_token(decoded, "\\end_preamble"):
387 if decoded.split()[0] in (
393 self.body.append(line)
396 self.header.append(line)
398 i = find_token(self.header, b"\\textclass", 0)
400 self.warning("Malformed LyX file: Missing '\\textclass'.")
401 i = find_token(self.header, b"\\lyxformat", 0) + 1
402 self.header[i:i] = [b"\\textclass article"]
404 self.textclass = get_value(self.header, b"\\textclass", 0, default=b"")
405 self.language = get_value(self.header, b"\\language", 0, default=b"english").decode(
408 self.inputencoding = get_value(
409 self.header, b"\\inputencoding", 0, default=b"auto"
411 self.format = self.read_format()
412 self.initial_format = self.format
413 self.encoding = get_encoding(
414 self.language, self.inputencoding, self.format, self.cjk_encoding
416 self.initial_version = self.read_version()
418 # Second pass over header and preamble, now we know the file encoding
419 # Do not forget the textclass (Debian bug #700828)
420 self.textclass = self.textclass.decode(self.encoding)
421 self.backend = get_backend(self.textclass)
422 for i in range(len(self.header)):
423 self.header[i] = self.header[i].decode(self.encoding)
424 for i in range(len(self.preamble)):
425 self.preamble[i] = self.preamble[i].decode(self.encoding)
426 for i in range(len(self.body)):
427 self.body[i] = self.body[i].decode(self.encoding)
431 line = self.input.readline().decode(self.encoding)
434 self.body.append(trim_eol(line))
437 "Writes the LyX file to self.output."
438 self.choose_output(self.output)
442 if self.encoding == "auto":
443 self.encoding = get_encoding(
444 self.language, self.encoding, self.format, self.cjk_encoding
447 i = find_token(self.header, "\\textclass", 0) + 1
448 preamble = ["\\begin_preamble"] + self.preamble + ["\\end_preamble"]
449 header = self.header[:i] + preamble + self.header[i:]
453 for line in header + [""] + self.body:
454 self.output.write(line + "\n")
456 def choose_output(self, output):
457 """Choose output streams dealing transparently with
460 # This is a bit complicated, because we need to be compatible both with
461 # python 2 and python 3. Therefore we handle the encoding here and not
462 # when writing individual lines and may need up to 3 layered file like
466 outputfileobj = open(output, "wb")
468 # We cannot not use stdout directly since it needs text, not bytes in python 3
469 outputfileobj = os.fdopen(sys.stdout.fileno(), "wb")
470 # We cannot not use gzip.open() since it is not supported by python 2
471 zipbuffer = gzip.GzipFile(mode="wb", fileobj=outputfileobj)
472 # We do not want to use different newlines on different OSes inside zipped files
473 self.output = io.TextIOWrapper(zipbuffer, encoding=self.encoding, newline="\n")
476 self.output = open(output, "w", encoding=self.encoding)
478 self.output = open(sys.stdout.fileno(), "w", encoding=self.encoding)
480 def choose_input(self, input):
481 """Choose input stream, dealing transparently with
484 # Since we do not know the encoding yet we need to read the input as
485 # bytes in binary mode, and convert later to unicode.
486 if input and input != "-":
487 self.dir = os.path.dirname(os.path.abspath(input))
489 gzip.open(input).readline()
490 self.input = gzip.open(input)
491 self.compressed = True
493 self.input = open(input, "rb")
494 self.compressed = False
497 self.input = os.fdopen(sys.stdin.fileno(), "rb")
498 self.compressed = False
500 def lyxformat(self, format):
501 "Returns the file format representation, an integer."
502 result = format_re.match(format)
504 format = int(result.group(1) + result.group(2))
508 self.error(str(format) + ": " + "Invalid LyX file.")
510 if format in formats_list():
513 self.error(str(format) + ": " + "Format not supported.")
516 def read_version(self):
517 """Searchs for clues of the LyX version used to write the
518 file, returns the most likely value, or None otherwise."""
520 for line in self.header:
521 if line[0:1] != b"#":
524 line = line.replace(b"fix", b".")
525 # need to test original_tex2lyx_version first because tex2lyx
526 # writes "#LyX file created by tex2lyx 2.2"
527 result = original_tex2lyx_version.match(line)
529 result = original_version.match(line)
531 # Special know cases: reLyX and KLyX
532 if line.find(b"reLyX") != -1 or line.find(b"KLyX") != -1:
535 res = result.group(1)
538 # self.warning("Version %s" % result.group(1))
539 return res.decode("ascii")
540 self.warning(str(self.header[:2]))
543 def set_version(self):
544 "Set the header with the version used."
546 initial_comment = " ".join(
548 "#LyX %s created this file." % version__,
549 "For more info see https://www.lyx.org/",
553 # Simple heuristic to determine the comment that always starts
555 if self.header[0].startswith("#"):
556 self.header[0] = initial_comment
558 self.header.insert(0, initial_comment)
560 # Old lyx files had a two lines comment header:
561 # 1) the first line had the user who had created it
562 # 2) the second line had the lyx version used
563 # later we decided that 1) was a privacy risk for no gain
564 # here we remove the second line effectively erasing 1)
565 if self.header[1][0] == "#":
568 def read_format(self):
569 "Read from the header the fileformat of the present LyX file."
570 for line in self.header:
571 result = fileformat.match(line.decode("ascii"))
573 return self.lyxformat(result.group(1))
575 self.error("Invalid LyX File: Missing format.")
578 def set_format(self):
579 "Set the file format of the file, in the header."
580 if self.format <= 217:
581 format = str(float(self.format) / 100)
583 format = str(self.format)
584 i = find_token(self.header, "\\lyxformat", 0)
585 self.header[i] = "\\lyxformat %s" % format
587 def set_textclass(self):
588 i = find_token(self.header, "\\textclass", 0)
589 self.header[i] = "\\textclass %s" % self.textclass
591 # Note that the module will be added at the END of the extant ones
592 def add_module(self, module):
593 "Append module to the modules list."
594 i = find_token(self.header, "\\begin_modules", 0)
596 # No modules yet included
597 i = find_token(self.header, "\\textclass", 0)
599 self.warning("Malformed LyX document: No \\textclass!!")
601 modinfo = ["\\begin_modules", module, "\\end_modules"]
602 self.header[i + 1 : i + 1] = modinfo
604 j = find_token(self.header, "\\end_modules", i)
606 self.warning("(add_module)Malformed LyX document: No \\end_modules.")
608 k = find_token(self.header, module, i)
609 if k != -1 and k < j:
611 self.header.insert(j, module)
613 def del_module(self, module):
614 "Delete `module` from module list, return success."
615 modlist = self.get_module_list()
616 if module not in modlist:
618 self.set_module_list([line for line in modlist if line != module])
621 def get_module_list(self):
622 "Return list of modules."
623 i = find_token(self.header, "\\begin_modules", 0)
626 j = find_token(self.header, "\\end_modules", i)
627 return self.header[i + 1 : j]
629 def set_module_list(self, mlist):
630 i = find_token(self.header, "\\begin_modules", 0)
632 # No modules yet included
633 tclass = find_token(self.header, "\\textclass", 0)
635 self.warning("Malformed LyX document: No \\textclass!!")
639 j = find_token(self.header, "\\end_modules", i)
641 self.warning("(set_module_list) Malformed LyX document: No \\end_modules.")
645 mlist = ["\\begin_modules"] + mlist + ["\\end_modules"]
646 self.header[i:j] = mlist
648 def set_parameter(self, param, value):
649 "Set the value of the header parameter."
650 i = find_token(self.header, "\\" + param, 0)
652 self.warning("Parameter not found in the header: %s" % param, 3)
654 self.header[i] = f"\\{param} {str(value)}"
656 def is_default_layout(self, layout):
657 "Check whether a layout is the default layout of this class."
658 # FIXME: Check against the real text class default layout
659 if layout == "Standard" or layout == self.default_layout:
664 "Convert from current (self.format) to self.end_format."
665 if self.format == self.end_format:
667 "No conversion needed: Target format %s "
668 "same as current format!" % self.format,
673 mode, conversion_chain = self.chain()
674 self.warning("conversion chain: " + str(conversion_chain), 3)
676 for step in conversion_chain:
677 steps = getattr(__import__("lyx_" + step), mode)
679 self.warning(f"Convertion step: {step} - {mode}", default_debug__ + 1)
682 "The conversion to an older "
683 "format (%s) is not implemented." % self.format
686 multi_conv = len(steps) != 1
687 for version, table in steps:
690 and (self.format >= version and mode == "convert")
691 or (self.format <= version and mode == "revert")
699 except Exception as exception:
701 f"An error occurred in {version}, {conv}",
704 if not self.try_hard:
709 f"{time.time() - init_t:f}: Elapsed time on {conv}",
712 self.format = version
713 if self.end_format == self.format:
717 """This is where all the decisions related with the
718 conversion are taken. It returns a list of modules needed to
719 convert the LyX file from self.format to self.end_format"""
724 for rel in format_relation:
725 if self.initial_version in rel[2]:
727 initial_step = rel[0]
731 if not correct_version:
734 "Version does not match file format, "
735 "discarding it. (Version %s, format %d)"
736 % (self.initial_version, self.format)
738 for rel in format_relation:
740 initial_step = rel[0]
743 # This should not happen, really.
744 self.error("Format not supported.")
746 # Find the final step
747 for rel in format_relation:
748 if self.end_format in rel[1]:
752 self.error("Format not supported.")
754 # Convertion mode, back or forth
756 if (initial_step, self.initial_format) < (final_step, self.end_format):
759 for step in format_relation:
760 if initial_step <= step[0] <= final_step and step[2][0] <= self.final_version:
761 full_steps.append(step)
762 if full_steps[0][1][-1] == self.format:
763 full_steps = full_steps[1:]
764 for step in full_steps:
765 steps.append(step[0])
768 relation_format = format_relation[:]
769 relation_format.reverse()
772 for step in relation_format:
773 if final_step <= step[0] <= initial_step:
774 steps.append(step[0])
777 if last_step[1][-1] == self.end_format:
780 self.warning(f"Convertion mode: {mode}\tsteps{steps}", 10)
783 def append_local_layout(self, new_layout):
784 "Append `new_layout` to the local layouts."
785 # new_layout may be a string or a list of strings (lines)
787 new_layout = new_layout.splitlines()
788 except AttributeError:
790 i = find_token(self.header, "\\begin_local_layout", 0)
792 k = find_token(self.header, "\\language", 0)
794 # this should not happen
795 self.warning("Malformed LyX document! No \\language header found!")
797 self.header[k:k] = ["\\begin_local_layout", "\\end_local_layout"]
800 j = find_end_of(self.header, i, "\\begin_local_layout", "\\end_local_layout")
802 # this should not happen
803 self.warning("Malformed LyX document: Can't find end of local layout!")
806 self.header[i + 1 : i + 1] = new_layout
808 def del_local_layout(self, layout_def):
809 "Delete `layout_def` from local layouts, return success."
810 i = find_complete_lines(self.header, layout_def)
813 j = i + len(layout_def)
815 self.header[i - 1] == "\\begin_local_layout"
816 and self.header[j] == "\\end_local_layout"
820 self.header[i:j] = []
823 def del_from_header(self, lines):
824 "Delete `lines` from the document header, return success."
825 i = find_complete_lines(self.header, lines)
829 self.header[i:j] = []
833 # Part of an unfinished attempt to make lyx2lyx gave a more
834 # structured view of the document.
835 # def get_toc(self, depth = 4):
836 # " Returns the TOC of this LyX document."
837 # paragraphs_filter = {'Title' : 0,'Chapter' : 1, 'Section' : 2,
838 # 'Subsection' : 3, 'Subsubsection': 4}
839 # allowed_insets = ['Quotes']
840 # allowed_parameters = ('\\paragraph_spacing', '\\noindent',
841 # '\\align', '\\labelwidthstring',
842 # "\\start_of_appendix", "\\leftindent")
844 # for section in paragraphs_filter.keys():
845 # sections.append('\\begin_layout %s' % section)
850 # i = find_tokens(self.body, sections, i)
854 # j = find_end_of(self.body, i + 1, '\\begin_layout', '\\end_layout')
856 # self.warning('Incomplete file.', 0)
859 # section = self.body[i].split()[1]
860 # if section[-1] == '*':
861 # section = section[:-1]
866 # # skip paragraph parameters
867 # while not self.body[k].strip() or self.body[k].split()[0] \
868 # in allowed_parameters:
872 # if check_token(self.body[k], '\\begin_inset'):
873 # inset = self.body[k].split()[1]
874 # end = find_end_of_inset(self.body, k)
875 # if end == -1 or end > j:
876 # self.warning('Malformed file.', 0)
878 # if inset in allowed_insets:
879 # par.extend(self.body[k: end+1])
882 # par.append(self.body[k])
885 # # trim empty lines in the end.
886 # while par and par[-1].strip() == '':
889 # toc_par.append(Paragraph(section, par))
896 class File(LyX_base):
897 "This class reads existing LyX files."
905 debug=default_debug__,
926 # FIXME: header settings are completely outdated, don't use like this
927 # class NewFile(LyX_base):
928 # " This class is to create new LyX files."
929 # def set_header(self, **params):
930 # # set default values
931 # self.header.extend([
932 # "#LyX xxxx created this file."
933 # "For more info see http://www.lyx.org/",
935 # "\\begin_document",
937 # "\\textclass article",
938 # "\\language english",
939 # "\\inputencoding auto",
940 # "\\font_roman default",
941 # "\\font_sans default",
942 # "\\font_typewriter default",
943 # "\\font_default_family default",
945 # "\\font_osf false",
946 # "\\font_sf_scale 100",
947 # "\\font_tt_scale 100",
948 # "\\graphics default",
949 # "\\paperfontsize default",
950 # "\\papersize default",
951 # "\\use_geometry false",
953 # "\\cite_engine basic",
954 # "\\use_bibtopic false",
955 # "\\use_indices false",
956 # "\\paperorientation portrait",
959 # "\\paragraph_separation indent",
960 # "\\defskip medskip",
961 # "\\quotes_language english",
962 # "\\papercolumns 1",
964 # "\\paperpagestyle default",
965 # "\\tracking_changes false",
968 # self.format = get_end_format()
969 # for param in params:
970 # self.set_parameter(param, params[param])
973 # def set_body(self, paragraphs):
974 # self.body.extend(['\\begin_body',''])
976 # for par in paragraphs:
977 # self.body.extend(par.asLines())
979 # self.body.extend(['','\\end_body', '\\end_document'])
982 # Part of an unfinished attempt to make lyx2lyx gave a more
983 # structured view of the document.
985 # # unfinished implementation, it is missing the Text and Insets
987 # " This class represents the LyX paragraphs."
988 # def __init__(self, name, body=[], settings = [], child = []):
990 # name: paragraph name.
991 # body: list of lines of body text.
992 # child: list of paragraphs that descend from this paragraph.
996 # self.settings = settings
1000 # """ Converts the paragraph to a list of strings, representing
1001 # it in the LyX file."""
1003 # result = ['','\\begin_layout %s' % self.name]
1004 # result.extend(self.settings)
1006 # result.extend(self.body)
1007 # result.append('\\end_layout')
1009 # if not self.child:
1012 # result.append('\\begin_deeper')
1013 # for node in self.child:
1014 # result.extend(node.asLines())
1015 # result.append('\\end_deeper')