All files / mustache / capture.ts

100.00% Branches 22/22
100.00% Lines 48/48
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
 
 
 
 
x9
x9
x9
x9
x9
x9
x9
x9
 
 
 
 
 
 
 
 
 
 
 
 
 
x9
x63
x63
x63
x63
x63
 
x854
x1189
x1229
x1245
x1245
x1229
x1229
x1229
x1189
x1189
 
x854
x892
x892
x892
x892
x892
x892
x892
x892
 
x854
x898
x898
x898
 
x854
x898
x4490
x898
x898
x854
 
x89
x91
x91
x77
x63



































































// Imports
import type { Nullable } from "@libs/typing"

/** Tokens. */
const tokens = {
  "(": ")",
  "[": "]",
  "{": "}",
  '"': '"',
  "'": "'",
  "`": "`",
} as Record<PropertyKey, string>

/**
 * Capture content between delimiters.
 *
 * ```ts
 * const { captured, match } = capture("foo {{ bar }} baz")!
 * console.assert(captured === "bar")
 * console.assert(match === "{{ bar }}")
 * ```
 *
 * @author Simon Lecoq (lowlighter)
 * @license MIT
 */
export function capture(string: string, offset = 0): Nullable<{ a: number; b: number; match: string; captured: string; triple: boolean }> {
  const stack = []
  let a = NaN
  let d = 2
  let quoted = false
  for (let i = offset; i < string.length; i++) {
    // Start capturing upon meeting mustache opening
    if (Number.isNaN(a)) {
      if ((string[i] === "{") && (string[i + 1] === "{")) {
        if (string[i + 2] === "{") {
          d = 3
        }
        a = i
        i += d - 1
      }
      continue
    }
    // Close capturing on mustache closing (stack must be empty)
    if ((!stack.length) && (string[i] === "}") && (string[i + 1] === "}") && ((d === 2) || (string[i + 2] === "}"))) {
      return {
        a,
        b: i + d,
        match: string.slice(a, i + d),
        captured: string.slice(a + d, i).trim(),
        triple: d === 3,
      }
    }
    // Close group on closing token
    if (string[i] === tokens[stack.at(-1)!]) {
      stack.pop()
      continue
    }
    // Open group on opening token
    if ((!quoted) && (string[i] in tokens)) {
      stack.push(string[i])
      quoted = ["'", '"', "`"].includes(string[i])
      continue
    }
  }
  // Throw on mustache unclosed
  if (!Number.isNaN(a)) {
    throw new SyntaxError(`Unclosed expression, unterminated expression at: ${a}\n${string.slice(a)}`)
  }
  return null
}