From efa0f19836f69b01bb61ed2567a08cee166c8c9c Mon Sep 17 00:00:00 2001 From: Georg Baum Date: Sun, 18 Dec 2011 21:27:17 +0000 Subject: [PATCH] More sensible longtable caption handling (needed for bug #7412) git-svn-id: svn://svn.lyx.org/lyx/lyx-devel/trunk@40522 a592a061-630c-0410-9148-cb99ea01b6c8 --- development/FORMAT | 7 ++ lib/doc/EmbeddedObjects.lyx | 36 ++++++++- lib/lyx2lyx/lyx_2_0.py | 2 - lib/lyx2lyx/lyx_2_1.py | 79 ++++++++++++++++++- lib/lyx2lyx/parser_tools.py | 9 +++ src/insets/InsetTabular.cpp | 153 ++++++++++++++++++++++-------------- src/insets/InsetTabular.h | 25 ++++-- src/tex2lyx/table.cpp | 47 ++++------- src/version.h | 4 +- 9 files changed, 254 insertions(+), 108 deletions(-) diff --git a/development/FORMAT b/development/FORMAT index d133dea45e..e5e69876ef 100644 --- a/development/FORMAT +++ b/development/FORMAT @@ -11,6 +11,13 @@ adjustments are made to tex2lyx and bugs are fixed in lyx2lyx. ----------------------- +2011-12-18 Georg Baum + * Format incremented to 421 (r40522) + The caption flag of longtable rows is no longer exclusive to the head + and foot flags, since captions can occur in any of the two heads and + two foots. Before, captions were implicitly in head or firsthead. + For Docbook and XHTML output the caption flag "wins" over head/foot. + 2011-12-12 Julien Rioux * Format incremented to 420 (r40484) New buffer param \biblio_style to specify a document-wide diff --git a/lib/doc/EmbeddedObjects.lyx b/lib/doc/EmbeddedObjects.lyx index b24a5aecfd..060c7fbaf0 100644 --- a/lib/doc/EmbeddedObjects.lyx +++ b/lib/doc/EmbeddedObjects.lyx @@ -231,11 +231,12 @@ initials \bullet 1 1 34 -1 \bullet 2 2 35 -1 \bullet 3 2 7 -1 -\tracking_changes false +\tracking_changes true \output_changes false \html_math_output 0 \html_css_as_file 0 \html_be_strict false +\author -195340706 "Georg Baum" \end_header \begin_body @@ -6118,7 +6119,38 @@ reference "sec:Longtables" \end_inset . - Only one table row can contain the caption. + +\change_inserted -195340706 1324242393 +A caption must be put into one of +\family sans +First +\begin_inset space ~ +\end_inset + +header +\family default +, +\family sans +Header +\family default +, +\family sans +Footer +\family default + and +\family sans +Fast +\begin_inset space ~ +\end_inset + +footer +\family default +. + Each kind of footer and header may only contain one +\change_deleted -195340706 1324242398 +Only one table row can contain the +\change_unchanged + caption. \end_layout \begin_layout Standard diff --git a/lib/lyx2lyx/lyx_2_0.py b/lib/lyx2lyx/lyx_2_0.py index d7891087b7..848621f16c 100644 --- a/lib/lyx2lyx/lyx_2_0.py +++ b/lib/lyx2lyx/lyx_2_0.py @@ -983,7 +983,6 @@ def revert_multirow(document): numrows = int(numrows) numcols = int(numcols) except: - document.warning(numrows) document.warning("Unable to determine rows and columns!") begin_table = end_table continue @@ -2222,7 +2221,6 @@ def revert_multirowOffset(document): numrows = int(numrows) numcols = int(numcols) except: - document.warning(numrows) document.warning("Unable to determine rows and columns!") begin_table = end_table continue diff --git a/lib/lyx2lyx/lyx_2_1.py b/lib/lyx2lyx/lyx_2_1.py index fccea99ef7..a708c2da16 100644 --- a/lib/lyx2lyx/lyx_2_1.py +++ b/lib/lyx2lyx/lyx_2_1.py @@ -25,8 +25,8 @@ import sys, os # Uncomment only what you need to import, please. -from parser_tools import del_token, find_token, find_end_of_inset, get_value, \ - get_quoted_value +from parser_tools import del_token, find_token, find_end_of, find_end_of_inset, \ + get_option_value, get_value, get_quoted_value, set_option_value #from parser_tools import find_token, find_end_of, find_tokens, \ #find_token_exact, find_end_of_inset, find_end_of_layout, \ @@ -220,7 +220,7 @@ def revert_australian(document): else: document.body[j] = document.body[j].replace("\\lang australian", "\\lang english") j += 1 - + def convert_biblio_style(document): "Add a sensible default for \\biblio_style based on the citation engine." @@ -261,6 +261,77 @@ def revert_biblio_style(document): i = j +def handle_longtable_captions(document, forward): + begin_table = 0 + while True: + begin_table = find_token(document.body, '') + if end_table == -1: + document.warning("Malformed LyX document: Could not find end of table.") + begin_table += 1 + continue + fline = find_token(document.body, "') + if end_row == -1: + document.warning("Can't find end of row " + str(row + 1)) + break + if forward: + if (get_option_value(document.body[begin_row], 'caption') == 'true' and + get_option_value(document.body[begin_row], 'endfirsthead') != 'true' and + get_option_value(document.body[begin_row], 'endhead') != 'true' and + get_option_value(document.body[begin_row], 'endfoot') != 'true' and + get_option_value(document.body[begin_row], 'endlastfoot') != 'true'): + document.body[begin_row] = set_option_value(document.body[begin_row], 'caption', 'true", endfirsthead="true') + elif get_option_value(document.body[begin_row], 'caption') == 'true': + if get_option_value(document.body[begin_row], 'endfirsthead') == 'true': + document.body[begin_row] = set_option_value(document.body[begin_row], 'endfirsthead', 'false') + if get_option_value(document.body[begin_row], 'endhead') == 'true': + document.body[begin_row] = set_option_value(document.body[begin_row], 'endhead', 'false') + if get_option_value(document.body[begin_row], 'endfoot') == 'true': + document.body[begin_row] = set_option_value(document.body[begin_row], 'endfoot', 'false') + if get_option_value(document.body[begin_row], 'endlastfoot') == 'true': + document.body[begin_row] = set_option_value(document.body[begin_row], 'endlastfoot', 'false') + begin_row = end_row + # since there could be a tabular inside this one, we + # cannot jump to end. + begin_table += 1 + + +def convert_longtable_captions(document): + "Add a firsthead flag to caption rows" + handle_longtable_captions(document, True) + + +def revert_longtable_captions(document): + "remove head/foot flag from caption rows" + handle_longtable_captions(document, False) + + ## # Conversion hub # @@ -274,9 +345,11 @@ convert = [ [418, []], [419, []], [420, [convert_biblio_style]], + [421, [convert_longtable_captions]], ] revert = [ + [420, [revert_longtable_captions]], [419, [revert_biblio_style]], [418, [revert_australian]], [417, [revert_justification]], diff --git a/lib/lyx2lyx/parser_tools.py b/lib/lyx2lyx/parser_tools.py index 31948fe30c..e32ac5dc4f 100644 --- a/lib/lyx2lyx/parser_tools.py +++ b/lib/lyx2lyx/parser_tools.py @@ -315,6 +315,15 @@ def get_option_value(line, option): return m.group(1) +def set_option_value(line, option, value): + rx = '(' + option + '\s*=\s*")[^"]+"' + rx = re.compile(rx) + m = rx.search(line) + if not m: + return line + return re.sub(rx, '\g<1>' + value + '"', line) + + def del_token(lines, token, start, end = 0): """ del_token(lines, token, start, end) -> int diff --git a/src/insets/InsetTabular.cpp b/src/insets/InsetTabular.cpp index 49a9fdcb8a..01ee12c516 100644 --- a/src/insets/InsetTabular.cpp +++ b/src/insets/InsetTabular.cpp @@ -1906,45 +1906,49 @@ bool Tabular::getLTNewPage(row_type row) const } -bool Tabular::haveLTHead() const +bool Tabular::haveLTHead(bool withcaptions) const { if (!is_long_tabular) return false; for (row_type i = 0; i < nrows(); ++i) - if (row_info[i].endhead) + if (row_info[i].endhead && + (withcaptions || !row_info[i].caption)) return true; return false; } -bool Tabular::haveLTFirstHead() const +bool Tabular::haveLTFirstHead(bool withcaptions) const { if (!is_long_tabular || endfirsthead.empty) return false; for (row_type r = 0; r < nrows(); ++r) - if (row_info[r].endfirsthead) + if (row_info[r].endfirsthead && + (withcaptions || !row_info[r].caption)) return true; return false; } -bool Tabular::haveLTFoot() const +bool Tabular::haveLTFoot(bool withcaptions) const { if (!is_long_tabular) return false; for (row_type r = 0; r < nrows(); ++r) - if (row_info[r].endfoot) + if (row_info[r].endfoot && + (withcaptions || !row_info[r].caption)) return true; return false; } -bool Tabular::haveLTLastFoot() const +bool Tabular::haveLTLastFoot(bool withcaptions) const { if (!is_long_tabular || endlastfoot.empty) return false; for (row_type r = 0; r < nrows(); ++r) - if (row_info[r].endlastfoot) + if (row_info[r].endlastfoot && + (withcaptions || !row_info[r].caption)) return true; return false; } @@ -1959,6 +1963,11 @@ Tabular::idx_type Tabular::setLTCaption(row_type row, bool what) setBottomLine(i, false); setLeftLine(i, false); setRightLine(i, false); + if (!row_info[row].endfirsthead && !row_info[row].endhead && + !row_info[row].endfoot && !row_info[row].endlastfoot) { + setLTHead(row, true, endfirsthead, true); + row_info[row].endfirsthead = true; + } } else { unsetMultiColumn(i); // When unsetting a caption row, also all existing @@ -1975,13 +1984,34 @@ bool Tabular::ltCaption(row_type row) const } -bool Tabular::haveLTCaption() const +bool Tabular::haveLTCaption(CaptionType captiontype) const { if (!is_long_tabular) return false; - for (row_type r = 0; r < nrows(); ++r) - if (row_info[r].caption) - return true; + for (row_type r = 0; r < nrows(); ++r) { + if (row_info[r].caption) { + switch (captiontype) { + case CAPTION_FIRSTHEAD: + if (row_info[r].endfirsthead) + return true; + break; + case CAPTION_HEAD: + if (row_info[r].endhead) + return true; + break; + case CAPTION_FOOT: + if (row_info[r].endfoot) + return true; + break; + case CAPTION_LASTFOOT: + if (row_info[r].endlastfoot) + return true; + break; + case CAPTION_ANY: + return true; + } + } + } return false; } @@ -2355,17 +2385,7 @@ void Tabular::TeXLongtableHeaderFooter(otexstream & os, if (!is_long_tabular) return; - // caption handling - // the caption must be output before the headers - if (haveLTCaption()) { - for (row_type r = 0; r < nrows(); ++r) { - if (row_info[r].caption) - TeXRow(os, r, runparams); - } - } // output first header info - // first header must be output before the header, otherwise the - // correct caption placement becomes really weird if (haveLTFirstHead()) { if (endfirsthead.topDL) os << "\\hline\n"; @@ -2487,7 +2507,7 @@ void Tabular::TeXRow(otexstream & os, row_type row, os << "\\textFR{"; else if (lang == "arabic_arabi") os << "\\textAR{"; - // currently, remaning RTL languages are + // currently, remaining RTL languages are // arabic_arabtex and hebrew else os << "\\R{"; @@ -2537,13 +2557,7 @@ void Tabular::TeXRow(otexstream & os, row_type row, os << " &\n"; } } - if (row_info[row].caption && !endfirsthead.empty && !haveLTFirstHead()) - // if no first header and no empty first header is used, - // the caption needs to be terminated by \endfirsthead - // (bug 6057) - os << "\\endfirsthead"; - else - os << "\\tabularnewline"; + os << "\\tabularnewline"; if (row_info[row].bottom_space_default) { if (use_booktabs) os << "\\addlinespace"; @@ -2809,6 +2823,7 @@ int Tabular::docbook(odocstream & os, OutputParams const & runparams) const //+--------------------------------------------------------------------- // output caption info + // The caption flag wins over head/foot if (haveLTCaption()) { os << "\n"; ++ret; @@ -2821,11 +2836,12 @@ int Tabular::docbook(odocstream & os, OutputParams const & runparams) const ++ret; } // output header info - if (haveLTHead() || haveLTFirstHead()) { + if (haveLTHead(false) || haveLTFirstHead(false)) { os << "\n"; ++ret; for (row_type r = 0; r < nrows(); ++r) { - if (row_info[r].endhead || row_info[r].endfirsthead) { + if ((row_info[r].endhead || row_info[r].endfirsthead) && + !row_info[r].caption) { ret += docbookRow(os, r, runparams); } } @@ -2833,11 +2849,12 @@ int Tabular::docbook(odocstream & os, OutputParams const & runparams) const ++ret; } // output footer info - if (haveLTFoot() || haveLTLastFoot()) { + if (haveLTFoot(false) || haveLTLastFoot(false)) { os << "\n"; ++ret; for (row_type r = 0; r < nrows(); ++r) { - if (row_info[r].endfoot || row_info[r].endlastfoot) { + if ((row_info[r].endfoot || row_info[r].endlastfoot) && + !row_info[r].caption) { ret += docbookRow(os, r, runparams); } } @@ -2943,6 +2960,7 @@ docstring Tabular::xhtml(XHTMLStream & xs, OutputParams const & runparams) const } xs << html::StartTag("div", "class='longtable' style='text-align: " + align + ";'") << html::CR(); + // The caption flag wins over head/foot if (haveLTCaption()) { xs << html::StartTag("div", "class='longtable-caption' style='text-align: " + align + ";'") << html::CR(); @@ -2956,30 +2974,32 @@ docstring Tabular::xhtml(XHTMLStream & xs, OutputParams const & runparams) const xs << html::StartTag("table") << html::CR(); // output header info - bool const havefirsthead = haveLTFirstHead(); + bool const havefirsthead = haveLTFirstHead(false); // if we have a first head, then we are going to ignore the // headers for the additional pages, since there aren't any // in XHTML. this test accomplishes that. - bool const havehead = !havefirsthead && haveLTHead(); + bool const havehead = !havefirsthead && haveLTHead(false); if (havehead || havefirsthead) { xs << html::StartTag("thead") << html::CR(); for (row_type r = 0; r < nrows(); ++r) { - if ((havefirsthead && row_info[r].endfirsthead) - || (havehead && row_info[r].endhead)) { + if (((havefirsthead && row_info[r].endfirsthead) || + (havehead && row_info[r].endhead)) && + !row_info[r].caption) { ret += xhtmlRow(xs, r, runparams, true); } } xs << html::EndTag("thead") << html::CR(); } // output footer info - bool const havelastfoot = haveLTLastFoot(); + bool const havelastfoot = haveLTLastFoot(false); // as before. - bool const havefoot = !havelastfoot && haveLTFoot(); + bool const havefoot = !havelastfoot && haveLTFoot(false); if (havefoot || havelastfoot) { xs << html::StartTag("tfoot") << html::CR(); for (row_type r = 0; r < nrows(); ++r) { - if ((havelastfoot && row_info[r].endlastfoot) - || (havefoot && row_info[r].endfoot)) { + if (((havelastfoot && row_info[r].endlastfoot) || + (havefoot && row_info[r].endfoot)) && + !row_info[r].caption) { ret += xhtmlRow(xs, r, runparams); } } @@ -4583,10 +4603,9 @@ bool InsetTabular::getStatus(Cursor & cur, FuncRequest const & cmd, break; // every row can only be one thing: - // either a footer or header or caption + // either a footer or header case Tabular::SET_LTFIRSTHEAD: - status.setEnabled(sel_row_start == sel_row_end - && !tabular.ltCaption(sel_row_start)); + status.setEnabled(sel_row_start == sel_row_end); status.setOnOff(tabular.getRowOfLTFirstHead(sel_row_start, dummyltt)); break; @@ -4595,8 +4614,7 @@ bool InsetTabular::getStatus(Cursor & cur, FuncRequest const & cmd, break; case Tabular::SET_LTHEAD: - status.setEnabled(sel_row_start == sel_row_end - && !tabular.ltCaption(sel_row_start)); + status.setEnabled(sel_row_start == sel_row_end); status.setOnOff(tabular.getRowOfLTHead(sel_row_start, dummyltt)); break; @@ -4605,8 +4623,7 @@ bool InsetTabular::getStatus(Cursor & cur, FuncRequest const & cmd, break; case Tabular::SET_LTFOOT: - status.setEnabled(sel_row_start == sel_row_end - && !tabular.ltCaption(sel_row_start)); + status.setEnabled(sel_row_start == sel_row_end); status.setOnOff(tabular.getRowOfLTFoot(sel_row_start, dummyltt)); break; @@ -4615,8 +4632,7 @@ bool InsetTabular::getStatus(Cursor & cur, FuncRequest const & cmd, break; case Tabular::SET_LTLASTFOOT: - status.setEnabled(sel_row_start == sel_row_end - && !tabular.ltCaption(sel_row_start)); + status.setEnabled(sel_row_start == sel_row_end); status.setOnOff(tabular.getRowOfLTLastFoot(sel_row_start, dummyltt)); break; @@ -4628,22 +4644,39 @@ bool InsetTabular::getStatus(Cursor & cur, FuncRequest const & cmd, status.setOnOff(tabular.getLTNewPage(sel_row_start)); break; - // only one row can be the caption + // only one row in head/firsthead/foot/lasthead can be the caption // and a multirow cannot be set as caption case Tabular::SET_LTCAPTION: - case Tabular::UNSET_LTCAPTION: - case Tabular::TOGGLE_LTCAPTION: status.setEnabled(sel_row_start == sel_row_end - && !tabular.getRowOfLTFirstHead(sel_row_start, dummyltt) - && !tabular.getRowOfLTHead(sel_row_start, dummyltt) - && !tabular.getRowOfLTFoot(sel_row_start, dummyltt) - && !tabular.getRowOfLTLastFoot(sel_row_start, dummyltt) - && (!tabular.haveLTCaption() - || tabular.ltCaption(sel_row_start)) + && (!tabular.getRowOfLTFirstHead(sel_row_start, dummyltt) + || !tabular.haveLTCaption(Tabular::CAPTION_FIRSTHEAD)) + && (!tabular.getRowOfLTHead(sel_row_start, dummyltt) + || !tabular.haveLTCaption(Tabular::CAPTION_HEAD)) + && (!tabular.getRowOfLTFoot(sel_row_start, dummyltt) + || !tabular.haveLTCaption(Tabular::CAPTION_FOOT)) + && (!tabular.getRowOfLTLastFoot(sel_row_start, dummyltt) + || !tabular.haveLTCaption(Tabular::CAPTION_LASTFOOT)) && !tabular.isMultiRow(sel_row_start)); status.setOnOff(tabular.ltCaption(sel_row_start)); break; + case Tabular::UNSET_LTCAPTION: + status.setEnabled(sel_row_start == sel_row_end && tabular.ltCaption(sel_row_start)); + break; + + case Tabular::TOGGLE_LTCAPTION: + status.setEnabled(sel_row_start == sel_row_end && (tabular.ltCaption(sel_row_start) + || ((!tabular.getRowOfLTFirstHead(sel_row_start, dummyltt) + || !tabular.haveLTCaption(Tabular::CAPTION_FIRSTHEAD)) + && (!tabular.getRowOfLTHead(sel_row_start, dummyltt) + || !tabular.haveLTCaption(Tabular::CAPTION_HEAD)) + && (!tabular.getRowOfLTFoot(sel_row_start, dummyltt) + || !tabular.haveLTCaption(Tabular::CAPTION_FOOT)) + && (!tabular.getRowOfLTLastFoot(sel_row_start, dummyltt) + || !tabular.haveLTCaption(Tabular::CAPTION_LASTFOOT))))); + status.setOnOff(tabular.ltCaption(sel_row_start)); + break; + case Tabular::SET_BOOKTABS: status.setOnOff(tabular.use_booktabs); break; diff --git a/src/insets/InsetTabular.h b/src/insets/InsetTabular.h index 81b6c14e5a..8546858ea9 100644 --- a/src/insets/InsetTabular.h +++ b/src/insets/InsetTabular.h @@ -317,6 +317,19 @@ public: BOX_MINIPAGE = 2 }; + enum CaptionType { + /// + CAPTION_FIRSTHEAD, + /// + CAPTION_HEAD, + /// + CAPTION_FOOT, + /// + CAPTION_LASTFOOT, + /// + CAPTION_ANY + }; + class ltType { public: // constructor @@ -505,8 +518,6 @@ public: // // Long Tabular Options support functions /// - bool checkLTType(row_type row, ltType const &) const; - /// void setLTHead(row_type row, bool flag, ltType const &, bool first); /// bool getRowOfLTHead(row_type row, ltType &) const; @@ -527,15 +538,15 @@ public: /// bool ltCaption(row_type row) const; /// - bool haveLTHead() const; + bool haveLTHead(bool withcaptions = true) const; /// - bool haveLTFirstHead() const; + bool haveLTFirstHead(bool withcaptions = true) const; /// - bool haveLTFoot() const; + bool haveLTFoot(bool withcaptions = true) const; /// - bool haveLTLastFoot() const; + bool haveLTLastFoot(bool withcaptions = true) const; /// - bool haveLTCaption() const; + bool haveLTCaption(CaptionType captiontype = CAPTION_ANY) const; /// // end longtable support /// diff --git a/src/tex2lyx/table.cpp b/src/tex2lyx/table.cpp index 06ad747dd0..fe6b8cf497 100644 --- a/src/tex2lyx/table.cpp +++ b/src/tex2lyx/table.cpp @@ -1171,45 +1171,28 @@ void handle_tabular(Parser & p, ostream & os, string const & name, // one multicolumn cell. The contents of that // cell must contain exactly one caption inset // and nothing else. - // LyX outputs all caption rows as first head, - // so we must not set the caption flag for - // captions not in the first head. // Fortunately, the caption flag is only needed // for tables with more than one column. - bool usecaption = (rowinfo[row].type == LT_NORMAL || - rowinfo[row].type == LT_FIRSTHEAD); - for (size_t r = 0; r < row && usecaption; ++r) - if (rowinfo[row].type != LT_NORMAL && - rowinfo[row].type != LT_FIRSTHEAD) - usecaption = false; - if (usecaption) { - rowinfo[row].caption = true; - for (size_t c = 1; c < cells.size(); ++c) { - if (!cells[c].empty()) { - cerr << "Moving cell content '" - << cells[c] - << "' into the caption cell. " - "This will probably not work." - << endl; - cells[0] += cells[c]; - } + rowinfo[row].caption = true; + for (size_t c = 1; c < cells.size(); ++c) { + if (!cells[c].empty()) { + cerr << "Moving cell content '" + << cells[c] + << "' into the caption cell. " + "This will probably not work." + << endl; + cells[0] += cells[c]; } - cells.resize(1); - cellinfo[row][col].align = colinfo[col].align; - cellinfo[row][col].multi = CELL_BEGIN_OF_MULTICOLUMN; - } else { - cellinfo[row][col].leftlines = colinfo[col].leftlines; - cellinfo[row][col].rightlines = colinfo[col].rightlines; - cellinfo[row][col].align = colinfo[col].align; } + cells.resize(1); + cellinfo[row][col].align = colinfo[col].align; + cellinfo[row][col].multi = CELL_BEGIN_OF_MULTICOLUMN; ostringstream os; parse_text_in_inset(p, os, FLAG_CELL, false, context); cellinfo[row][col].content += os.str(); - if (usecaption) { - // add dummy multicolumn cells - for (size_t c = 1; c < colinfo.size(); ++c) - cellinfo[row][c].multi = CELL_PART_OF_MULTICOLUMN; - } + // add dummy multicolumn cells + for (size_t c = 1; c < colinfo.size(); ++c) + cellinfo[row][c].multi = CELL_PART_OF_MULTICOLUMN; } else { cellinfo[row][col].leftlines = colinfo[col].leftlines; cellinfo[row][col].rightlines = colinfo[col].rightlines; diff --git a/src/version.h b/src/version.h index 92e2958b44..ae495126f7 100644 --- a/src/version.h +++ b/src/version.h @@ -30,8 +30,8 @@ extern char const * const lyx_version_info; // Do not remove the comment below, so we get merge conflict in // independent branches. Instead add your own. -#define LYX_FORMAT_LYX 420 // jrioux : document-wide bibliography style -#define LYX_FORMAT_TEX2LYX 420 // jrioux : document-wide bibliography style +#define LYX_FORMAT_LYX 421 // baum : longtable captions +#define LYX_FORMAT_TEX2LYX 421 #if LYX_FORMAT_FOR_TEX2LYX != LYX_FORMAT_FOR_LYX #warning "tex2lyx produces an out of date file format." -- 2.39.2