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
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
|
# Pattern is a zero-conflict wrapper extending RegExp features
# in order to make YAML parsing regex more expressive.
#
class Pattern
# @property [RegExp] The RegExp instance
regex: null
# @property [String] The raw regex string
rawRegex: null
# @property [String] The cleaned regex string (used to create the RegExp instance)
cleanedRegex: null
# @property [Object] The dictionary mapping names to capturing bracket numbers
mapping: null
# Constructor
#
# @param [String] rawRegex The raw regex string defining the pattern
#
constructor: (rawRegex, modifiers = '') ->
cleanedRegex = ''
len = rawRegex.length
mapping = null
# Cleanup raw regex and compute mapping
capturingBracketNumber = 0
i = 0
while i < len
_char = rawRegex.charAt(i)
if _char is '\\'
# Ignore next character
cleanedRegex += rawRegex[i..i+1]
i++
else if _char is '('
# Increase bracket number, only if it is capturing
if i < len - 2
part = rawRegex[i..i+2]
if part is '(?:'
# Non-capturing bracket
i += 2
cleanedRegex += part
else if part is '(?<'
# Capturing bracket with possibly a name
capturingBracketNumber++
i += 2
name = ''
while i + 1 < len
subChar = rawRegex.charAt(i + 1)
if subChar is '>'
cleanedRegex += '('
i++
if name.length > 0
# Associate a name with a capturing bracket number
mapping ?= {}
mapping[name] = capturingBracketNumber
break
else
name += subChar
i++
else
cleanedRegex += _char
capturingBracketNumber++
else
cleanedRegex += _char
else
cleanedRegex += _char
i++
@rawRegex = rawRegex
@cleanedRegex = cleanedRegex
@regex = new RegExp @cleanedRegex, 'g'+modifiers.replace('g', '')
@mapping = mapping
# Executes the pattern's regex and returns the matching values
#
# @param [String] str The string to use to execute the pattern
#
# @return [Array] The matching values extracted from capturing brackets or null if nothing matched
#
exec: (str) ->
@regex.lastIndex = 0
matches = @regex.exec str
if not matches?
return null
if @mapping?
for name, index of @mapping
matches[name] = matches[index]
return matches
# Tests the pattern's regex
#
# @param [String] str The string to use to test the pattern
#
# @return [Boolean] true if the string matched
#
test: (str) ->
@regex.lastIndex = 0
return @regex.test str
# Replaces occurences matching with the pattern's regex with replacement
#
# @param [String] str The source string to perform replacements
# @param [String] replacement The string to use in place of each replaced occurence.
#
# @return [String] The replaced string
#
replace: (str, replacement) ->
@regex.lastIndex = 0
return str.replace @regex, replacement
# Replaces occurences matching with the pattern's regex with replacement and
# get both the replaced string and the number of replaced occurences in the string.
#
# @param [String] str The source string to perform replacements
# @param [String] replacement The string to use in place of each replaced occurence.
# @param [Integer] limit The maximum number of occurences to replace (0 means infinite number of occurences)
#
# @return [Array] A destructurable array containing the replaced string and the number of replaced occurences. For instance: ["my replaced string", 2]
#
replaceAll: (str, replacement, limit = 0) ->
@regex.lastIndex = 0
count = 0
while @regex.test(str) and (limit is 0 or count < limit)
@regex.lastIndex = 0
str = str.replace @regex, replacement
count++
return [str, count]
module.exports = Pattern
|