123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246 |
- import { not } from "fp-ts/Predicate"
- import { pipe, flow } from "fp-ts/function"
- import * as Str from "fp-ts/string"
- import * as RA from "fp-ts/ReadonlyArray"
- import * as A from "fp-ts/Array"
- import * as O from "fp-ts/Option"
- import * as E from "fp-ts/Either"
- import * as P from "parser-ts/Parser"
- import * as S from "parser-ts/string"
- import * as C from "parser-ts/char"
- import { recordUpdate } from "./utils/record"
- /**
- * Special characters in the Raw Key Value Grammar
- */
- const SPECIAL_CHARS = ["#", ":"] as const
- export type RawKeyValueEntry = {
- key: string
- value: string
- active: boolean
- }
- /* Beginning of Parser Definitions */
- const wsSurround = P.surroundedBy(S.spaces)
- const stringArrayJoin = (sep: string) => (input: string[]) => input.join(sep)
- const stringTakeUntilChars = (chars: C.Char[]) => pipe(
- P.takeUntil((c: C.Char) => chars.includes(c)),
- P.map(stringArrayJoin("")),
- )
- const stringTakeUntilCharsInclusive = flow(
- stringTakeUntilChars,
- P.chainFirst(() => P.sat(() => true)),
- )
- const quotedString = pipe(
- S.doubleQuotedString,
- P.map((x) => JSON.parse(`"${x}"`))
- )
- const key = pipe(
- wsSurround(quotedString),
- P.alt(() =>
- pipe(
- stringTakeUntilChars([":", "\n"]),
- P.map(Str.trim)
- )
- )
- )
- const value = pipe(
- wsSurround(quotedString),
- P.alt(() =>
- pipe(
- stringTakeUntilChars(["\n"]),
- P.map(Str.trim)
- )
- )
- )
- const commented = pipe(
- S.maybe(S.string("#")),
- P.map(not(Str.isEmpty))
- )
- const line = pipe(
- wsSurround(commented),
- P.bindTo("commented"),
- P.bind("key", () => wsSurround(key)),
- P.chainFirst(() => C.char(":")),
- P.bind("value", () => value),
- )
- const lineWithNoColon = pipe(
- wsSurround(commented),
- P.bindTo("commented"),
- P.bind("key", () => P.either(
- stringTakeUntilCharsInclusive(["\n"]),
- () => pipe(
- P.manyTill(P.sat((_: string) => true), P.eof()),
- P.map(flow(
- RA.toArray,
- stringArrayJoin("")
- ))
- )
- )),
- P.map(flow(
- O.fromPredicate(({ key }) => !Str.isEmpty(key))
- ))
- )
- const file = pipe(
- P.manyTill(wsSurround(line), P.eof()),
- )
- /**
- * This Raw Key Value parser ignores the key value pair (no colon) issues
- */
- const tolerantFile = pipe(
- P.manyTill(
- P.either(
- pipe(line, P.map(O.some)),
- () => pipe(
- lineWithNoColon,
- P.map(flow(
- O.map((a) => ({ ...a, value: "" }))
- ))
- )
- ),
- P.eof()
- ),
- P.map(flow(
- RA.filterMap(flow(
- O.fromPredicate(O.isSome),
- O.map((a) => a.value)
- ))
- ))
- )
- /* End of Parser Definitions */
- /**
- * Detect whether the string needs to have escape characters in raw key value strings
- * @param input The string to check against
- */
- const stringNeedsEscapingForRawKVString = (input: string) => {
- // If there are any of our special characters, it needs to be escaped definitely
- if (SPECIAL_CHARS.some((x) => input.includes(x)))
- return true
- // The theory behind this impl is that if we apply JSON.stringify on a string
- // it does escaping and then return a JSON string representation.
- // We remove the quotes of the JSON and see if it can be matched against the input string
- const stringified = JSON.stringify(input)
- const y = stringified
- .substring(1, stringified.length - 1)
- .trim()
- return y !== input
- }
- /**
- * Applies Raw Key Value escaping (via quotes + escape chars) if needed
- * @param input The input to apply escape on
- * @returns If needed, the escaped string, else the input string itself
- */
- const applyEscapeIfNeeded = (input: string) =>
- stringNeedsEscapingForRawKVString(input)
- ? JSON.stringify(input)
- : input
- /**
- * Converts Raw Key Value Entries to the file string format
- * @param entries The entries array
- * @returns The entries in string format
- */
- export const rawKeyValueEntriesToString = (entries: RawKeyValueEntry[]) =>
- pipe(
- entries,
- A.map(
- flow(
- recordUpdate("key", applyEscapeIfNeeded),
- recordUpdate("value", applyEscapeIfNeeded),
- ({ key, value, active }) =>
- active ? `${(key)}: ${value}` : `# ${key}: ${value}`
- )
- ),
- stringArrayJoin("\n")
- )
- /**
- * Parses raw key value entries string to array
- * @param s The file string to parse from
- * @returns Either the parser fail result or the raw key value entries
- */
- export const parseRawKeyValueEntriesE = (s: string) =>
- pipe(
- tolerantFile,
- S.run(s),
- E.mapLeft((err) => ({
- message: `Expected ${err.expected.map((x) => `'${x}'`).join(", ")}`,
- expected: err.expected,
- pos: err.input.cursor,
- })),
- E.map(
- ({ value }) => pipe(
- value,
- RA.map(({ key, value, commented }) =>
- <RawKeyValueEntry>{
- active: !commented,
- key,
- value
- }
- )
- )
- )
- )
- /**
- * Less error tolerating version of `parseRawKeyValueEntriesE`
- * @param s The file string to parse from
- * @returns Either the parser fail result or the raw key value entries
- */
- export const strictParseRawKeyValueEntriesE = (s: string) =>
- pipe(
- file,
- S.run(s),
- E.mapLeft((err) => ({
- message: `Expected ${err.expected.map((x) => `'${x}'`).join(", ")}`,
- expected: err.expected,
- pos: err.input.cursor,
- })),
- E.map(
- ({ value }) => pipe(
- value,
- RA.map(({ key, value, commented }) =>
- <RawKeyValueEntry>{
- active: !commented,
- key,
- value
- }
- )
- )
- )
- )
- /**
- * Kept for legacy code compatibility, parses raw key value entries.
- * If failed, it returns an empty array
- * @deprecated Use `parseRawKeyValueEntriesE` instead
- */
- export const parseRawKeyValueEntries = flow(
- parseRawKeyValueEntriesE,
- E.map(RA.toArray),
- E.getOrElse(() => [] as RawKeyValueEntry[])
- )
|