@@ -76,6 +76,13 @@ import {
7676 skipTagBoundary ,
7777} from "./scanner.js" ;
7878import { 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
8087const 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