Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions devel/221_2.md
Original file line number Diff line number Diff line change
@@ -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 中脚注正文通过 `<float|footnote|...>` 进入页面插入通道
src/Typeset/Bridge/bridge_gui.cpp 中:
1. 为 ornament 内部生成的 `array<page_item>` 增加 `fl` 收集逻辑
2. 在 `insert_ornament` 将外框内容重新插入段落时,把收集到的 `fl` 重新挂回外层 page item
src/Typeset/Line/lazy_gui.cpp 中同步补充 lazy ornament / art_box 路径,对内部 `fl` 进行收集并重新附着,避免不同渲染链行为不一致

### 备注
本次问题的根因不是脚注没有生成,而是有框定理在 ornament 层重新包装内容时,没有保留脚注对应的页面 float。
29 changes: 26 additions & 3 deletions src/Typeset/Bridge/bridge_gui.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<lazy> ornament_fl;

public:
bridge_ornamented_rep (typesetter ttt, tree st, path ip, int idx);
Expand Down Expand Up @@ -158,6 +159,18 @@ make_ornament_body (path ip, array<page_item> 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) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

doxygen style comment in Chinese

int i;
Expand All @@ -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);
Expand All @@ -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);
}

Expand Down
8 changes: 8 additions & 0 deletions src/Typeset/Format/page_item.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,11 @@ operator<< (tm_ostream& out, page_item item) {
}
return out << "unknown";
}

array<lazy>
collect_attached_floats (array<page_item> items) {
array<lazy> fl;
for (int i= 0; i < N (items); i++)
if (N (items[i]->fl) > 0) fl << items[i]->fl;
return fl;
}
1 change: 1 addition & 0 deletions src/Typeset/Format/page_item.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -52,5 +52,6 @@ class page_item {
CONCRETE_NULL_CODE (page_item);

tm_ostream& operator<< (tm_ostream& out, page_item item);
array<lazy> collect_attached_floats (array<page_item> items);

#endif // defined PAGE_ITEM_H
70 changes: 64 additions & 6 deletions src/Typeset/Line/lazy_gui.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<lazy> 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);
Expand All @@ -203,7 +232,7 @@ lazy_ornament_rep::produce (lazy_type request, format fm) {
if (request == LAZY_BOX) return make_lazy_box (hb);
else {
array<page_item> l;
l << page_item (hb);
l << page_item (hb, fl);
return lazy_vstream (ip, "", l, stack_border ());
}
}
Expand Down Expand Up @@ -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<lazy> 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
Expand All @@ -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<page_item> l;
l << page_item (hb);
l << page_item (hb, fl);
return lazy_vstream (ip, "", l, stack_border ());
}
}
Expand Down
116 changes: 116 additions & 0 deletions tests/Typeset/Bridge/bridge_gui/bridge_ornamented_rep_test.cpp
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/gpl-3.0.html>.
******************************************************************************/

#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 <QtTest/QtTest>
#include <moebius/drd/drd_std.hpp>

using namespace moebius;
using moebius::drd::std_drd;

static edit_env
create_test_env () {
drd_info drd ("none", std_drd);
hashmap<string, tree> h1 (UNINIT), h2 (UNINIT);
hashmap<string, tree> h3 (UNINIT), h4 (UNINIT);
hashmap<string, tree> 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<page_item> 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<lazy>
collect_attached_floats_for_test (array<page_item> items) {
array<lazy> 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<page_item> inner_items (1);
array<page_item> footnote_lines (1);
footnote_lines[0]= page_item (empty_box (path (0)));
array<lazy> 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<lazy> 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"
Loading
Loading