Hyperlinkv0.8.0-beta.28

Channel

Channel.splitLinesconsteffect/Channel.ts:6332
<Err, Done>(): Channel<
  Arr.NonEmptyReadonlyArray<string>,
  Err,
  Done,
  Arr.NonEmptyReadonlyArray<string>,
  Err,
  Done
>

Splits upstream string chunks into lines, recognizing \n, \r\n, and standalone \r as line terminators. The behavior matches String.linesIterator regardless of how the input is chunked.

Details

A line terminator at the very end of the stream does not produce a trailing empty line (consistent with String.linesIterator). Conversely, if the stream ends without a terminator the final partial line is still emitted.

Example (Splitting string chunks into lines)

import { Effect, Stream } from "effect"

Effect.runPromise(Effect.gen(function*() {
  const result = yield* Stream.runCollect(
    Stream.splitLines(Stream.make("hel", "lo\r\nwor", "ld\n"))
  )
  console.log(result)
  // [ 'hello', 'world' ]
}))
String manipulation
Source effect/Channel.ts:6332101 lines
export const splitLines = <Err, Done>(): Channel<
  Arr.NonEmptyReadonlyArray<string>,
  Err,
  Done,
  Arr.NonEmptyReadonlyArray<string>,
  Err,
  Done
> =>
  fromTransform((upstream, _scope) =>
    Effect.sync(() => {
      // Accumulates text that has not yet been terminated by a line break.
      // Content is carried across chunks until a terminator is found.
      let stringBuilder = ""
      // Set when a chunk ends with \r so the next chunk can check whether
      // the following character is \n (completing a \r\n pair) or not
      // (standalone \r, which is itself a line terminator).
      let midCRLF = false
      // Remembers the upstream Done value after the first time the upstream
      // signals completion, so subsequent pulls return Done immediately
      // without pulling upstream again.
      let done = Option.none<Done>()

      function splitLinesArray(chunk: Arr.NonEmptyReadonlyArray<string>): Arr.NonEmptyReadonlyArray<string> | null {
        const chunkBuilder: Array<string> = []

        function pushLine(segment: string): void {
          if (stringBuilder.length === 0) {
            chunkBuilder.push(segment)
          } else {
            chunkBuilder.push(stringBuilder + segment)
            stringBuilder = ""
          }
        }

        for (let i = 0; i < chunk.length; i++) {
          const str = chunk[i]
          if (str.length !== 0) {
            let from = 0
            let indexOfCR = str.indexOf("\r")
            let indexOfLF = str.indexOf("\n")
            if (midCRLF) {
              if (indexOfLF === 0) {
                pushLine("")
                from = 1
                indexOfLF = str.indexOf("\n", from)
              } else {
                pushLine("")
              }
              midCRLF = false
            }
            while (indexOfCR !== -1 || indexOfLF !== -1) {
              if (indexOfCR === -1 || (indexOfLF !== -1 && indexOfLF < indexOfCR)) {
                pushLine(str.substring(from, indexOfLF))
                from = indexOfLF + 1
                indexOfLF = str.indexOf("\n", from)
              } else {
                if (str.length === indexOfCR + 1) {
                  midCRLF = true
                  indexOfCR = -1
                } else {
                  pushLine(str.substring(from, indexOfCR))
                  from = indexOfCR + (indexOfLF === indexOfCR + 1 ? 2 : 1)
                  indexOfCR = str.indexOf("\r", from)
                  indexOfLF = str.indexOf("\n", from)
                }
              }
            }
            stringBuilder = stringBuilder + str.substring(from, str.length - (midCRLF ? 1 : 0))
          }
        }
        return Arr.isReadonlyArrayNonEmpty(chunkBuilder) ? chunkBuilder : null
      }

      const pullOrFlush: Pull.Pull<Arr.NonEmptyReadonlyArray<string>, Err, Done> = Effect.suspend(() => {
        if (done._tag === "Some") {
          return Cause.done(done.value)
        }
        return Pull.matchEffect(upstream, {
          onSuccess: loop,
          onFailure: Effect.failCause,
          onDone: (leftover) => {
            done = Option.some(leftover)
            if (stringBuilder.length > 0 || midCRLF) {
              const last = stringBuilder
              stringBuilder = ""
              midCRLF = false
              return Effect.succeed([last] as Arr.NonEmptyReadonlyArray<string>)
            }
            return Cause.done(leftover)
          }
        })
      })

      function loop(chunk: Arr.NonEmptyReadonlyArray<string>): Pull.Pull<Arr.NonEmptyReadonlyArray<string>, Err, Done> {
        const lines = splitLinesArray(chunk)
        return lines !== null ? Effect.succeed(lines) : pullOrFlush
      }

      return pullOrFlush
    })
  )
Referenced by 1 symbols