UNPKG

8.23 kBJavaScriptView Raw
1'use strict'
2
3var _eval = require('../_eval'),
4 ParseError = require('./ParseError'),
5 Mixin = require('./Mixin')
6
7/**
8 * @class
9 * @param {number} sourceLine
10 */
11function Obj(sourceLine) {
12 /** @param {string[]} */
13 this.lines = []
14
15 /**
16 * @member {Object}
17 * @property {number} begin
18 * @property {number} end
19 */
20 this.source = {
21 begin: sourceLine,
22 end: sourceLine
23 }
24
25 /** @member {boolean} */
26 this.parsed = false
27
28 /** @member {Object|Array|string|Mixin} */
29 this.value = null
30}
31
32/**
33 * Add one more line as the source of the Obj
34 * @param {string} line
35 * @throws if already parsed
36 */
37Obj.prototype.push = function (line) {
38 if (this.parsed) {
39 throw new Error('Can\t push, Obj already parsed')
40 }
41 if (line.trim() === '' && !this.lines.length) {
42 // Ignore the first blank line
43 this.source.begin++
44 } else if (!/^\s*\/\//.test(line)) {
45 // Ignore comment lines
46 this.lines.push(line)
47 }
48 this.source.end++
49}
50
51/**
52 * Parses the object content and make it ready to execute
53 * @returns {Obj} itself
54 */
55Obj.prototype.parse = function () {
56 var i
57
58 if (!this.parsed) {
59 // Ignore empty lines at the end
60 for (i = this.lines.length - 1; i >= 0; i--) {
61 if (this.lines[i].trim()) {
62 break
63 }
64 }
65 this.lines = this.lines.slice(0, i + 1)
66
67 if (this.lines.length && this.lines[0][0] === '\t') {
68 throw new ParseError('Unexpected "\\t" at the line beginning', this)
69 }
70
71 if (!(this._parseArray() ||
72 this._parseObject() ||
73 this._parseMixin() ||
74 this._parseJS())) {
75 throw new ParseError('Invalid syntax', this)
76 }
77 this.parsed = true
78
79 // Remove now useless data
80 delete this.lines
81 }
82 return this
83}
84
85/**
86 * Execute and return the result for the parsed Obj
87 * @param {Object} context
88 * @param {string} name A string like '<' + description + '>' to be part of a thrown execption
89 * @returns {*}
90 * @throws if not parsed
91 */
92Obj.prototype.execute = function (context, name) {
93 var r, key
94
95 if (!this.parsed) {
96 throw new Error('Can\'t execute, Obj not parsed')
97 }
98
99 if (Array.isArray(this.value)) {
100 r = this.value.map(function (each, i) {
101 return each.execute(context, name + '.' + i)
102 })
103 r.isOrdered = this.value.isOrdered
104 return r
105 } else if (typeof this.value === 'string') {
106 return _eval(this.value, context, name)
107 } else if (this.value instanceof Mixin) {
108 return this.value.execute(context, name)
109 } else {
110 r = Object.create(null)
111 for (key in this.value) {
112 r[key] = this.value[key].execute(context, name + '.' + key)
113 }
114 return r
115 }
116}
117
118/**
119 * Get an empty Obj already parsed
120 * This obj, when parsed, will give `{}`
121 * @returns {Obj}
122 */
123Obj.empty = function () {
124 var obj = new Obj(0)
125 obj.push('({})')
126 return obj.parse()
127}
128
129module.exports = Obj
130
131/**
132 * @returns {boolean} false if it's probably not an array
133 * @throws {ParseError} if invalid syntax
134 * @private
135 */
136Obj.prototype._parseArray = function () {
137 var i, line, obj
138
139 if (!this.lines.length || (this.lines[0][0] !== '*' && this.lines[0][0] !== '@')) {
140 // An array must start with a '*' or '@'
141 return false
142 }
143
144 // Split each array element
145 this.value = []
146 this.value.isOrdered = true
147 for (i = 0; i < this.lines.length; i++) {
148 line = this.lines[i]
149 if (line[0] === '*' || line[0] === '@') {
150 // A new element
151 if (line[1] !== '\t') {
152 throw new ParseError('Expected a "\\t" after "' + line[0] + '"', this)
153 }
154 if (obj) {
155 this.value.push(obj.parse())
156 }
157 obj = new Obj(this.source.begin + i)
158 obj.push(line.substr(2))
159
160 // Get ordering
161 if (!i) {
162 this.value.isOrdered = line[0] === '*'
163 } else if (this.value.isOrdered !== (line[0] === '*')) {
164 throw new ParseError('Either all elements start with "*" or "@"', this)
165 }
166 } else if (line[0] === '\t') {
167 // Last obj continuation
168 obj.push(line.substr(1))
169 } else {
170 throw new ParseError('Expected either "*", "@" or "\\t"', this)
171 }
172 }
173 this.value.push(obj.parse())
174 return true
175}
176
177/**
178 * @param {boolean} [acceptPath=false] whether the object keys can be paths
179 * @returns {boolean} false if it's probably not an object
180 * @throws {ParseError} if invalid syntax
181 * @private
182 */
183Obj.prototype._parseObject = function (acceptPath) {
184 var that = this,
185 keyRegex = /^(?:([a-z$_][a-z0-9$_]*\??)|(['"])((\\\\|\\\2|.)*?)\2):/i,
186 pathRegex = /^((\d+|[a-z$_][a-z0-9$_]*)(\.(\d+|[a-z$_][a-z0-9$_]*))*):/i,
187 regex = acceptPath ? pathRegex : keyRegex,
188 i, line, obj, match, key
189
190 if (!this.lines.length || !regex.test(this.lines[0])) {
191 // An object must start with '_key_:'
192 return false
193 }
194
195 this.value = Object.create(null)
196 var save = function (key, obj) {
197 if (obj) {
198 if (key in that.value) {
199 throw new ParseError('Duplicate key "' + key + '"', that)
200 }
201 that.value[key] = obj.parse()
202 }
203 }
204
205 // Split each object key element
206 for (i = 0; i < this.lines.length; i++) {
207 line = this.lines[i]
208 if ((match = line.match(regex))) {
209 // A new key
210 save(key, obj)
211 // 1 for path and simple key; 3 for escaped key
212 key = acceptPath ? match[1] : match[1] || match[3]
213 obj = new Obj(this.source.begin + i)
214 obj.push(line.substr(match[0].length).trim())
215 } else if (line[0] === '\t') {
216 // Last obj continuation
217 obj.push(line.substr(1))
218 } else if (line) { // Ignore empty lines
219 throw new ParseError('Expected either "_key_:" or "\\t"', this)
220 }
221 }
222 save(key, obj)
223 return true
224}
225
226/**
227 * @returns {boolean} false if it's probably not a mixin
228 * @throws {ParseError} if invalid syntax
229 * @private
230 */
231Obj.prototype._parseMixin = function () {
232 var path, str, i, line, additions
233
234 if (!this.lines.length ||
235 !(path = readPath(this.lines[0])) ||
236 !path.newStr.match(/^with(out)?( |$)/)) {
237 // Must start with a path followed by 'with' or 'without'
238 return false
239 }
240
241 // Base object
242 this.value = new Mixin
243 this.value.base = path.parts
244 str = path.newStr
245
246 // Without
247 if (str.indexOf('without') === 0) {
248 str = eat(str, 7)
249 while ((path = readPath(str))) {
250 this.value.removals.push(path.parts)
251 str = path.newStr
252
253 if (!str || str[0] === ';') {
254 // End of 'without' list
255 str = eat(str, 1)
256 break
257 } else if (str[0] === ',') {
258 str = eat(str, 1)
259 } else {
260 throw new ParseError('Expected either ";" or "," after path "' + path.name + '"', this)
261 }
262 }
263 }
264
265 // With
266 if (str.indexOf('with') === 0) {
267 str = eat(str, 4)
268 additions = new Obj(this.source.begin)
269 additions.push(str)
270 for (i = 1; i < this.lines.length; i++) {
271 line = this.lines[i]
272 if (line[0] === '\t') {
273 additions.push(line.substr(1))
274 } else {
275 throw new ParseError('Expected the line to start with "\\t"', this)
276 }
277 }
278
279 if (!additions._parseObject(true)) {
280 throw new ParseError('Expected an object sub-document', additions)
281 }
282 additions.parsed = true
283
284 this.value.additions = additions
285 } else if (this.lines.length !== 1) {
286 throw new ParseError('Could not parse as mixin', this)
287 }
288
289 return true
290}
291
292/**
293 * Try to extract a path from the beginning of the string
294 * @param {string} str
295 * @returns {Object} with keys 'name', 'parts' and 'newStr' or null if no path could be read
296 */
297function readPath(str) {
298 var match, parts
299 match = str.match(/^(([0-9]+|[a-zA-Z$_][a-zA-Z0-9$_]*)(\.([0-9]+|[a-zA-Z$_][a-zA-Z0-9$_]*))*)/)
300 if (match) {
301 parts = match[1].split('.').map(function (each) {
302 return /^[0-9]+$/.test(each) ? Number(each) : each
303 })
304 return {
305 name: match[1],
306 parts: parts,
307 newStr: eat(str, match[1].length)
308 }
309 }
310}
311
312/**
313 * Remove `n` chars and blank chars from the beginning of the string
314 * @param {string} str
315 * @param {number} n
316 * @returns {string}
317 */
318function eat(str, n) {
319 return str.substr(n).trimLeft()
320}
321
322/**
323 * @returns {boolean} false if it's probably not a JS expression
324 * @throws {ParseError} if invalid syntax
325 * @private
326 */
327Obj.prototype._parseJS = function () {
328 if (this.lines.length !== 1) {
329 return false
330 }
331 this.value = this.lines[0]
332 return true
333}
\No newline at end of file