source: node_modules/minim/lib/primitives/Element.js

main
Last change on this file was d24f17c, checked in by Aleksandar Panovski <apano77@…>, 15 months ago

Initial commit

  • Property mode set to 100644
File size: 9.8 KB
Line 
1const isEqual = require('lodash/isEqual');
2const KeyValuePair = require('../KeyValuePair');
3const ArraySlice = require('../ArraySlice.js');
4
5/**
6 * @class
7 *
8 * @param content
9 * @param meta
10 * @param attributes
11 *
12 * @property {string} element
13 */
14class Element {
15 constructor(content, meta, attributes) {
16 // Lazy load this.meta and this.attributes because it's a Minim element
17 // Otherwise, we get into circuluar calls
18 if (meta) {
19 this.meta = meta;
20 }
21
22 if (attributes) {
23 this.attributes = attributes;
24 }
25
26 this.content = content;
27 }
28
29 /**
30 * Freezes the element to prevent any mutation.
31 * A frozen element will add `parent` property to every child element
32 * to allow traversing up the element tree.
33 */
34 freeze() {
35 if (Object.isFrozen(this)) {
36 return;
37 }
38
39 if (this._meta) {
40 this.meta.parent = this;
41 this.meta.freeze();
42 }
43
44 if (this._attributes) {
45 this.attributes.parent = this;
46 this.attributes.freeze();
47 }
48
49 this.children.forEach((element) => {
50 element.parent = this;
51 element.freeze();
52 }, this);
53
54 if (this.content && Array.isArray(this.content)) {
55 Object.freeze(this.content);
56 }
57
58 Object.freeze(this);
59 }
60
61 primitive() {
62
63 }
64
65 /**
66 * Creates a deep clone of the instance
67 */
68 clone() {
69 const copy = new this.constructor();
70
71 copy.element = this.element;
72
73 if (this.meta.length) {
74 copy._meta = this.meta.clone();
75 }
76
77 if (this.attributes.length) {
78 copy._attributes = this.attributes.clone();
79 }
80
81 if (this.content) {
82 if (this.content.clone) {
83 copy.content = this.content.clone();
84 } else if (Array.isArray(this.content)) {
85 copy.content = this.content.map(element => element.clone());
86 } else {
87 copy.content = this.content;
88 }
89 } else {
90 copy.content = this.content;
91 }
92
93 return copy;
94 }
95
96 /**
97 */
98 toValue() {
99 if (this.content instanceof Element) {
100 return this.content.toValue();
101 }
102
103 if (this.content instanceof KeyValuePair) {
104 return {
105 key: this.content.key.toValue(),
106 value: this.content.value ? this.content.value.toValue() : undefined,
107 };
108 }
109
110 if (this.content && this.content.map) {
111 return this.content.map(element => element.toValue(), this);
112 }
113
114 return this.content;
115 }
116
117 /**
118 * Creates a reference pointing at the Element
119 * @returns {RefElement}
120 * @memberof Element.prototype
121 */
122 toRef(path) {
123 if (this.id.toValue() === '') {
124 throw Error('Cannot create reference to an element that does not contain an ID');
125 }
126
127 const ref = new this.RefElement(this.id.toValue());
128
129 if (path) {
130 ref.path = path;
131 }
132
133 return ref;
134 }
135
136 /**
137 * Finds the given elements in the element tree.
138 * When providing multiple element names, you must first freeze the element.
139 *
140 * @param names {...elementNames}
141 * @returns {ArraySlice}
142 */
143 findRecursive(...elementNames) {
144 if (arguments.length > 1 && !this.isFrozen) {
145 throw new Error('Cannot find recursive with multiple element names without first freezing the element. Call `element.freeze()`');
146 }
147
148 const elementName = elementNames.pop();
149 let elements = new ArraySlice();
150
151 const append = (array, element) => {
152 array.push(element);
153 return array;
154 };
155
156 // Checks the given element and appends element/sub-elements
157 // that match element name to given array
158 const checkElement = (array, element) => {
159 if (element.element === elementName) {
160 array.push(element);
161 }
162
163 const items = element.findRecursive(elementName);
164 if (items) {
165 items.reduce(append, array);
166 }
167
168 if (element.content instanceof KeyValuePair) {
169 if (element.content.key) {
170 checkElement(array, element.content.key);
171 }
172
173 if (element.content.value) {
174 checkElement(array, element.content.value);
175 }
176 }
177
178 return array;
179 };
180
181 if (this.content) {
182 // Direct Element
183 if (this.content.element) {
184 checkElement(elements, this.content);
185 }
186
187 // Element Array
188 if (Array.isArray(this.content)) {
189 this.content.reduce(checkElement, elements);
190 }
191 }
192
193 if (!elementNames.isEmpty) {
194 elements = elements.filter((element) => {
195 let parentElements = element.parents.map(e => e.element);
196
197 // eslint-disable-next-line no-restricted-syntax
198 for (const namesIndex in elementNames) {
199 const name = elementNames[namesIndex];
200 const index = parentElements.indexOf(name);
201
202 if (index !== -1) {
203 parentElements = parentElements.splice(0, index);
204 } else {
205 return false;
206 }
207 }
208
209 return true;
210 });
211 }
212
213 return elements;
214 }
215
216 set(content) {
217 this.content = content;
218 return this;
219 }
220
221 equals(value) {
222 return isEqual(this.toValue(), value);
223 }
224
225 getMetaProperty(name, value) {
226 if (!this.meta.hasKey(name)) {
227 if (this.isFrozen) {
228 const element = this.refract(value);
229 element.freeze();
230 return element;
231 }
232
233 this.meta.set(name, value);
234 }
235
236 return this.meta.get(name);
237 }
238
239 setMetaProperty(name, value) {
240 this.meta.set(name, value);
241 }
242
243 /**
244 * @type String
245 */
246 get element() {
247 // Returns 'element' so we don't have undefined as element
248 return this._storedElement || 'element';
249 }
250
251 set element(element) {
252 this._storedElement = element;
253 }
254
255 get content() {
256 return this._content;
257 }
258
259 set content(value) {
260 if (value instanceof Element) {
261 this._content = value;
262 } else if (value instanceof ArraySlice) {
263 this.content = value.elements;
264 } else if (
265 typeof value == 'string'
266 || typeof value == 'number'
267 || typeof value == 'boolean'
268 || value === 'null'
269 || value == undefined
270 ) {
271 // Primitive Values
272 this._content = value;
273 } else if (value instanceof KeyValuePair) {
274 this._content = value;
275 } else if (Array.isArray(value)) {
276 this._content = value.map(this.refract);
277 } else if (typeof value === 'object') {
278 this._content = Object.keys(value).map(key => new this.MemberElement(key, value[key]));
279 } else {
280 throw new Error('Cannot set content to given value');
281 }
282 }
283
284 /**
285 * @type ObjectElement
286 */
287 get meta() {
288 if (!this._meta) {
289 if (this.isFrozen) {
290 const meta = new this.ObjectElement();
291 meta.freeze();
292 return meta;
293 }
294
295 this._meta = new this.ObjectElement();
296 }
297
298 return this._meta;
299 }
300
301 set meta(value) {
302 if (value instanceof this.ObjectElement) {
303 this._meta = value;
304 } else {
305 this.meta.set(value || {});
306 }
307 }
308
309 /**
310 * The attributes property defines attributes about the given instance
311 * of the element, as specified by the element property.
312 *
313 * @type ObjectElement
314 */
315 get attributes() {
316 if (!this._attributes) {
317 if (this.isFrozen) {
318 const meta = new this.ObjectElement();
319 meta.freeze();
320 return meta;
321 }
322
323 this._attributes = new this.ObjectElement();
324 }
325
326 return this._attributes;
327 }
328
329 set attributes(value) {
330 if (value instanceof this.ObjectElement) {
331 this._attributes = value;
332 } else {
333 this.attributes.set(value || {});
334 }
335 }
336
337 /**
338 * Unique Identifier, MUST be unique throughout an entire element tree.
339 * @type StringElement
340 */
341 get id() {
342 return this.getMetaProperty('id', '');
343 }
344
345 set id(element) {
346 this.setMetaProperty('id', element);
347 }
348
349 /**
350 * @type ArrayElement
351 */
352 get classes() {
353 return this.getMetaProperty('classes', []);
354 }
355
356 set classes(element) {
357 this.setMetaProperty('classes', element);
358 }
359
360 /**
361 * Human-readable title of element
362 * @type StringElement
363 */
364 get title() {
365 return this.getMetaProperty('title', '');
366 }
367
368 set title(element) {
369 this.setMetaProperty('title', element);
370 }
371
372 /**
373 * Human-readable description of element
374 * @type StringElement
375 */
376 get description() {
377 return this.getMetaProperty('description', '');
378 }
379
380 set description(element) {
381 this.setMetaProperty('description', element);
382 }
383
384 /**
385 * @type ArrayElement
386 */
387 get links() {
388 return this.getMetaProperty('links', []);
389 }
390
391 set links(element) {
392 this.setMetaProperty('links', element);
393 }
394
395 /**
396 * Returns whether the element is frozen.
397 * @type boolean
398 * @see freeze
399 */
400 get isFrozen() {
401 return Object.isFrozen(this);
402 }
403
404 /**
405 * Returns all of the parent elements.
406 * @type ArraySlice
407 */
408 get parents() {
409 let { parent } = this;
410 const parents = new ArraySlice();
411
412 while (parent) {
413 parents.push(parent);
414
415 // eslint-disable-next-line prefer-destructuring
416 parent = parent.parent;
417 }
418
419 return parents;
420 }
421
422 /**
423 * Returns all of the children elements found within the element.
424 * @type ArraySlice
425 * @see recursiveChildren
426 */
427 get children() {
428 if (Array.isArray(this.content)) {
429 return new ArraySlice(this.content);
430 }
431
432 if (this.content instanceof KeyValuePair) {
433 const children = new ArraySlice([this.content.key]);
434
435 if (this.content.value) {
436 children.push(this.content.value);
437 }
438
439 return children;
440 }
441
442 if (this.content instanceof Element) {
443 return new ArraySlice([this.content]);
444 }
445
446 return new ArraySlice();
447 }
448
449 /**
450 * Returns all of the children elements found within the element recursively.
451 * @type ArraySlice
452 * @see children
453 */
454 get recursiveChildren() {
455 const children = new ArraySlice();
456
457 this.children.forEach((element) => {
458 children.push(element);
459
460 element.recursiveChildren.forEach((child) => {
461 children.push(child);
462 });
463 });
464
465 return children;
466 }
467}
468
469module.exports = Element;
Note: See TracBrowser for help on using the repository browser.