UNPKG

3.92 kBJavaScriptView Raw
1'use strict'
2let extend = require('util')._extend
3
4/**
5 * Regex-based parser tool.
6 *
7 * rules = new Matcher({
8 * date: /\d\d\/\d\d:/
9 * transaction: '%{amount} - %{description}'
10 * amount: /\d+/
11 * description: /.*?/
12 * })
13 *
14 * Iterating through lines (`#switch`):
15 *
16 * lines.forEach(function (line) {
17 * rules.switch(line, {
18 * transaction: function (m) {
19 * console.log(" Bought %s for %s", m.description, m.amount)
20 * },
21 * date: function (m) {
22 * }
23 * })
24 * })
25 */
26
27function Matcher (rules) {
28 var options = rules.options
29 delete rules.options
30
31 this.rules = extend({}, Matcher.defaults.rules)
32 this.rules = extend(this.rules, rules)
33
34 this.options = extend({}, Matcher.defaults.options)
35 this.options = extend(this.options, options)
36
37 this._cache = {}
38}
39
40Matcher.defaults = {
41 rules: {
42 space: '\\s+'
43 },
44 options: {
45 trim: false
46 }
47}
48
49Matcher.prototype = {
50 /**
51 * Returns info on a given rule, including its regex
52 *
53 * m.build('function')
54 * => { regexp: '^...$', indices: '..' }
55 */
56
57 build (id) {
58 var rule = this.partial(id)
59 if (!rule) return
60
61 var re = rule.regexp
62 if (this.options.trim) re = '\\s*' + re + '\\s*'
63
64 return {
65 regexp: '^' + re + '$',
66 indices: rule.indices
67 }
68 },
69
70 partial (id) {
71 if (this._cache[id]) return this._cache[id]
72
73 var re
74 var regexp = this.rules[id]
75 if (!regexp) return
76
77 if (regexp.many) {
78 re = this.partialFromMany(regexp)
79 } else {
80 re = this.partialFromRegexp(regexp)
81 }
82
83 this._cache[id] = re
84 return re
85 },
86
87 /**
88 * Internal: Builds a partial regex.
89 */
90
91 partialFromRegexp (regexp) {
92 var indices = []
93 var matcher = this
94
95 // Convert to string
96 if (regexp.constructor === RegExp) regexp = regexp.source
97
98 // Whitespace
99 regexp = regexp.replace(/ /g, this.rules.space)
100 regexp = escapeGroups(regexp)
101
102 // Recurse into nested partials
103 regexp = regexp.replace(/%\{(?:([A-Za-z0-9]+):)?([A-Za-z0-9]+)\}/g, function (_, alias, id) {
104 var r = matcher.partial(id)
105 indices.push(alias || id)
106 indices = indices.concat(r.indices)
107 return `(${r.regexp})`
108 })
109
110 return { regexp: regexp, indices: indices }
111 },
112
113 partialFromMany (def) {
114 if (!def.separator) throw new Error('No separator')
115
116 var many = def.many
117 var sep = def.separator
118 if (sep.source) sep = sep.source
119
120 var regexp = this.partial(many).regexp
121 regexp = escapeGroups(regexp)
122 regexp = `${regexp}(?:${sep}${regexp})*`
123
124 return { regexp: regexp, indices: [] }
125 },
126
127 /**
128 * Match
129 */
130
131 match (id, str) {
132 var matcher = this.build(id)
133 if (!matcher) return
134
135 var m = str.match(matcher.regexp)
136 if (!m) return false
137
138 var output = {}
139 output[id] = m[0]
140
141 if (matcher.indices.length > 0) {
142 matcher.indices.forEach(function (name, i) {
143 output[name] = m[i + 1]
144 })
145 }
146
147 return output
148 },
149
150 /**
151 * Multi
152 *
153 * multi(['doc', 'docend'], '..source..')
154 */
155
156 multi (ids, str) {
157 for (var i in ids) {
158 var id = ids[i]
159 var output = this.match(id, str)
160 if (output) {
161 if (typeof output === 'object') {
162 output.rule = id
163 return output
164 } else {
165 return {
166 value: output,
167 rule: id
168 }
169 }
170 }
171 }
172
173 return false
174 },
175
176 /**
177 * Switch
178 */
179
180 switch (str, callbacks) {
181 var ids = Object.keys(callbacks)
182 var m = this.multi(ids, str)
183
184 if (!m) return callbacks.else ? callbacks.else(m) : false
185
186 return callbacks[m.rule](m)
187 }
188}
189
190/**
191 * Change unescaped parentheses ("(hello) => (?:hello)")
192 */
193
194function escapeGroups (regexp) {
195 return regexp.replace(/^\(|[^\\]\((?!\?:)/g, function (match) {
196 return match + '?:'
197 })
198}
199
200module.exports = Matcher