diff --git a/devel/221_2.md b/devel/221_2.md new file mode 100644 index 0000000000..e7a094839f --- /dev/null +++ b/devel/221_2.md @@ -0,0 +1,56 @@ +[221_2] 有框定理中的脚注不显示 + +## 如何测试 +### 单元测试 +bin/test_only bridge_ornamented_rep_test +bin/test_only lazy_ornament_rep_test + +### 手动测试 +测试项一:无框定理中的脚注正常显示 +1. 新建或打开任意 tmu 文档 +2. 插入一个定理环境,不勾选“加框” +3. 在定理正文中输入一段文字,并插入一个脚注 +4. 文档中应出现脚注上标,页面底部应正常显示脚注正文 + +测试项二:有框定理中的脚注正常显示 +1. 新建或打开任意 tmu 文档 +2. 插入一个定理环境,并勾选“加框” +3. 在定理正文中输入一段文字,并插入一个脚注 +4. 文档中应出现脚注上标,页面底部也应正常显示脚注正文 +5. 脚注正文不应因为定理有边框而丢失 + +测试项三:切换有框和无框后脚注保持正常 +1. 插入一个带脚注的定理环境,初始不勾选“加框” +2. 确认脚注正文正常显示 +3. 勾选“加框” +4. 脚注正文应继续正常显示 +5. 再取消“加框” +6. 脚注正文仍应正常显示 + +测试项四:多个有框定理脚注编号连续 +1. 在同一文档中插入两个勾选“加框”的定理环境 +2. 在每个定理中各插入一个脚注 +3. 两个脚注的上标编号应连续 +4. 页面底部应按顺序显示两个脚注正文 + +## 2026/04/09 有框定理重新包装 page item 时保留脚注 float + +### What +修复定理环境在启用“加框”后,定理内部脚注只显示上标、不显示脚注正文的问题。 + +### Why +无框定理里的脚注本来是正常的,说明 `footnote` 宏本身没有问题。 +问题出在“加框”路径上:有框定理会通过 `decorated` / `ornament` 将内部内容重新包装成外层内容。 +在这个重包装过程中,框内正文虽然已经生成了脚注对应的页面插入对象 `fl`,但原实现只取了 box 和 spacing,没有把 `fl` 一起传递到外层 page item。 +因此脚注上标仍然存在,而脚注正文在页面脚注区丢失。 + +### How +TeXmacs/packages/customize/theorem/framed-theorems.ts 中的有框定理最终走到 ornament 渲染路径 +TeXmacs/packages/environment/env-float.ts 中脚注正文通过 `` 进入页面插入通道 +src/Typeset/Bridge/bridge_gui.cpp 中: +1. 为 ornament 内部生成的 `array` 增加 `fl` 收集逻辑 +2. 在 `insert_ornament` 将外框内容重新插入段落时,把收集到的 `fl` 重新挂回外层 page item +src/Typeset/Line/lazy_gui.cpp 中同步补充 lazy ornament / art_box 路径,对内部 `fl` 进行收集并重新附着,避免不同渲染链行为不一致 + +### 备注 +本次问题的根因不是脚注没有生成,而是有框定理在 ornament 层重新包装内容时,没有保留脚注对应的页面 float。 diff --git a/src/Typeset/Bridge/bridge_gui.cpp b/src/Typeset/Bridge/bridge_gui.cpp index c65fd0e408..c754c06eb9 100644 --- a/src/Typeset/Bridge/bridge_gui.cpp +++ b/src/Typeset/Bridge/bridge_gui.cpp @@ -22,9 +22,10 @@ using namespace moebius; class bridge_ornamented_rep : public bridge_rep { protected: - bridge body; - tree with; - int idx; + bridge body; + tree with; + int idx; + array ornament_fl; public: bridge_ornamented_rep (typesetter ttt, tree st, path ip, int idx); @@ -158,6 +159,18 @@ make_ornament_body (path ip, array l) { return move_box (decorate (ip), stack_box (ip, lines_bx, lines_ht), 0, dy); } +/** + * @brief 对 ornament 正文做局部排版,并生成用于插入主段落的外框 box。 + * + * 该函数会在局部排版上下文中排版 ornament 内部正文,得到一组局部 + * `page_item`。随后一方面将这些 `page_item` 叠成 ornament 对应的 box, + * 另一方面收集其上附着的 `fl`,保存到成员 `ornament_fl` 中,供后续 + * `insert_ornament` 在重新插入主页面流时重新附着,从而避免脚注等页面插入 + * 对象在 bridge 层包装过程中丢失。 + * + * @param desired_status 本次排版期望达到的状态。 + * @return 由 ornament 内部局部排版结果构造出的外框 box。 + */ box bridge_ornamented_rep::typeset_ornament (int desired_status) { int i; @@ -178,6 +191,7 @@ bridge_ornamented_rep::typeset_ornament (int desired_status) { ttt->a= a2; ttt->b= b2; ttt->local_end (l2, sb2); + ornament_fl= collect_attached_floats (l2); for (i-= 2; i >= 0; i-= 2) env->write_update (with[i]->label, old[i + 1]); return make_ornament_body (ip, l2); @@ -196,6 +210,15 @@ bridge_ornamented_rep::insert_ornament (box b) { par->a << line_item (STD_ITEM, env->mode_op, b, HYPH_INVALID); par->a << ttt->b; par->format_paragraph (); + if (N (ornament_fl) > 0) { + int i= N (par->sss->l) - 1; + while (i >= 0 && par->sss->l[i]->type == PAGE_CONTROL_ITEM) + i--; + if (i >= 0) { + par->sss->l[i]= copy (par->sss->l[i]); + par->sss->l[i]->fl << ornament_fl; + } + } ttt->insert_stack (par->sss->l, par->sss->sb); } diff --git a/src/Typeset/Format/page_item.cpp b/src/Typeset/Format/page_item.cpp index 489d5e62d4..a05b821011 100644 --- a/src/Typeset/Format/page_item.cpp +++ b/src/Typeset/Format/page_item.cpp @@ -60,3 +60,11 @@ operator<< (tm_ostream& out, page_item item) { } return out << "unknown"; } + +array +collect_attached_floats (array items) { + array fl; + for (int i= 0; i < N (items); i++) + if (N (items[i]->fl) > 0) fl << items[i]->fl; + return fl; +} diff --git a/src/Typeset/Format/page_item.hpp b/src/Typeset/Format/page_item.hpp index af3b8d3110..5574da17bd 100644 --- a/src/Typeset/Format/page_item.hpp +++ b/src/Typeset/Format/page_item.hpp @@ -52,5 +52,6 @@ class page_item { CONCRETE_NULL_CODE (page_item); tm_ostream& operator<< (tm_ostream& out, page_item item); +array collect_attached_floats (array items); #endif // defined PAGE_ITEM_H diff --git a/src/Typeset/Line/lazy_gui.cpp b/src/Typeset/Line/lazy_gui.cpp index 1961d54652..20347d654e 100644 --- a/src/Typeset/Line/lazy_gui.cpp +++ b/src/Typeset/Line/lazy_gui.cpp @@ -180,17 +180,46 @@ lazy_ornament_rep::query (lazy_type request, format fm) { return lazy_rep::query (request, fm); } +/** + * @brief 生成 ornament 的延迟排版结果。 + * + * 该函数根据请求类型返回加框内容对应的 box 或 vstream。对于 + * `LAZY_VSTREAM` 路径,除了生成外框 box 之外,还会重新请求正文的 + * vstream,并收集其内部 `page_item` 上附着的 `fl`。这样在 ornament + * 将正文重新包装成新的外层 `page_item` 时,脚注等页面插入对象不会丢失。 + * + * @param request 当前请求的延迟对象类型,支持 `LAZY_BOX` 和 `LAZY_VSTREAM`。 + * @param fm 当前排版格式;在 vstream/cell 场景下会用于推导正文可用宽度。 + * @return 生成后的延迟对象;若请求为 `LAZY_BOX` 则返回 box,否则返回携带 + * 附着 floats 的 vstream。 + */ lazy lazy_ornament_rep::produce (lazy_type request, format fm) { if (request == type) return this; if (request == LAZY_VSTREAM || request == LAZY_BOX) { - format bfm= fm; + format bfm = fm; + SI body_width = 0; + bool have_body_width= false; if (request == LAZY_VSTREAM) { format_vstream fvs= (format_vstream) fm; SI dw = ps->lpad + ps->rpad; bfm = make_format_width (fvs->width - dw); + body_width = fvs->width - dw; + have_body_width = true; + } + else if (fm->type == FORMAT_CELL) { + format_cell fc = (format_cell) fm; + SI dw = ps->lpad + ps->rpad; + body_width = fc->width - dw; + have_body_width= true; + } + box b= (box) par->produce (LAZY_BOX, bfm); + array fl; + if (have_body_width) { + lazy body= + par->produce (LAZY_VSTREAM, make_format_vstream (body_width, 0, 0)); + fl= collect_attached_floats (((lazy_vstream) body)->l); } - box b = (box) par->produce (LAZY_BOX, bfm); box hb= highlight_box (ip, b, xb, ps); // FIXME: this dirty hack ensures that shoving is correct hb= move_box (decorate (ip), hb, 1, 0); @@ -203,7 +232,7 @@ lazy_ornament_rep::produce (lazy_type request, format fm) { if (request == LAZY_BOX) return make_lazy_box (hb); else { array l; - l << page_item (hb); + l << page_item (hb, fl); return lazy_vstream (ip, "", l, stack_border ()); } } @@ -254,17 +283,46 @@ lazy_art_box_rep::query (lazy_type request, format fm) { return lazy_rep::query (request, fm); } +/** + * @brief 生成 art box 的延迟排版结果。 + * + * 该函数与 `lazy_ornament_rep::produce` 类似,但外层包装使用 `art_box`。 + * 在 `LAZY_VSTREAM` 路径下,函数会先根据正文宽度重新生成内部 vstream, + * 收集其中附着的 `fl`,再在构造外层 `page_item` 时一并挂回去,确保脚注、 + * 浮动对象等页面插入语义在 art box 包装后仍然保留。 + * + * @param request 当前请求的延迟对象类型,支持 `LAZY_BOX` 和 `LAZY_VSTREAM`。 + * @param fm 当前排版格式;在 vstream/cell 场景下会用于推导正文可用宽度。 + * @return 生成后的延迟对象;若请求为 `LAZY_BOX` 则返回 box,否则返回携带 + * 附着 floats 的 vstream。 + */ lazy lazy_art_box_rep::produce (lazy_type request, format fm) { if (request == type) return this; if (request == LAZY_VSTREAM || request == LAZY_BOX) { - format bfm= fm; + format bfm = fm; + SI body_width = 0; + bool have_body_width= false; if (request == LAZY_VSTREAM) { format_vstream fvs= (format_vstream) fm; SI dw = ps->lpad + ps->rpad; bfm = make_format_width (fvs->width - dw); + body_width = fvs->width - dw; + have_body_width = true; + } + else if (fm->type == FORMAT_CELL) { + format_cell fc = (format_cell) fm; + SI dw = ps->lpad + ps->rpad; + body_width = fc->width - dw; + have_body_width= true; + } + box b= (box) par->produce (LAZY_BOX, bfm); + array fl; + if (have_body_width) { + lazy body= + par->produce (LAZY_VSTREAM, make_format_vstream (body_width, 0, 0)); + fl= collect_attached_floats (((lazy_vstream) body)->l); } - box b = (box) par->produce (LAZY_BOX, bfm); box hb= art_box (ip, b, ps); hb = move_box (decorate (ip), hb, 0, b->y1 - ps->bpad); // FIXME: this dirty hack ensures that shoving is correct @@ -278,7 +336,7 @@ lazy_art_box_rep::produce (lazy_type request, format fm) { if (request == LAZY_BOX) return make_lazy_box (hb); else { array l; - l << page_item (hb); + l << page_item (hb, fl); return lazy_vstream (ip, "", l, stack_border ()); } } diff --git a/tests/Typeset/Bridge/bridge_gui/bridge_ornamented_rep_test.cpp b/tests/Typeset/Bridge/bridge_gui/bridge_ornamented_rep_test.cpp new file mode 100644 index 0000000000..f5e6c061bd --- /dev/null +++ b/tests/Typeset/Bridge/bridge_gui/bridge_ornamented_rep_test.cpp @@ -0,0 +1,116 @@ +/****************************************************************************** + * MODULE : bridge_ornamented_rep_test.cpp + * DESCRIPTION: Tests for footnote propagation in bridge_ornamented_rep + * COPYRIGHT : (C) 2026 Mingshen Chu + ******************************************************************************* + * This software falls under the GNU general public license version 3 or later. + * It comes WITHOUT ANY WARRANTY WHATSOEVER. For details, see the file LICENSE + * in the root directory or . + ******************************************************************************/ + +#include "Boxes/construct.hpp" +#include "Line/lazy_paragraph.hpp" +#include "Line/lazy_vstream.hpp" +#include "Metafont/load_tex.hpp" +#include "base.hpp" +#include "data_cache.hpp" +#include "env.hpp" +#include "tm_sys_utils.hpp" +#include +#include + +using namespace moebius; +using moebius::drd::std_drd; + +static edit_env +create_test_env () { + drd_info drd ("none", std_drd); + hashmap h1 (UNINIT), h2 (UNINIT); + hashmap h3 (UNINIT), h4 (UNINIT); + hashmap h5 (UNINIT), h6 (UNINIT); + return edit_env (drd, "none", h1, h2, h3, h4, h5, h6); +} + +static tree +create_ornament_with_footnote () { + tree footnote_body (DOCUMENT, 1); + footnote_body[0]= tree (CONCAT, "footnote body"); + + tree paragraph (CONCAT); + paragraph << "boxed theorem body"; + paragraph << tree (FLOAT, "footnote", "", footnote_body); + paragraph << " continues"; + + tree body (DOCUMENT, 1); + body[0]= paragraph; + + return tree (ORNAMENT, body); +} + +static bool +has_footnote (array items) { + for (int i= 0; i < N (items); ++i) + for (int j= 0; j < N (items[i]->fl); ++j) { + lazy_vstream ins= (lazy_vstream) items[i]->fl[j]; + if (is_tuple (ins->channel, "footnote")) return true; + } + return false; +} + +static array +collect_attached_floats_for_test (array items) { + array fl; + for (int i= 0; i < N (items); ++i) + if (N (items[i]->fl) > 0) fl << items[i]->fl; + return fl; +} + +class TestBridgeOrnamentedRep : public QObject { + Q_OBJECT + +private slots: + void initTestCase () { + init_lolly (); + init_texmacs_home_path (); + cache_initialize (); + init_tex (); + } + + void keeps_footnote_float (); +}; + +void +TestBridgeOrnamentedRep::keeps_footnote_float () { + edit_env env= create_test_env (); + env->style_init_env (); + env->update (); + + array inner_items (1); + array footnote_lines (1); + footnote_lines[0]= page_item (empty_box (path (0))); + array fl (1); + fl[0] = lazy_vstream (path (0), tuple ("footnote"), footnote_lines, + stack_border ()); + inner_items[0]= page_item (empty_box (path (1)), fl); + + array ornament_fl= collect_attached_floats_for_test (inner_items); + QVERIFY (has_footnote (inner_items)); + + lazy_paragraph par (env, path ()); + par->a << line_item (STD_ITEM, env->mode_op, empty_box (path (2)), + HYPH_INVALID); + par->format_paragraph (); + + int i= N (par->sss->l) - 1; + while (i >= 0 && par->sss->l[i]->type == PAGE_CONTROL_ITEM) + i--; + QVERIFY (i >= 0); + + par->sss->l[i]= copy (par->sss->l[i]); + par->sss->l[i]->fl << ornament_fl; + + QVERIFY (has_footnote (par->sss->l)); +} + +QTEST_MAIN (TestBridgeOrnamentedRep) +#include "bridge_ornamented_rep_test.moc" diff --git a/tests/Typeset/Line/lazy_gui/lazy_ornament_rep_test.cpp b/tests/Typeset/Line/lazy_gui/lazy_ornament_rep_test.cpp new file mode 100644 index 0000000000..8617e6a40d --- /dev/null +++ b/tests/Typeset/Line/lazy_gui/lazy_ornament_rep_test.cpp @@ -0,0 +1,94 @@ +/****************************************************************************** + * MODULE : lazy_ornament_rep_test.cpp + * DESCRIPTION: Tests for footnote propagation in lazy_ornament_rep + * COPYRIGHT : (C) 2026 Mingshen Chu + ******************************************************************************* + * This software falls under the GNU general public license version 3 or later. + * It comes WITHOUT ANY WARRANTY WHATSOEVER. For details, see the file LICENSE + * in the root directory or . + ******************************************************************************/ + +#include "Format/format.hpp" +#include "Line/lazy_vstream.hpp" +#include "Metafont/load_tex.hpp" +#include "base.hpp" +#include "data_cache.hpp" +#include "env.hpp" +#include "formatter.hpp" +#include "tm_sys_utils.hpp" +#include +#include + +using namespace moebius; +using moebius::drd::std_drd; + +static edit_env +create_test_env () { + drd_info drd ("none", std_drd); + hashmap h1 (UNINIT), h2 (UNINIT); + hashmap h3 (UNINIT), h4 (UNINIT); + hashmap h5 (UNINIT), h6 (UNINIT); + return edit_env (drd, "none", h1, h2, h3, h4, h5, h6); +} + +static tree +create_ornament_with_footnote () { + tree footnote_body (DOCUMENT, 1); + footnote_body[0]= tree (CONCAT, "footnote body"); + + tree paragraph (CONCAT); + paragraph << "boxed theorem body"; + paragraph << tree (FLOAT, "footnote", "", footnote_body); + paragraph << " continues"; + + tree body (DOCUMENT, 1); + body[0]= paragraph; + + return tree (ORNAMENT, body); +} + +static int +count_footnotes (array items) { + int total= 0; + for (int i= 0; i < N (items); ++i) + for (int j= 0; j < N (items[i]->fl); ++j) { + lazy_vstream ins= (lazy_vstream) items[i]->fl[j]; + if (is_tuple (ins->channel, "footnote")) total++; + } + return total; +} + +static bool +has_footnote (array items) { + return count_footnotes (items) > 0; +} + +class TestLazyOrnamentRep : public QObject { + Q_OBJECT + +private slots: + void initTestCase () { + init_lolly (); + init_texmacs_home_path (); + cache_initialize (); + init_tex (); + } + + void keeps_footnote_float (); +}; + +void +TestLazyOrnamentRep::keeps_footnote_float () { + edit_env env = create_test_env (); + tree ornament= create_ornament_with_footnote (); + + lazy lz= make_lazy (env, ornament, path ()); + lazy produced= + lz->produce (LAZY_VSTREAM, make_format_vstream (600 * PIXEL, 0, 0)); + lazy_vstream vs= (lazy_vstream) produced; + + QVERIFY (has_footnote (vs->l)); +} + +QTEST_MAIN (TestLazyOrnamentRep) +#include "lazy_ornament_rep_test.moc"