Skip to content

Commit 4f1b659

Browse files
committed
fix:1.3.4 (a)
- **重构:统一闭合归属判定** - 新增统一的归属仲裁函数,集中处理 shorthand / full-form 的闭合竞争。 - shorthand 的 push、close、fallback 恢复路径改为复用同一套规则。 - **修复:full-form close 与 shorthand continuation 竞争时,优先 full-form** - 当 `tagClose` 与父级 `endTag` 在同一游标重叠时,优先保留父级 full-form 闭合归属。 - 避免 shorthand 抢占父级闭合,覆盖 `=bold<bold<=bold<>=>=` 及等价自定义分隔符场景。 - **修复:endTag 延迟确认显式化,且对截断安全** - endTag 探测结果细分为 `full` / `truncated-prefix` / `none`。 - 仅 `full` 视为可消费闭合;命中截断前缀时立即解除延迟并恢复常规扫描。 - **模型一致性** - 移除分支内各自猜测式判优,收敛为单一、确定的归属规则。 - 无公共 API 变化
1 parent 0887b5d commit 4f1b659

3 files changed

Lines changed: 102 additions & 51 deletions

File tree

CHANGELOG.md

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,17 @@
44

55
### 1.3.4
66

7-
- **Fix: resolve shorthand/full-form close conflicts by preserving full-form close ownership**
8-
- In inline-arg context, when shorthand close (`tagClose`) overlaps a parent full-form close (`endTag`), the parser now prioritizes full-form close ownership.
9-
- This fixes conflict cases such as `=bold<bold<=bold<>=>=` and nested custom-syntax variants without introducing broad fallback heuristics.
10-
- **Parser model refinement: local overlap decision instead of long-range rescue scanning**
11-
- Removed forward-looking rescue behavior that depended on scanning ahead for another `endTag`.
12-
- Conflict handling is now decided at the current cursor position with stack-top frame semantics, keeping behavior deterministic and consistent with stepwise parsing.
13-
- **Fix: delayed endTag confirmation with truncation-safe matching**
14-
- Added explicit end-tag match states (`full` / `truncated-prefix` / `none`) for overlap points.
15-
- If an `endTag` prefix is truncated, delay is dropped immediately and scanning continues as text; only `full` matches are treated as close tokens.
16-
- **Performance: avoid redundant overlap work in shorthand ambiguity path**
17-
- Kept shorthand probe reuse and switched overlap checks to bounded match evaluation at the cursor, reducing unnecessary repeated scans in conflict-heavy inputs.
7+
- **Refactor: unified close-ownership arbitration**
8+
- Added one shared ownership resolver for shorthand/full-form close conflicts.
9+
- The same rule is now used in shorthand push, shorthand close, and fallback resume paths.
10+
- **Fix: full-form close wins when competing with shorthand continuation**
11+
- When `tagClose` and parent `endTag` overlap at the same cursor, parser keeps ownership for the parent full-form close.
12+
- Prevents shorthand from stealing parent closure in conflict cases such as `=bold<bold<=bold<>=>=` (and equivalent custom delimiters).
13+
- **Fix: delayed endTag confirmation is explicit and truncation-safe**
14+
- End-tag probe now returns `full` / `truncated-prefix` / `none`.
15+
- Only `full` consumes close; truncated prefix immediately exits delay and resumes normal scan.
16+
- **Model consistency**
17+
- Removed branch-local conflict guessing and consolidated to one deterministic ownership rule.
1818
- No public API changes
1919

2020
### 1.3.3

CHANGELOG.zh-CN.md

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,17 @@
44

55
### 1.3.4
66

