@@ -29,7 +29,7 @@ function isCommentNode(node) {
2929 * Parse all directive blocks from AST in a single pass
3030 *
3131 * @param {Object } tree Remark AST
32- * @returns {Array } Array of block objects with start, end , and content
32+ * @returns {Array } Array of block objects with startNode, endNode , and content
3333 */
3434export function parseDirectiveBlocks ( tree ) {
3535 const blocks = [ ]
@@ -48,7 +48,7 @@ export function parseDirectiveBlocks(tree) {
4848 if ( beginMatch ) {
4949 if ( currentBlock ) {
5050 throw new Error (
51- `Nested BEGIN blocks not allowed. Found BEGIN at line ${ lineNumber } , previous BEGIN at line ${ currentBlock . start } ` ,
51+ `Nested BEGIN blocks not allowed. Found BEGIN at line ${ lineNumber } , previous BEGIN at line ${ currentBlock . startLine } ` ,
5252 )
5353 }
5454
@@ -58,9 +58,11 @@ export function parseDirectiveBlocks(tree) {
5858 }
5959
6060 currentBlock = {
61- start : lineNumber ,
61+ startNode : node ,
62+ startLine : lineNumber ,
6263 content : blockContent . trim ( ) ,
63- end : null ,
64+ endNode : null ,
65+ endLine : null ,
6466 }
6567 return
6668 }
@@ -81,13 +83,14 @@ export function parseDirectiveBlocks(tree) {
8183
8284 if ( endContent . trim ( ) !== currentBlock . content ) {
8385 throw new Error (
84- `Mismatched block names: BEGIN="${ currentBlock . content } " at line ${ currentBlock . start } , ` +
86+ `Mismatched block names: BEGIN="${ currentBlock . content } " at line ${ currentBlock . startLine } , ` +
8587 `END="${ endContent . trim ( ) } " at line ${ lineNumber } ` ,
8688 )
8789 }
8890
8991 // Complete the block
90- currentBlock . end = lineNumber
92+ currentBlock . endNode = node
93+ currentBlock . endLine = lineNumber
9194 blocks . push ( currentBlock )
9295 currentBlock = null
9396 }
@@ -96,47 +99,56 @@ export function parseDirectiveBlocks(tree) {
9699 // Check for unclosed blocks
97100 if ( currentBlock ) {
98101 throw new Error (
99- `Unclosed BEGIN block: "${ currentBlock . content } " opened at line ${ currentBlock . start } ` ,
102+ `Unclosed BEGIN block: "${ currentBlock . content } " opened at line ${ currentBlock . startLine } ` ,
100103 )
101104 }
102105
103106 return blocks
104107}
105108
106109/**
107- * Remove nodes from AST within specified line range
110+ * Remove nodes from AST within specified block using node references
108111 *
109- * This handles nodes from included partials which may not have position data
110- * matching the parent file's line numbers. We track when we enter/exit the
111- * removal range based on the BEGIN/END comments.
112+ * This uses actual node object references to identify the range, avoiding
113+ * issues with overlapping line numbers from multiple partials.
112114 *
113115 * @param {Object } tree Remark AST
114- * @param {number } startLine Start line (inclusive)
115- * @param {number } endLine End line (inclusive)
116+ * @param {Object } block Block object with startNode and endNode references
116117 */
117- export function removeNodesInRange ( tree , startLine , endLine ) {
118+ export function removeNodesInRange ( tree , block ) {
119+ const { startNode, endNode } = block
120+
118121 function removeFromNodes ( nodes , depth = 0 , parentInsideRange = false ) {
119122 if ( ! Array . isArray ( nodes ) ) {
120123 return parentInsideRange
121124 }
122125
123126 const indicesToRemove = [ ]
124127 let insideRange = parentInsideRange
125- let lastEndLine = null // Track the line number where we last found an END comment
126128
127129 for ( let i = 0 ; i < nodes . length ; i ++ ) {
128130 const node = nodes [ i ]
129- const hasPosition = node . position ?. start ?. line && node . position ?. end ?. line
130131
131- // Recursively check children FIRST to find any nested END comments
132- // before deciding whether to remove this node
133- // const hadChildren = !!node.children
132+ // Check if this is the START node
133+ if ( node === startNode ) {
134+ insideRange = true
135+ indicesToRemove . push ( i )
136+ continue
137+ }
138+
139+ // Check if this is the END node
140+ if ( node === endNode ) {
141+ indicesToRemove . push ( i )
142+ insideRange = false
143+ continue
144+ }
145+
146+ // Recursively check children
134147 const wasInsideRange = insideRange
135148 if ( node . children ) {
136149 insideRange = removeFromNodes ( node . children , depth + 1 , insideRange )
137150
138- // After recursion, children have already been removed.
139- // Determine whether to remove the parent node:
151+ // After recursion, decide whether to remove the parent node
140152 const nodeIsComment = isCommentNode ( node )
141153
142154 // If we were inside range and still are, remove the parent (fully in range)
@@ -146,9 +158,7 @@ export function removeNodesInRange(tree, startLine, endLine) {
146158 }
147159
148160 // If range ENDED inside this node's children (END found in children),
149- // only remove parent if it's now empty (all children were removed).
150- // Example: list containing [listItem1 (removed), listItem2 with END (removed), listItem3 (kept)]
151- // → Keep the list since listItem3 remains
161+ // only remove parent if it's now empty (all children were removed)
152162 if ( wasInsideRange && ! insideRange ) {
153163 if ( ! node . children || node . children . length === 0 ) {
154164 indicesToRemove . push ( i )
@@ -157,70 +167,15 @@ export function removeNodesInRange(tree, startLine, endLine) {
157167 }
158168
159169 // If range STARTED inside this node's children (BEGIN found in children),
160- // the parent node CONTAINS the range start and should NOT be removed.
161- // Example: listItem containing [paragraph, BEGIN comment] - keep the listItem, only BEGIN removed
170+ // the parent node CONTAINS the range start and should NOT be removed
162171 if ( ! wasInsideRange && insideRange ) {
163172 continue
164173 }
165174 }
166175
167- if ( hasPosition ) {
168- const nodeStart = node . position . start . line
169- const nodeEnd = node . position . end . line
170-
171- // Check if we've moved past the end of the range
172- // Only check this for comment nodes (jsx/html/code), as content nodes
173- // from partials can have any line numbers
174- const nodeIsComment = isCommentNode ( node )
175- if (
176- insideRange &&
177- nodeIsComment &&
178- nodeStart >= startLine &&
179- nodeStart > endLine
180- ) {
181- insideRange = false
182- }
183-
184- // If we're NOT inside a range, check if this node starts one
185- if ( ! insideRange ) {
186- // Special case: if we just found an END comment on this same line (lastEndLine),
187- // do NOT start a new range. This prevents removing unrelated BEGIN comments
188- // that happen to be on the same line as the END comment (e.g., line 24 has both
189- // "<!-- END: TFC:only -->" and "<!-- BEGIN: TFEnterprise:only -->")
190- if ( lastEndLine !== null && nodeEnd === lastEndLine ) {
191- // This is a BEGIN comment on the same line as the last END - skip it
192- }
193- // Check if this node marks the start of the range
194- // Only jsx/html/code nodes can be BEGIN comments
195- else if (
196- nodeIsComment &&
197- nodeStart >= startLine &&
198- nodeEnd <= endLine
199- ) {
200- insideRange = true
201- indicesToRemove . push ( i )
202- }
203- }
204- // If we're inside a range, all nodes are from the partial (except the END comment)
205- else {
206- // Check if this is the END comment
207- // END comments are jsx/html/code nodes with position data from the parent file
208- if ( nodeIsComment && nodeStart >= startLine && nodeEnd === endLine ) {
209- indicesToRemove . push ( i )
210- insideRange = false
211- lastEndLine = nodeEnd // Remember the line where we found the END comment
212- }
213- // Otherwise, it's a partial node - remove it
214- else {
215- indicesToRemove . push ( i )
216- }
217- }
218- } else {
219- // Node without position (e.g., from included partial)
220- // Remove it if we're currently inside the range
221- if ( insideRange ) {
222- indicesToRemove . push ( i )
223- }
176+ // If we're inside the range and this node isn't a boundary comment, remove it
177+ if ( insideRange ) {
178+ indicesToRemove . push ( i )
224179 }
225180 }
226181
0 commit comments