文档段落保留功能实现分析:基于层级与权限的标题保留策略

一、问题背景

在文档处理场景中,用户踢出需求,需要根据以下规则隐藏或保留段落:

  1. 标题保留条件:仅当标题的子段落中存在有权限的填报项时,该标题才被保留。
  2. 段落类型区分
    • 正文/表格:根据权限决定是否保留。
    • 标题:需结合子段落的保留状态判断是否保留。
  3. 层级嵌套复杂性:标题的层级(如一级标题、二级标题)需通过段落层级信息(OutLevel)动态计算其子段落范围。

存在的典型问题

  • 标题范围计算错误:未正确记录标题的结束位置,导致误判子段落范围。
  • 保留条件不精准:未区分“有权限填报项”与其他保留类型(如正文),导致标题被错误保留。

二、解决思路

1. 分阶段处理逻辑

将问题拆解为三个阶段,逐步解决:

  1. 基础保留标记:标记每个段落的保留状态(retained数组)。
  2. 标题范围与保留判断:通过层级嵌套关系确定标题的子段落范围,并判断是否保留。
  3. 结果生成:根据保留标记生成最终的删除/保留指令。

2. 关键逻辑设计

(1) 标题范围计算

  • 使用栈结构:通过栈记录当前层级的标题,动态维护标题的嵌套关系。
  • 层级比较:当遇到新标题时,若其层级≤栈顶标题层级,则弹出栈顶标题并记录其结束位置。
  • 结束位置处理:遍历结束后,栈中剩余标题的结束位置设为文档末尾。

(2) 保留条件细化

  • 段落保留规则
    • 正文/表格:仅保留有权限的填报项。
    • 标题:仅保留子段落中存在有权限填报项的标题。
  • 权限判断:通过FillInfo对象获取段落的填报项权限信息。

(3) 标题保留判断

  • 遍历子段落:对每个标题的子段落范围(startend),检查是否存在有权限的填报项。
  • 标记保留状态:若满足条件,将标题的titleShouldKeep标记为true

三、关键代码解析

1. 基础保留标记(retained数组)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 标记每个段落的保留状态
for (int i = 0; i < size; i++) {
Para p = paras.get(i);
int paraOutLevel = XxxUtil.getParaOutLevel(p, styles);
FillInfo fillItemPermission = getFillItemPermission(fillItemList, p.getPos());
boolean hasPermission = fillItemPermission.isHasPermission();
FillItem fillItem = fillItemPermission.getFillItem();
if (!hasPermission && fillItem != null) {
retained[i] = false; // 无权限的填报项不保留
} else if (paraOutLevel == 0) {
// 正文或填报项:仅保留有权限的
retained[i] = hasPermission;
} else {
// 标题或表格:根据层级是否需要保留
if (levelsToKeep.contains(paraOutLevel)) {
retained[i] = true; // 标题先标记为保留,后续根据子段落修正
}
}
}

关键点

  • 权限优先:无权限的填报项直接标记为不保留。
  • 层级过滤:仅保留需要显示的层级(如levelsToKeep中的层级)。

2. 标题范围计算(栈结构)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 确定标题的子段落范围
List<Range> titleRanges = new ArrayList<>();
Stack<Range> stack = new Stack<>();
for (int i = 0; i < size; i++) {
Para p = allPapx.get(i);
int paraOutLevel = DocDataUtil.getParaOutLevel(p, styles);
if (paraOutLevel > 0 && levelsToKeep.contains(paraOutLevel)) {
while (!stack.isEmpty() && paraOutLevel <= stack.peek().getLevel()) {
Range popped = stack.pop();
popped.setEnd(i - 1); // 当前段落的前一个位置为结束
titleRanges.add(popped);
}
Range newRange = new Range();
newRange.setStart(i);
newRange.setLevel(paraOutLevel);
stack.push(newRange);
}
}
// 处理栈中剩余标题
while (!stack.isEmpty()) {
Range r = stack.pop();
r.setEnd(size - 1); // 结束位置为文档末尾
titleRanges.add(r);
}

关键点

  • 栈的层级判断:通过层级比较确保标题范围的正确嵌套。
  • 结束位置修正:遍历结束后,确保所有标题范围到文档末尾。

3. 标题保留判断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 判断标题是否保留
for (Range r : titleRanges) {
boolean hasPermissionFilledForm = false;
for (int j = r.getStart() + 1; j <= r.getEnd(); j++) {
FillInfo fillItemPermission = getFillItemPermission(fillItemList, allPapx.get(j).getPos());
if (retained[j] && fillItemPermission.isHasPermission()) {
hasPermissionFilledForm = true;
break;
}
}
if (hasPermissionFilledForm) {
titleShouldKeep[r.getStart()] = true; // 标记标题保留
}
}

关键点

  • 子段落遍历:检查标题的子段落(start+1end)中是否存在有权限的填报项。
  • 权限双重验证:仅当retained[j]true(被基础保留)且hasPermissiontrue(有权限)时,才视为有效。

4. 结果生成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 生成最终的删除/保留指令
List<ResultItem> result = new ArrayList<>();
int i = 0;
while (i < size) {
Para p = paras.get(i);
int paraOutLevel = XxxUtil.getParaOutLevel(p, styles);
if (paraOutLevel > 0 && levelsToKeep.contains(paraOutLevel)) {
// 标题的保留状态由titleShouldKeep决定
retained[i] = titleShouldKeep[i];
}
// 处理表格或普通段落
if (/* 是表格 */) {
// 删除表格并跳过所有表格段落
} else {
if (!retained[i]) {
// 记录删除指令
result.add(new ResultItem(p.getPos(), p.getLen(), 0));
}
i++;
}
}

关键点

  • 标题保留状态更新:根据titleShouldKeep覆盖标题的retained标记。
  • 统一处理:通过retained数组统一判断段落的保留或删除。

四、验证与测试

1. 测试案例

以用户提供的案例为例:

1
2
3
4
5
6
7
8
段落索引 | 内容              | 层级 | 是否保留
-----------------------------------------------
0 | 一级1 | 1 | false(无子段落有权限填报项)
1 | 二级1 | 2 | false
... | ... | ... | ...
18 | 一级2 | 1 | true(子段落包含有权限填报项)
21 | 二级10 | 2 | true(子段落包含有权限填报项)
25 | 有权限填报项 | 0 | true

验证逻辑

  • 一级2的保留:子段落(索引19-25)包含索引25的有权限填报项 → 保留。
  • 二级10的保留:子段落包含索引25的有权限填报项 → 保留。
  • 一级1的不保留:子段落中无有权限的填报项 → 不保留。

2. 边界条件测试

  • 无子段落的标题:若标题无子段落,不保留。
  • 层级嵌套交叉:如一级→二级→三级的复杂结构,确保范围计算正确。

五、总结与扩展

1. 解决方案优势

  • 精准控制:通过层级栈和权限双重判断,确保标题保留的条件严格符合需求。
  • 可扩展性:通过levelsToKeep和权限接口,可灵活适配不同业务场景。

2. 可优化方向

  • 性能优化:对大规模文档,可优化子段落遍历逻辑(如预处理子段落列表)。
  • 异常处理:增加对层级不合法或权限信息缺失的容错机制。