Skip to content

Commit 1afdb79

Browse files
committed
refactor: 新增纯函数模块(ownership/replay 抽离) structural.ts 仅做接线替换 + 测试名注释
1 parent a8a71c5 commit 1afdb79

2 files changed

Lines changed: 252 additions & 150 deletions

File tree

src/core/structural.ts

Lines changed: 72 additions & 150 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,13 @@ import {
7676
skipTagBoundary,
7777
} from "./scanner.js";
7878
import { makePosition, type PositionTracker } from "../internal/positions.js";
79+
import {
80+
buildMalformedInlineReplayPlan,
81+
resolveShorthandOwnershipClose,
82+
resolveShorthandOwnershipPush,
83+
scanEndTagAt,
84+
type ShorthandProbeState,
85+
} from "./structuralOwnership.js";
7986

8087
const emptyBuffer = (): BufferState => ({ start: -1, end: -1, segments: null });
8188

@@ -297,13 +304,6 @@ const parseNodesWithFactory = <TNode extends StructuralNode | IndexedStructuralN
297304
// 没有 resume 闭包。子帧完成后由 completeChild 按 returnKind 分发。
298305

299306
type ReturnKind = "inline" | "rawArgs" | "blockArgs" | "blockContent";
300-
interface ShorthandProbeState {
301-
textEnd: number;
302-
startI: number;
303-
boundaryI: number;
304-
reject: boolean;
305-
}
306-
307307
interface ParseFrame {
308308
text: string;
309309
depth: number;
@@ -667,133 +667,46 @@ const parseNodesWithFactory = <TNode extends StructuralNode | IndexedStructuralN
667667
};
668668
};
669669

