All files / diff / apply.ts

98.18% Branches 54/55
100.00% Functions 1/1
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
121
122
123
124
125
 
 
 
 
 
 
x1
 
 
x1
 
 
 
 
 
x1
x1
x1
x1
x1
x1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
x1
x26
x26
x26
x18
x18
x26
x26
x26
x26
x26
 
x69
x69
x44
x44
x100
 
x25
x25
x25
x25
x69
x163
x163
x163
x38
x38
x38
x1
x1
x1
x37
x37
x163
x36
x36
x36
x36
x163
x70
x1
x1
x1
x69
x69
x69
x163
x12
x12
x12
x163
x4
x4
x163
x163
x23
x23
 
x23
x23
x69
x1
x1
x69
x1
x1
x69
x4
x4
x69
 
x26
x4
x4
x26
x26
x5
 
 
 
x26
x4
x4
x22
x26



















































































































I






/**
 * Apply back an unified patch to a string.
 * @module
 */

// 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
}