All files / mustache / capture.ts

100.00% Branches 28/28
100.00% Functions 1/1
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
x54
x54
x54
x54
x54
 
x791
x335
x40
x16
x16
x40
x40
x40
x335
x335
 
x791
x38
x38
x38
x38
x38
x38
x38
x38
 
x791
x44
x44
x44
 
x791
x44
x44
x44
x44
x791
 
x26
x2
x2
x14
x54



































































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