@@ -117,6 +117,48 @@ export class StringValue extends RuntimeValue<string> {
117
117
return new StringValue ( this . value . trimStart ( ) ) ;
118
118
} ) ,
119
119
] ,
120
+ [
121
+ "split" ,
122
+ // follows Python's `str.split(sep=None, maxsplit=-1)` function behavior
123
+ // https://docs.python.org/3.13/library/stdtypes.html#str.split
124
+ new FunctionValue ( ( args ) => {
125
+ const sep = args [ 0 ] ?? new NullValue ( ) ;
126
+ if ( ! ( sep instanceof StringValue || sep instanceof NullValue ) ) {
127
+ throw new Error ( "sep argument must be a string or null" ) ;
128
+ }
129
+ const maxsplit = args [ 1 ] ?? new NumericValue ( - 1 ) ;
130
+ if ( ! ( maxsplit instanceof NumericValue ) ) {
131
+ throw new Error ( "maxsplit argument must be a number" ) ;
132
+ }
133
+
134
+ let result = [ ] ;
135
+ if ( sep instanceof NullValue ) {
136
+ // If sep is not specified or is None, runs of consecutive whitespace are regarded as a single separator, and the
137
+ // result will contain no empty strings at the start or end if the string has leading or trailing whitespace.
138
+ // Trailing whitespace may be present when maxsplit is specified and there aren't sufficient matches in the string.
139
+ const text = this . value . trimStart ( ) ;
140
+ for ( const { 0 : match , index } of text . matchAll ( / \S + / g) ) {
141
+ if ( maxsplit . value !== - 1 && result . length >= maxsplit . value && index !== undefined ) {
142
+ result . push ( match + text . slice ( index + match . length ) ) ;
143
+ break ;
144
+ }
145
+ result . push ( match ) ;
146
+ }
147
+ } else {
148
+ // If sep is specified, consecutive delimiters are not grouped together and are deemed to delimit empty strings.
149
+ if ( sep . value === "" ) {
150
+ throw new Error ( "empty separator" ) ;
151
+ }
152
+ result = this . value . split ( sep . value ) ;
153
+ if ( maxsplit . value !== - 1 && result . length > maxsplit . value ) {
154
+ // Follow Python's behavior: If maxsplit is given, at most maxsplit splits are done,
155
+ // with any remaining text returned as the final element of the list.
156
+ result . push ( result . splice ( maxsplit . value ) . join ( sep . value ) ) ;
157
+ }
158
+ }
159
+ return new ArrayValue ( result . map ( ( part ) => new StringValue ( part ) ) ) ;
160
+ } ) ,
161
+ ] ,
120
162
] ) ;
121
163
}
122
164
@@ -543,6 +585,8 @@ export class Interpreter {
543
585
}
544
586
} )
545
587
) ;
588
+ case "join" :
589
+ return new StringValue ( operand . value . map ( ( x ) => x . value ) . join ( "" ) ) ;
546
590
default :
547
591
throw new Error ( `Unknown ArrayValue filter: ${ filter . value } ` ) ;
548
592
}
@@ -570,6 +614,7 @@ export class Interpreter {
570
614
)
571
615
. join ( "\n" )
572
616
) ;
617
+ case "join" :
573
618
case "string" :
574
619
return operand ; // no-op
575
620
default :
@@ -610,6 +655,24 @@ export class Interpreter {
610
655
throw new Error ( "If set, indent must be a number" ) ;
611
656
}
612
657
return new StringValue ( toJSON ( operand , indent . value ) ) ;
658
+ } else if ( filterName === "join" ) {
659
+ let value ;
660
+ if ( operand instanceof StringValue ) {
661
+ // NOTE: string.split('') breaks for unicode characters
662
+ value = Array . from ( operand . value ) ;
663
+ } else if ( operand instanceof ArrayValue ) {
664
+ value = operand . value . map ( ( x ) => x . value ) ;
665
+ } else {
666
+ throw new Error ( `Cannot apply filter "${ filterName } " to type: ${ operand . type } ` ) ;
667
+ }
668
+ const [ args , kwargs ] = this . evaluateArguments ( filter . args , environment ) ;
669
+
670
+ const separator = args . at ( 0 ) ?? kwargs . get ( "separator" ) ?? new StringValue ( "" ) ;
671
+ if ( ! ( separator instanceof StringValue ) ) {
672
+ throw new Error ( "separator must be a string" ) ;
673
+ }
674
+
675
+ return new StringValue ( value . join ( separator . value ) ) ;
613
676
}
614
677
615
678
if ( operand instanceof ArrayValue ) {
0 commit comments