summaryrefslogtreecommitdiff
path: root/mod.ts
blob: 2600198d96a56d34a85c82386c701efefa1b1c9e (plain) (blame)
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
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
/*
 * SPDX-License-Identifier: MIT
 * Copyright © 2015-2025 Desmond Brand
 * Copyright © 2026 Paul Anthony Webb (modifications)
 *
 * Dap is distributed in the hope that it will be useful, but WITHOUT ANY
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
 * A PARTICULAR PURPOSE.
 */

/*** UTILITY ------------------------------------------ ***/

const dedent: Dedent = createDedent({});

interface Dedent {
	(literals: string): string;
	(strings: TemplateStringsArray, ...values: unknown[]): string;
	withOptions: CreateDedent;
}

interface DedentOptions {
	alignValues?: boolean;
	escapeSpecialCharacters?: boolean;
	trimWhitespace?: boolean;
}

type CreateDedent = (options: DedentOptions) => Dedent;

/*** EXPORT ------------------------------------------- ***/

export default dedent;

/*** PROGRAM ------------------------------------------ ***/

function createDedent(options: DedentOptions): Dedent {
  function dedent(literals: string): string;
  function dedent(strings: TemplateStringsArray, ...values: unknown[]): string;

  function dedent(strings: TemplateStringsArray | string, ...values: unknown[]) {
    const raw = typeof strings === "string" ?
      [strings] :
      strings.raw;

    const {
      alignValues = false,
      escapeSpecialCharacters = Array.isArray(strings),
      trimWhitespace = true
    } = options;

    let result = "";

    for (let i = 0; i < raw.length; i++) {
      let next = raw[i];

      if (escapeSpecialCharacters) {
        next = next
          .replace(/\\\n[ \t]*/g, "")
          .replace(/\\`/g, "`")
          .replace(/\\\$/g, "$")
          .replace(/\\\{/g, "{");
      }

      result += next;

      if (i < values.length) {
        result += alignValues ?
          alignValue(values[i], result) :
          values[i];
      }
    }

    const lines = result.split("\n");
    let minIndent: number | null = null;

    for (const line of lines) {
      const match = line.match(/^(\s+)\S+/);

      if (match) {
        const indent = match[1].length;

        minIndent = minIndent === null ?
          indent :
          Math.min(minIndent, indent);
      }
    }

    if (minIndent !== null) {
      result = lines
        .map((line) => (line[0] === " " || line[0] === "\t" ? line.slice(minIndent) : line))
        .join("\n");
    }

    if (trimWhitespace)
      result = result.trim();

    if (escapeSpecialCharacters)
      result = result.replace(/\\n/g, "\n");

    return result;
  }

  dedent.withOptions = (newOptions: DedentOptions): Dedent => createDedent({ ...options, ...newOptions });

  return dedent;
}

/*** HELPER ------------------------------------------- ***/

function alignValue(value: unknown, precedingText: string) {
  if (typeof value !== "string" || !value.includes("\n"))
    return value;

  const currentLine = precedingText.slice(precedingText.lastIndexOf("\n") + 1);
  const indentMatch = currentLine.match(/^(\s+)/);

  if (indentMatch)
    return value.replace(/\n/g, `\n${indentMatch[1]}`);

  return value;
}

/*** adapted from https://github.com/dmnd/dedent ***/