670-
type EndTagMatchState = "none" | "full" | "truncated-prefix";
671-
const scanEndTagAt = (text: string, start: number, endExclusive: number): EndTagMatchState => {
672-
if (start >= endExclusive) return "none";
673-
if (text[start] !== endTag[0]) return "none";
674-
let offset = 0;
675-
while (offset < endTag.length) {
676-
const pos = start + offset;
677-
if (pos >= endExclusive) return "truncated-prefix";
678-
if (text[pos] !== endTag[offset]) return "none";
679-
offset++;
680-
}
681-
return "full";
670+
const getAncestorEndTagOwner = (frame: ParseFrame | null): ParseFrame | null => {
671+
if (!frame) return null;
672+
const ownerIndex = frame.ancestorEndTagOwnerIndex;
673+
return ownerIndex >= 0 ? (stack[ownerIndex] ?? null) : null;
682674
};
683675

684-
type ShorthandOwnershipPhase = "push" | "close";
685-
type ShorthandOwnershipDecision = "allow" | "defer-parent";
686-
interface ShorthandOwnershipInput {
687-
phase: ShorthandOwnershipPhase;
688-
frame: ParseFrame;
689-
parent: ParseFrame | null;
690-
info?: ShorthandStartInfo;
691-
at?: number;
692-
}
693-
const resolveShorthandOwnership = (
694-
input: ShorthandOwnershipInput,
695-
): ShorthandOwnershipDecision => {
696-
const getAncestorEndTagOwner = (frame: ParseFrame | null): ParseFrame | null => {
697-
if (!frame) return null;
698-
const ownerIndex = frame.ancestorEndTagOwnerIndex;
699-
return ownerIndex >= 0 ? (stack[ownerIndex] ?? null) : null;
700-
};
701-
702-
const getEndTagOwner = (frame: ParseFrame | null): ParseFrame | null => {
703-
if (!frame) return null;
704-
if (frame.inlineCloseToken === endTag) return frame;
705-
return getAncestorEndTagOwner(frame);
706-
};
707-
708-
const hasEndTagOwnerAt = (frame: ParseFrame | null, at: number): boolean => {
709-
const owner = getEndTagOwner(frame);
710-
return !!owner && scanEndTagAt(owner.text, at, owner.textEnd) === "full";
711-
};
712-
713-
if (input.phase === "push") {
714-
const info = input.info;
715-
if (!info) return "allow";
716-
const frame = input.frame;
717-
718-
// `name<` 的 argStart 与父级 endTag 完全重叠时,优先父级闭合。
719-
if (
720-
frame.inlineCloseToken === endTag &&
721-
scanEndTagAt(frame.text, info.argStart, frame.textEnd) === "full"
722-
) {
723-
return "defer-parent";
724-
}
725-
726-
if (hasEndTagOwnerAt(getAncestorEndTagOwner(frame), info.argStart)) {
727-
return "defer-parent";
728-
}
729-
730-
if (frame.inlineCloseToken !== endTag) return "allow";
731-
732-
const shorthandProbe = frame.shorthandProbe;
733-
const canReuseProbe =
734-
shorthandProbe !== null &&
735-
shorthandProbe.textEnd === frame.textEnd &&
736-
info.argStart >= shorthandProbe.startI &&
737-
info.argStart <= shorthandProbe.boundaryI;
738-
739-
if (!canReuseProbe) {
740-
let boundary = frame.textEnd;
741-
let reject = false;
742-
let probe = info.argStart;
743-
while (probe < frame.textEnd) {
744-
const [escaped, nextEsc] = readEscapedSequence(frame.text, probe, syntax);
745-
if (escaped !== null) {
746-
probe = nextEsc;
747-
continue;
748-
}
749-
if (readTagStartInfo(frame.text, probe, syntax, tagName)) {
750-
boundary = probe;
751-
reject = false;
752-
break;
753-
}
754-
if (frame.text.startsWith(tagClose, probe)) {
755-
boundary = probe;
756-
reject = scanEndTagAt(frame.text, probe, frame.textEnd) === "full";
757-
break;
758-
}
759-
probe++;
760-
}
761-
762-
frame.shorthandProbe = {
763-
textEnd: frame.textEnd,
764-
startI: info.argStart,
765-
boundaryI: boundary,
766-
reject,
767-
};
768-
}
769-
770-
return frame.shorthandProbe?.reject ? "defer-parent" : "allow";
771-
}
772-
773-
if (input.phase === "close") {
774-
const at = input.at;
775-
if (at === undefined) return "allow";
776-
const frame = input.frame;
777-
if (!frame.implicitInlineShorthand) return "allow";
778-
return hasEndTagOwnerAt(input.parent, at) ? "defer-parent" : "allow";
779-
}
676+
const getEndTagOwner = (frame: ParseFrame | null): ParseFrame | null => {
677+
if (!frame) return null;
678+
if (frame.inlineCloseToken === endTag) return frame;
679+
return getAncestorEndTagOwner(frame);
680+
};
780681

781-
return "allow";
682+
const hasEndTagOwnerAt = (frame: ParseFrame | null, at: number): boolean => {
683+
const owner = getEndTagOwner(frame);
684+
return !!owner && scanEndTagAt(owner.text, endTag, at, owner.textEnd) === "full";
782685
};
783686

784687
const tryPushInlineShorthandChild = (
785688
frame: ParseFrame,
786689
tagStartI: number,
787690
info: ShorthandStartInfo,
788691
): boolean => {
789-
if (
790-
resolveShorthandOwnership({
791-
phase: "push",
792-
frame,
793-
parent: null,
794-
info,
795-
}) === "defer-parent"
796-
) {
692+
const ownership = resolveShorthandOwnershipPush({
693+
argStart: info.argStart,
694+
frameInlineCloseToken: frame.inlineCloseToken,
695+
frameText: frame.text,
696+
frameTextEnd: frame.textEnd,
697+
endTag,
698+
tagClose,
699+
currentProbe: frame.shorthandProbe,
700+
hasAncestorEndTagOwnerAt: at => hasEndTagOwnerAt(getAncestorEndTagOwner(frame), at),
701+
readEscapedNext: at => {
702+
const [escaped, nextEsc] = readEscapedSequence(frame.text, at, syntax);
703+
return escaped !== null ? nextEsc : null;
704+
},
705+
hasTagStartAt: at => Boolean(readTagStartInfo(frame.text, at, syntax, tagName)),
706+
});
707+
frame.shorthandProbe = ownership.nextProbe;
708+
// 对应测试: [Coverage/Structural] shorthand ownership probe should skip escaped sequence before boundary
709+
if (ownership.decision === "defer-parent") {
797710
return false;
798711
}
799712

@@ -816,7 +729,15 @@ const parseNodesWithFactory = <TNode extends StructuralNode | IndexedStructuralN
816729
return true;
817730
};
818731

819-
const emitUnclosedInlineFrameError = (frame: ParseFrame) => {
732+
interface UnclosedInlineErrorFrame {
733+
implicitInlineShorthand: boolean;
734+
text: string;
735+
tagStartI: number;
736+
argStartI: number;
737+
tagOpenPos: number;
738+
}
739+
740+
const emitUnclosedInlineFrameError = (frame: UnclosedInlineErrorFrame) => {
820741
emitError(
821742
tracker,
822743
onError,
@@ -829,27 +750,30 @@ const parseNodesWithFactory = <TNode extends StructuralNode | IndexedStructuralN
829750
};
830751

831752
const replayMalformedInlineChainAtEof = (frame: ParseFrame): boolean => {
832-
let replayFrame: ParseFrame = frame;
753+
const replayPlan = buildMalformedInlineReplayPlan(frame, parentIndex =>
754+
parentIndex >= 0 ? (stack[parentIndex] ?? null) : null,
755+
);
833756

834-
while (true) {
757+
for (let index = 0; index < replayPlan.chain.length; index++) {
758+
const replayFrame = replayPlan.chain[index];
835759
emitUnclosedInlineFrameError(replayFrame);
836760
stack.pop();
761+
}
837762

838-
const parent =
839-
replayFrame.parentIndex >= 0 ? (stack[replayFrame.parentIndex] ?? null) : null;
840-
if (!parent) {
841-
return true;
842-
}
843-
if (stack[stack.length - 1] !== parent) {
844-
throw new Error("Malformed EOF inline replay expects parent to be the current stack top.");
845-
}
846-
if (parent.inlineCloseToken === null) {
847-
appendBuf(parent, replayFrame.tagStartI, replayFrame.argStartI);
848-
parent.i = replayFrame.argStartI;
849-
return true;
850-
}
851-
replayFrame = parent;
763+
if (replayPlan.resumeParentIndex < 0) {
764+
return true;
765+
}
766+
const parent = stack[replayPlan.resumeParentIndex];
767+
if (!parent) {
768+
return true;
769+
}
770+
if (stack[stack.length - 1] !== parent) {
771+
throw new Error("Malformed EOF inline replay expects parent to be the current stack top.");
852772
}
773+
// 对应测试: [Coverage/Structural] malformed inline chain at EOF should replay once and degrade to full source text
774+
appendBuf(parent, replayPlan.resumeTagStartI, replayPlan.resumeArgStartI);
775+
parent.i = replayPlan.resumeArgStartI;
776+
return true;
853777
};
854778

855779
// ── 主循环 ──
@@ -867,15 +791,13 @@ const parseNodesWithFactory = <TNode extends StructuralNode | IndexedStructuralN
867791
const parent = frame.parentIndex >= 0 ? stack[frame.parentIndex] : null;
868792
// full-form close 与 shorthand close 竞争时,先让 full-form close 拥有 token。
869793
if (
870-
scanEndTagAt(frameText, i, frame.textEnd) === "full" &&
871-
resolveShorthandOwnership({
872-
phase: "close",
873-
frame,
874-
parent,
875-
at: i,
876-
}) === "defer-parent"
794+
scanEndTagAt(frameText, endTag, i, frame.textEnd) === "full" &&
795+
resolveShorthandOwnershipClose(i, frame.implicitInlineShorthand, at =>
796+
hasEndTagOwnerAt(parent, at),
797+
) === "defer-parent"
877798
) {
878799
stack.pop();
800+
// 对应测试: [Coverage/Structural] shorthand defer-parent downgrade should merge adjacent text with continuous position
879801
return downgradeInlineIntoParent(frame, i);
880802
}
881803

@@ -890,7 +812,7 @@ const parseNodesWithFactory = <TNode extends StructuralNode | IndexedStructuralN
890812
if (!frameText.startsWith(tagClose, i)) return false;
891813

892814
// ) 系列判定(完整 DSL inline 参数区)
893-
if (scanEndTagAt(frameText, i, frame.textEnd) === "full") {
815+
if (scanEndTagAt(frameText, endTag, i, frame.textEnd) === "full") {
894816
// )$$ → inline close
895817
flushBuffer(frame);
896818
frame.inlineCloseWidth = endTag.length;
@@ -1293,7 +1215,7 @@ const parseNodesWithFactory = <TNode extends StructuralNode | IndexedStructuralN
12931215

12941216
// ── 非 inline 帧的意外 endTag ──
12951217
// 非 inline 帧不存在合法 endTag 闭合;只消费 tagClose,把 tagPrefix 留给下一轮 tag 识别。
1296-
if (scanEndTagAt(frameText, i, frame.textEnd) === "full") {
1218+
if (scanEndTagAt(frameText, endTag, i, frame.textEnd) === "full") {
12971219
const nextIsTag = readTagStartInfo(frameText, i + tagClose.length, syntax, tagName);
12981220
if (!nextIsTag) {
12991221
emitError(

0 commit comments

Comments
 (0)