7-
- **修复:shorthand / full-form 闭合冲突时,优先保留 full-form 闭合归属**
8-
- 在 inline 参数上下文中,当 shorthand 的闭合符(`tagClose`)与父级 full-form 闭合(`endTag`)发生重叠时,现优先保 full-form 闭合归属
9-
- 修复了 `=bold<bold<=bold<>=>=` 及其自定义语法嵌套变体中的冲突归属问题,同时避免引入宽泛回退副作用
10-
- **解析模型收敛:移除“向前找救济”的长距前瞻**
11-
- 删除依赖“继续向后扫描是否还有 `endTag`”的救济分支
12-
- 冲突判定改为当前位置、基于栈顶 frame 的局部决策,行为更确定,符合步进状态机语义
13-
- **修复:endTag 延迟确认 + 截断安全匹配**
14-
- 新增 endTag 匹配状态(`full` / `truncated-prefix` / `none`
15-
- 当仅命中 endTag 前缀但被截断时,立即解除延迟并继续按文本扫描;`full` 命中才视为闭合 token
16-
- **性能:减少 shorthand 歧义路径中的重复开销**
17-
- 保留 shorthand 前探复用,并将重叠判定收敛为当前位置有界匹配,降低冲突密集输入下的重复扫描成本
7+
- **重构:统一闭合归属判定**
8+
- 新增统一的归属仲裁函数,集中处理 shorthand / full-form 的闭合竞争
9+
- shorthand 的 push、close、fallback 恢复路径改为复用同一套规则
10+
- **修复:full-form close 与 shorthand continuation 竞争时,优先 full-form**
11+
- `tagClose` 与父级 `endTag` 在同一游标重叠时,优先保留父级 full-form 闭合归属
12+
- 避免 shorthand 抢占父级闭合,覆盖 `=bold<bold<=bold<>=>=` 及等价自定义分隔符场景
13+
- **修复:endTag 延迟确认显式化,且对截断安全**
14+
- endTag 探测结果细分为 `full` / `truncated-prefix` / `none`
15+
-`full` 视为可消费闭合;命中截断前缀时立即解除延迟并恢复常规扫描
16+
- **模型一致性**
17+
- 移除分支内各自猜测式判优,收敛为单一、确定的归属规则
1818
- 无公共 API 变化
1919

2020
### 1.3.3

src/structural.ts

Lines changed: 80 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -565,21 +565,31 @@ const parseNodesWithFactory = <TNode extends StructuralNode | IndexedStructuralN
565565
return "full";
566566
};
567567

