All files / diff / apply.ts

96.97% Branches 32/33
96.67% Lines 87/90
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
 
x1
 
 
x1
 
 
 
 
 
x1
x1
x1
x1
x1
x1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
x1
x27
x27
x27
x45
x45
x27
x27
x27
x27
x27
 
x96
x96
x140
x140
x684
 
x121
x121
x605
x121
x96
x259
x259
x556
x297
x297
x297
x298
x298
x298
x334
x334
x259
x295
x295
x295
x295
x259
x329
x330
x330
x330
x398
x398
x398
x259
x271
x271
x271
x259
x263
x263
x259
x259
x119
x119
 
x119
x119
x96
x97
x97
x96
x97
x97
x96
x100
x100
x96
 
x27
x31
x31
x27
x27
x32
 
 
 
x27
x31
x31
x49
x27














































































































I






// Imports
import { tokenize } from "./diff.ts"

/** Hunk header regular expression */
const HUNK_HEADER = /^@@ -(?<ai>\d+)(?:,(?<aj>\d+))? \+(?<bi>\d+)(?:,(?<bj>\d+))? @@/

/**
 * ANSI patterns
 * https://github.com/chalk/ansi-regex/blob/02fa893d619d3da85411acc8fd4e2eea0e95a9d9/index.js
 */
const ANSI_PATTERN = new RegExp(
  [
    "[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]+)*|[a-zA-Z\\d]+(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)",
    "(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TXZcf-nq-uy=><~]))",
  ].join("|"),
  "g",
)

/**
 * Apply back an unified patch to a string.
 *
 * ```ts
 * import { apply } from "./apply.ts"
 * apply("foo\n", `--- a
 * +++ b
 * @@ -1 +1 @@
 * -foo
 * +foo
 * \\ No newline at end of file`)
 * ```
 *
 * @author Simon Lecoq (lowlighter)
 * @license MIT
 */
export function apply(a: string, patch: string): string {
  const patchable = patch.trim() ? tokenize(patch.replace(ANSI_PATTERN, "")) : []
  const b = tokenize(a)
  if (b.at(-1) === "\n") {
    b.pop()
  }
  let offset = 0
  let k = 0
  let newline = (patchable.length > 0) || (a.endsWith("\n"))
  const errors = []
  for (let i = 0; i < patchable.length; i++) {
    // Parse hunk header
    const header = patchable[i].match(HUNK_HEADER)
    if (!header) {
      continue
    }
    const { ai, aj, bi, bj } = Object.fromEntries(Object.entries(header.groups!).map(([k, v]) => [k, Number(v ?? 1)]))
    // Apply hunk
    try {
      let j = ai - 1 + offset
      const count = { aj: 0, bj: 0, context: 0 }
      k++
      while ((++i < patchable.length) && (!HUNK_HEADER.test(patchable[i]))) {
        const diff = patchable[i]
        switch (true) {
          case diff.startsWith("-"): {
            let [removed] = b.splice(j, 1)
            count.aj++
            if (removed !== diff.slice(1)) {
              removed ??= ""
              throw new SyntaxError(`Patch ${k}: line ${j} mismatch (expected "${diff.slice(1).trim()}", actual "${removed.trim()}")`)
            }
            break
          }
          case diff.startsWith("+"):
            b.splice(j, 0, patchable[i].slice(1))
            j++
            count.bj++
            break
          case diff.startsWith(" "):
            if (b[j] !== diff.slice(1)) {
              b[j] ??= ""
              throw new SyntaxError(`Patch ${k}: line ${j} mismatch (expected "${diff.slice(1).trim()}", actual "${b[j].trim()}")`)
            }
            j++
            count.context++
            break
          case diff === "\n":
            j++
            count.context++
            break
          case (i + 1 === patchable.length) && (diff === "\\ No newline at end of file\n"):
            newline = false
            break
        }
      }
      i--
      offset = j - (bi + bj)
      // Check hunk header counts
      count.bj += count.context
      count.aj += count.context
      if (count.bj !== bj) {
        throw new SyntaxError(`Patch ${k}: hunk header text b count mismatch (expected ${bj}, actual ${count.bj})`)
      }
      if (count.aj !== aj) {
        throw new SyntaxError(`Patch ${k}: hunk header text a count mismatch (expected ${aj}, actual ${count.aj})`)
      }
    } catch (error) {
      errors.push(error)
    }
  }
  // Return patched string
  if (!b.length) {
    newline = false
  }
  let result = b.join("")
  if ((result.endsWith("\n")) && (!newline)) {
    result = result.slice(0, -1)
  } else if (!result.endsWith("\n") && newline) {
    result += "\n"
  }
  if (errors.length) {
    throw new AggregateError(errors, result)
  }
  return result
}