All files / src/codec/indented index.ts

100% Statements 52/52
100% Branches 17/17
100% Functions 6/6
100% Lines 52/52

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 941x 1x 1x 1x   1x 102x 102x 102x 102x 102x 102x 102x 257x 102x 102x                       1x         1x 1x 1x 3x 102x 1x                   1x 83x 83x   22x 22x 22x 22x 22x 22x   22x 22x                 1x 145x 145x 145x 145x     145x 145x   44x   44x 145x 141x 62x 62x 62x 79x   44x 44x   1x 167x  
import {annotateDepthUnfold, preOrderFold} from '#ops'
import {fixTree, treeAna, treeHylo, type Tree} from '#tree'
import * as TreeF from '#treeF'
import {Array, pipe, String} from '#util'
 
const _encode = (self: Tree<string>, indent = 2): Array.NonEmptyArray<string> =>
  pipe(
    [self, 0],
    treeHylo(
      annotateDepthUnfold<string>,
      preOrderFold<readonly [string, number]>,
    ),
    Array.map(([a, depth]) =>
      pipe((depth - 1) * indent, String.nSpaces, String.suffix(a)),
    ),
  )
 
/**
 * Encode a string tree into a `YAML`-like indented format where indentation, by
 * default set at `2` spaces, indicates node depth.
 *
 * You will find a curried version under the key `curried`.
 * @param self The tree to be encoded.
 * @param indent Optional number of space characters that separate adjacent tree levels. Default is `2`.
 * @category codec
 * @function
 */
export const encode: {
  (self: Tree<string>, indent?: number): Array.NonEmptyArray<string>
  curried: (
    indent?: number,
  ) => (self: Tree<string>) => Array.NonEmptyArray<string>
} = Object.assign(_encode, {
  curried:
    (indent?: number) =>
    (self: Tree<string>): Array.NonEmptyArray<string> =>
      _encode(self, indent),
})
 
/**
 * Decode a list of indented lines into a string tree.
 * @param lines Non-empty array of non-empty strings, each encoding a tree
 * node with indent set as a multiple of node depth.
 * @returns A decoded string tree.
 * @category codec
 * @function
 */
export const decode = (lines: Array.NonEmptyArray<string>): Tree<string> => {
  const [first, second] = lines
  if (second === undefined) return fixTree(TreeF.leafF(first))
 
  const indentSize = countStartingSpaces(second),
    computeDepth = (line: string) => countStartingSpaces(line) / indentSize,
    mapped: Array.NonEmptyArray<[string, number]> = pipe(
      lines,
      Array.map(line => [line.trimStart(), computeDepth(line)] as const),
    )
 
  return pipe(mapped, treeAna(decodeIndentedUnfold))
}
 
/**
 * Decode a single level of a tree from a list of indented lines where indentation
 * represents node depth.
 * @category codec
 * @category unfold
 * @function
 */
export const decodeIndentedUnfold = ([
  [value, depth],
  second,
  ...tail
]: Array.NonEmptyArray<[string, number]>): TreeF.TreeF<
  string,
  Array.NonEmptyArray<[string, number]>
> => {
  if (second === undefined) return TreeF.leafF(value)
 
  const nodes: Array.NonEmptyArray2<[string, number]> = [[second]]
 
  for (const [lineValue, lineDepth] of tail)
    if (lineDepth > depth + 1)
      (nodes.at(-1) as Array.NonEmptyArray<[string, number]>).push([
        lineValue,
        lineDepth,
      ])
    else nodes.push([[lineValue, lineDepth]])
 
  return TreeF.branchF(value, nodes)
}
 
const countStartingSpaces = (s: string): number =>
  pipe(s, String.takeWhile(String.isEqual(' ')), String.length)