summaryrefslogtreecommitdiff
path: root/src/Pattern.coffee
blob: 82f96e70ee5c373b429d8f120d5469421d540724 (plain)
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