568-
const tryPushInlineShorthandChild = (
569-
frame: ParseFrame,
570-
tagStartI: number,
571-
info: ShorthandStartInfo,
572-
): boolean => {
573-
// Guard ambiguity like `=bold<bold<>=`:
574-
// if shorthand arg starts exactly at parent's inline close token (`endTag`),
575-
// this `name<` is text and the following close belongs to parent.
576-
if (frame.inlineCloseToken === endTag && scanEndTagAt(frame.text, info.argStart, frame.textEnd) === "full") {
577-
return false;
578-
}
568+
type ShorthandOwnershipPhase = "push" | "close" | "eof";
569+
type ShorthandOwnershipDecision = "allow" | "defer-parent";
570+
interface ShorthandOwnershipInput {
571+
phase: ShorthandOwnershipPhase;
572+
frame: ParseFrame;
573+
parent: ParseFrame | null;
574+
info?: ShorthandStartInfo;
575+
at?: number;
576+
}
577+
const resolveShorthandOwnership = (input: ShorthandOwnershipInput): ShorthandOwnershipDecision => {
578+
if (input.phase === "push") {
579+
const info = input.info;
580+
if (!info) return "allow";
581+
const frame = input.frame;
582+
583+
// `name<` 的 argStart 与父级 endTag 完全重叠时,优先父级闭合。
584+
if (
585+
frame.inlineCloseToken === endTag &&
586+
scanEndTagAt(frame.text, info.argStart, frame.textEnd) === "full"
587+
) {
588+
return "defer-parent";
589+
}
590+
591+
if (frame.inlineCloseToken !== endTag) return "allow";
579592

580-
// Guard ambiguity where shorthand would consume the `tagClose` that is
581-
// actually the start of parent's `endTag` (e.g. `=bold<bold<<>=`).
582-
if (frame.inlineCloseToken === endTag) {
583593
const canReuseProbe =
584594
frame.shorthandProbeStartI >= 0 &&
585595
frame.shorthandProbeBoundaryI >= 0 &&
@@ -596,8 +606,6 @@ const parseNodesWithFactory = <TNode extends StructuralNode | IndexedStructuralN
596606
probe = nextEsc;
597607
continue;
598608
}
599-
// Full DSL structure has priority over shorthand:
600-
// once a full tag starts, shorthand child would end before it.
601609
if (readTagStartInfo(frame.text, probe, syntax, tagName)) {
602610
boundary = probe;
603611
reject = false;
@@ -616,9 +624,38 @@ const parseNodesWithFactory = <TNode extends StructuralNode | IndexedStructuralN
616624
frame.shorthandProbeReject = reject;
617625
}
618626

619-
if (frame.shorthandProbeReject) {
620-
return false;
621-
}
627+
return frame.shorthandProbeReject ? "defer-parent" : "allow";
628+
}
629+
630+
if (input.phase === "close") {
631+
const at = input.at;
632+
if (at === undefined) return "allow";
633+
const frame = input.frame;
634+
const parent = input.parent;
635+
if (!frame.implicitInlineShorthand) return "allow";
636+
if (!parent || parent.inlineCloseToken !== endTag) return "allow";
637+
if (scanEndTagAt(frame.text, at, frame.textEnd) !== "full") return "allow";
638+
return "defer-parent";
639+
}
640+
641+
// EOF 恢复统一交给父帧继续决议(历史语义:回到 argStart 重扫)。
642+
return "defer-parent";
643+
};
644+
645+
const tryPushInlineShorthandChild = (
646+
frame: ParseFrame,
647+
tagStartI: number,
648+
info: ShorthandStartInfo,
649+
): boolean => {
650+
if (
651+
resolveShorthandOwnership({
652+
phase: "push",
653+
frame,
654+
parent: null,
655+
info,
656+
}) === "defer-parent"
657+
) {
658+
return false;
622659
}
623660

624661
if (frame.depth >= depthLimit) {
@@ -664,6 +701,16 @@ const parseNodesWithFactory = <TNode extends StructuralNode | IndexedStructuralN
664701
);
665702
stack.pop();
666703
const parent = stack[frame.parentIndex];
704+
const eofOwnership = resolveShorthandOwnership({
705+
phase: "eof",
706+
frame,
707+
parent: parent ?? null,
708+
});
709+
if (eofOwnership === "defer-parent") {
710+
appendBuf(parent, frame.tagStartI, frame.argStartI);
711+
parent.i = frame.argStartI;
712+
continue;
713+
}
667714
appendBuf(parent, frame.tagStartI, frame.argStartI);
668715
parent.i = frame.argStartI;
669716
continue;
@@ -700,18 +747,22 @@ const parseNodesWithFactory = <TNode extends StructuralNode | IndexedStructuralN
700747

701748
if (frame.inlineCloseToken === tagClose) {
702749
if (frameText.startsWith(tagClose, i)) {
703-
if (frame.implicitInlineShorthand) {
704-
const parent = stack[frame.parentIndex];
705-
if (
706-
parent &&
707-
parent.inlineCloseToken === endTag &&
708-
scanEndTagAt(frameText, i, frame.textEnd) === "full"
709-
) {
710-
stack.pop();
711-
appendBuf(parent, frame.tagStartI, frame.argStartI);
712-
parent.i = frame.argStartI;
750+
const parent = frame.parentIndex >= 0 ? stack[frame.parentIndex] : null;
751+
if (
752+
resolveShorthandOwnership({
753+
phase: "close",
754+
frame,
755+
parent,
756+
at: i,
757+
}) === "defer-parent"
758+
) {
759+
stack.pop();
760+
if (!parent) {
713761
continue;
714762
}
763+
appendBuf(parent, frame.tagStartI, frame.argStartI);
764+
parent.i = frame.argStartI;
765+
continue;
715766
}
716767
flushBuffer(frame);
717768
frame.inlineCloseWidth = tagClose.length;

0 commit comments

Comments
 (0)