ReviewOS

also looking at this

stacks/stx

fix(loops): render the row a @break or @continue stops in

#2102
Open mkantautas wants to merge fix/foreach-break-continue-row into main
2 files +89 -43
packages/stx/src/loops.tsmodified+28-43
Changes to packages/stx/src/loops.ts
@@ -394,27 +394,22 @@ function processUnconditionalLoopControl(content: string): string {
394394}
395395
396396/**
397 * Check if content contains a break marker and clean it up
397 * Cut a row at its first break or continue marker.
398 *
399 * Returns which one it was (null when the row has neither) and the content
400 * before it, which is still part of the row.
398401 */
399function checkAndCleanBreak(content: string): { hasBreak: boolean, content: string } {
400 const hasBreak = content.includes(BREAK_MARKER)
401 // Remove marker and everything after it
402 const cleanContent = hasBreak
403 ? content.substring(0, content.indexOf(BREAK_MARKER))
404 : content
405 return { hasBreak, content: cleanContent }
406}
402function cutAtLoopControl(content: string): { control: 'break' | 'continue' | null, content: string } {
403 const breakAt = content.indexOf(BREAK_MARKER)
404 const continueAt = content.indexOf(CONTINUE_MARKER)
405 if (breakAt === -1 && continueAt === -1)
406 return { control: null, content }
407407
408/**
409 * Check if content contains a continue marker and clean it up
410 */
411function checkAndCleanContinue(content: string): { hasContinue: boolean, content: string } {
412 const hasContinue = content.includes(CONTINUE_MARKER)
413 // Remove marker and everything after it
414 const cleanContent = hasContinue
415 ? content.substring(0, content.indexOf(CONTINUE_MARKER))
416 : content
417 return { hasContinue, content: cleanContent }
408 const isBreak = continueAt === -1 || (breakAt !== -1 && breakAt < continueAt)
409 return {
410 control: isBreak ? 'break' : 'continue',
411 content: content.substring(0, isBreak ? breakAt : continueAt),
412 }
418413}
419414
420415/**
@@ -1044,19 +1039,12 @@ export function processLoops(template: string, context: Record<string, any>, fil
10441039 // These are evaluated immediately based on the condition expression
10451040 let processedContent = processConditionalLoopControl(content, itemContext)
10461041
1047 // Check for continue marker from conditional @continue(condition)
1048 let continueCheck = checkAndCleanContinue(processedContent)
1049 if (continueCheck.hasContinue) {
1050 loopResult += continueCheck.content
1051 continue
1052 }
1053
1054 // Check for break marker from conditional @break(condition)
1055 let breakCheck = checkAndCleanBreak(processedContent)
1056 if (breakCheck.hasBreak) {
1057 loopResult += breakCheck.content
1058 break
1059 }
1042 // A @break or @continue ends the row where it stands. The content
1043 // before it is still this row, so it goes through every step below
1044 // with the row's context; appending it here left its for the
1045 // page pass, which no longer has the loop variable.
1046 let stop = cutAtLoopControl(processedContent)
1047 processedContent = stop.content
10601048
10611049 // Step 2a: Process any nested @forelse blocks with the iteration context
10621050 processedContent = processForelse(processedContent, itemContext)
@@ -1072,17 +1060,12 @@ export function processLoops(template: string, context: Record<string, any>, fil
10721060 // Now that conditionals are processed, @break/@continue from @if blocks are visible
10731061 processedContent = processUnconditionalLoopControl(processedContent)
10741062
1075 // Check for break/continue markers
1076 breakCheck = checkAndCleanBreak(processedContent)
1077 if (breakCheck.hasBreak) {
1078 loopResult += breakCheck.content
1079 break
1080 }
1081 continueCheck = checkAndCleanContinue(processedContent)
1082 if (continueCheck.hasContinue) {
1083 loopResult += continueCheck.content
1084 continue
1085 }
1063 // A marker revealed here sits before any found in step 1, since
1064 // everything after that one was already cut away.
1065 const revealed = cutAtLoopControl(processedContent)
1066 if (revealed.control)
1067 stop = revealed
1068 processedContent = revealed.content
10861069
10871070 // Step 5: Process expressions with the item context
10881071 processedContent = processExpressions(processedContent, itemContext, filePath)
@@ -1106,6 +1089,8 @@ export function processLoops(template: string, context: Record<string, any>, fil
11061089 processedContent = preEvaluateIncludeData(processedContent, itemContext)
11071090
11081091 loopResult += processedContent
1092 if (stop.control === 'break')
1093 break
11091094 }
11101095
11111096 // Replace the @foreach block with the processed result
packages/stx/test/directives/loops.test.tsmodified+61-0
Changes to packages/stx/test/directives/loops.test.ts
@@ -745,6 +745,67 @@ describe('Loops Comprehensive', () => {
745745 })
746746 })
747747
748 // ===========================================================================
749 // Content before @break and @continue
750 // ===========================================================================
751 describe('content before @break and @continue', () => {
752 it('should render the row an unconditional @break stops', () => {
753 const result = run(
754 ``,
755 { items: ['a', 'b', 'c'] },
756 )
757 expect(result).toBe('<li>a</li><li>b</li>')
758 })
759
760 it('should render the row a conditional @break(condition) stops', () => {
761 const result = run(
762 '',
763 { items: [1, 2, 3] },
764 )
765 expect(result).toBe('<li>1</li><i>1</i><li>2</li>')
766 })
767
768 it('should process directives in the row a conditional @break(condition) stops', () => {
769 const result = run(
770 '',
771 { items: [1, 2, 3] },
772 )
773 expect(result).toBe('<b>2</b>')
774 })
775
776 it('should render the row an unconditional @continue skips the rest of', () => {
777 const result = run(
778 ``,
779 { items: ['a', 'b', 'c'] },
780 )
781 expect(result).toBe('<li>a</li><i>a</i><li>b</li><li>c</li><i>c</i>')
782 })
783
784 it('should render the row a conditional @continue(condition) skips the rest of', () => {
785 const result = run(
786 '',
787 { items: [1, 2, 3] },
788 )
789 expect(result).toBe('<li>1</li><i>1</i><li>2</li><li>3</li><i>3</i>')
790 })
791
792 it('should stop at whichever of @break and @continue comes first in the row', () => {
793 const result = run(
794 '',
795 { items: [1, 2, 3] },
796 )
797 expect(result).toBe('<li>1</li><i>1</i><li>2</li>')
798 })
799
800 it('should let a @continue inside @if win over a later @break(condition)', () => {
801 const result = run(
802 '',
803 { items: [1, 2, 3] },
804 )
805 expect(result).toBe('<li>1</li><i>1</i><li>2</li><li>3</li><i>3</i>')
806 })
807 })
808
748809 // ===========================================================================
749810 // Prop Bindings in Loops
750811 // ===========================================================================