@caweb/cli 1.11.0 → 1.11.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,992 @@
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import path from 'path';
5
+ import fs from 'fs';
6
+ import chalk from 'chalk';
7
+ import jsdom from 'jsdom';
8
+ import { parse } from 'node-html-parser';
9
+
10
+ /**
11
+ * Internal dependencies
12
+ */
13
+ import { appPath, projectPath, writeLine } from '../../lib/index.js';
14
+
15
+ import validatePage from './validation.js';
16
+
17
+ /**
18
+ * Constants used during the conversion process.
19
+ */
20
+ const { DIVI_VER } = JSON.parse( fs.readFileSync( path.join(projectPath, 'package.json') ) ).config;
21
+
22
+ /**
23
+ * Modules allow list
24
+ */
25
+ const allowedModules = {
26
+ 'et_pb_text': ['p', 'span', 'a', 'b'],
27
+ 'et_pb_heading': ['span', 'a', 'b']
28
+ }
29
+
30
+
31
+ /**
32
+ * Returns the limiter character based on the type.
33
+ *
34
+ * @param {string} type Choices are 'angle', 'bracket' or 'none. Default is 'angle'.
35
+ * @returns {object}
36
+ */
37
+ function getLimiter( type ) {
38
+ let limiter = {
39
+ open: '<',
40
+ close: '>'
41
+ }
42
+ if ('bracket' === type ) {
43
+ limiter.open = '[';
44
+ limiter.close = ']';
45
+ }else if ('none' === type ) {
46
+ limiter.open = '';
47
+ limiter.close = '';
48
+ }
49
+
50
+ return limiter;
51
+ }
52
+
53
+ /**
54
+ * This function is used to sanitize the JSON data.
55
+ *
56
+ * @param {object} json The JSON data to sanitize.
57
+ *
58
+ * @returns {object} The sanitized JSON data.
59
+ */
60
+ function sanitizeJson(json){
61
+ for( const j in json ) {
62
+ let obj = json[j];
63
+
64
+ // we want to iterate over the content of the json object
65
+ // if the value is content
66
+ // if the content is an object, we want to recursively sanitize it
67
+ if( obj.childNodes ) {
68
+ sanitizeJson( obj.childNodes );
69
+ obj.childNodes = obj.childNodes.filter( c => c );
70
+ }
71
+
72
+ // blank tag names indicate text nodes
73
+ if( '' === obj.rawTagName ) {
74
+ // blank lines are not needed
75
+ if( obj['_rawText'] && '' === obj['_rawText'].trim() ){
76
+ delete json[j];
77
+ }else{
78
+ // this will remove all new lines and double spacing
79
+ // this might also remove formatting. Need to test more
80
+ json[j]['_rawText'] = obj['_rawText'].replace(/([\n\r]|\s{2,})/g, '')
81
+ }
82
+ }
83
+
84
+
85
+ }
86
+
87
+ return json;
88
+ }
89
+
90
+ /**
91
+ * Converts the string attributes to a json object. Used when parsing attributes from the html
92
+ *
93
+ * @param {string} rawAttrs Raw attribute string to convert.
94
+ * @returns {object} The converted attributes.
95
+ */
96
+ function convertRawAttrs( rawAttrs ) {
97
+ if( ! rawAttrs || 'string' !== typeof rawAttrs ) {
98
+ return {};
99
+ }
100
+
101
+ let attrs = {};
102
+
103
+ // attributes follow the format of key="value" key="value"
104
+ // we want to split the string by "space
105
+ let attrArray = rawAttrs.split( '" ' );
106
+
107
+ attrArray.forEach( (a) => {
108
+ let key = a.split( '=' )[0].trim();
109
+ let value = a.split( '=' )[1].replace(/"/g, '').trim();
110
+
111
+ if( attrs[key] ) {
112
+ attrs[key] += ` ${value}`;
113
+ }else{
114
+ attrs[key] = value;
115
+ }
116
+ });
117
+
118
+ return attrs;
119
+
120
+ }
121
+
122
+ /**
123
+ * Converts attributes from json object to a string. Used when generating shortcodes.
124
+ *
125
+ * @param {*} attributes
126
+ * @returns
127
+ */
128
+ function getAttrString(attributes, limiter) {
129
+ let attrString = '';
130
+ for( let key in attributes ) {
131
+ if( attributes[key] ) {
132
+ let value = 'object' === typeof attributes[key] ?
133
+ JSON.stringify( attributes[key] ): attributes[key].trim();
134
+
135
+ if( 'bracket' === limiter ) {
136
+ // divi uses shortcode with different attributes
137
+ switch( key ) {
138
+ case 'class':
139
+ key = 'module_class';
140
+ break;
141
+ case 'id':
142
+ key = 'module_id';
143
+ break;
144
+ case 'style':
145
+ key = [];
146
+
147
+ value.split(';').forEach( (v) => {
148
+ let k = v.split(':')[0].trim();
149
+ let s = v.split(':')[1].trim();
150
+
151
+ // style mappings
152
+ switch( k ) {
153
+ case 'background-color':
154
+ k = 'background_color';
155
+ break;
156
+ case 'background-image':
157
+ k = 'background_image';
158
+
159
+ let gradient = s.replace(/.*[,\s]*linear-gradient\((.*?)\).*/g, '$1').replace(/["']/g, '');
160
+
161
+ if( gradient ) {
162
+ // if the gradient is present we want to add the appropriate values
163
+ let gradientValues = gradient.split(',');
164
+
165
+ key.push(`background_color_gradient_direction="${gradientValues[0].trim()}"`);
166
+ key.push(`background_color_gradient_stops="${gradientValues.slice(1).join('|')}"`);
167
+ key.push('use_background_color_gradient="on"');
168
+ }
169
+
170
+ s = s.replace(/url\((.*?)\).*/g, '$1').replace(/["']/g, '');
171
+ break;
172
+
173
+ case 'background-repeat':
174
+ k = 'background_repeat';
175
+ s = s.replace(/(.*?)(repeat|no-repeat)/g, '$2');
176
+ break;
177
+
178
+ case 'background-position':
179
+ k = 'background_position';
180
+ /**
181
+ * position can be 4 different syntaxes so lets split the values
182
+ * @link https://developer.mozilla.org/en-US/docs/Web/CSS/background-position
183
+ */
184
+ let values = s.split(' ');
185
+ // for whatever reason Divi has these values inverted
186
+
187
+ switch( values.length ) {
188
+ case 1:
189
+ s = values[0];
190
+ break;
191
+ case 2:
192
+ s = `${values[1]}_${values[0]}`;
193
+ break;
194
+ case 3:
195
+ case 4:
196
+ s = `${values[2]}_${values[0]}`;
197
+ break;
198
+ }
199
+ break;
200
+
201
+ case 'background-size':
202
+ k = 'background_size';
203
+
204
+ break;
205
+ default:
206
+ k = '';
207
+ break;
208
+ }
209
+
210
+ if( k ) {
211
+ key.push(`${k}="${s}"`);
212
+ }
213
+ });
214
+ break;
215
+
216
+ }
217
+ }
218
+
219
+ if( '' !== key && 'string' === typeof key ) {
220
+ // we have to encode certain characters
221
+ value = value.replaceAll('"', '%22');
222
+
223
+ attrString += ` ${key}="${value}"`;
224
+ }else if( 'object' === typeof key ) {
225
+ attrString += ` ${key.join(' ')}`;
226
+ }
227
+ }
228
+ }
229
+
230
+ return attrString;
231
+ }
232
+
233
+ /**
234
+ * Generates an html element string.
235
+ *
236
+ * @param {string} tag
237
+ * @param {object} opts
238
+ * @param {object} opts.attrs The attributes to add to the element.
239
+ * @param {string} opts.content The content to add to the element.
240
+ * @param {string} opts.limiter The type of limiter to use. Choices are 'angle' or 'bracket'.
241
+ * @param {boolean} opts.isUnclosed True if the element is unclosed. Defaults to false.
242
+ * @param {boolean} opts.isSelfClosing True if the element is self closing. Defaults to false.
243
+ * @param {boolean} opts.closing True if the element is closing. Defaults to false.
244
+ * @returns {string}
245
+ */
246
+ function addElement( tag, opts = {} ) {
247
+ let defaultOpts = {
248
+ attrs : {},
249
+ content: '',
250
+ limiter: 'angle',
251
+ isUnclosed: false,
252
+ isSelfClosing: false,
253
+ closing: false
254
+ }
255
+
256
+ let { attrs, content, limiter, isUnclosed, isSelfClosing } = {...defaultOpts, ...opts};
257
+
258
+
259
+ let lmtrObj = getLimiter( limiter );
260
+
261
+ // generate attribute string
262
+ let attrString = getAttrString( attrs, limiter );
263
+
264
+ // if the tag is empty, we want to return the content
265
+ if( ! tag ) {
266
+ return content;
267
+ }
268
+
269
+ let output = `${lmtrObj.open}${tag}${attrString}${tag && isSelfClosing ? ' /' : ''}${lmtrObj.close}${content.trim()}`
270
+
271
+ if( ! isUnclosed && ! isSelfClosing && tag) {
272
+ output += closeElement(tag, lmtrObj );
273
+ }
274
+
275
+ return output;
276
+ }
277
+
278
+ /**
279
+ * Closes an element.
280
+ *
281
+ * @param {string} tag The tag to close.
282
+ * @param {object} limiter The type of limiter character.
283
+ */
284
+ function closeElement( tag, limiter ) {
285
+ let lmtrObj = 'string' === typeof limiter ? getLimiter( limiter ) : limiter;
286
+ return `${lmtrObj.open}/${tag}${lmtrObj.close}`
287
+ }
288
+
289
+
290
+ /**
291
+ * Generates shortcodes from the json object.
292
+ * Converts a json dom object into shortcodes.
293
+ *
294
+ *
295
+ * @param {array[object]} mainContent Array of json objects to convert.
296
+ * @param {object} opts Json object of configurations used during the conversion process.
297
+ * @param {string} opts.limiter The type of limiter to use. Choices are 'angle' or 'bracket'.
298
+ * @param {boolean} opts.openElement Current opened element. Defaults to false.
299
+ * @param {boolean} opts.inFullwidth True if the element is in a fullwidth section. Defaults to false.
300
+ * @param {boolean} opts.inSection True if the element is in a section. Defaults to false.
301
+ * @param {boolean} opts.inRow True if the element is in a row. Defaults to false.
302
+ * @param {boolean} opts.inColumn True if the element is in a column. Defaults to false.
303
+ * @param {number} opts.columnCount The number of columns in the row. Defaults to 0.
304
+ * @returns
305
+ */
306
+ function generateShortcodes( mainContent, opts = {
307
+ openElement: false,
308
+ limiter: 'bracket',
309
+ inFullwidth: false,
310
+ inSection: false,
311
+ inRow: false,
312
+ inColumn: false,
313
+ columnCount: 0,
314
+ }) {
315
+ const setFullwidthTag = ( value, fullwidth ) => {
316
+ return fullwidth ? value.replace('et_pb_', 'et_pb_fullwidth_') : value;
317
+ }
318
+
319
+ let output = '';
320
+
321
+ for( const element in mainContent ) {
322
+ let htmlElement = mainContent[element];
323
+
324
+ let {
325
+ rawTagName: type,
326
+ rawAttrs,
327
+ childNodes: content,
328
+ classList,
329
+ } = htmlElement;
330
+
331
+ // classList returns a DOMTokenList, we need to convert it to an array
332
+ let classes = classList ? [...classList.values()] : [];
333
+
334
+ // we convert the raw attributes to a json object
335
+ let attrs = convertRawAttrs( rawAttrs );
336
+
337
+ /**
338
+ * We dont want to convert empty elements
339
+ */
340
+ if( ! content.length ){
341
+ // image tags are self closing and don't have content this is ok.
342
+ if( 'img' === type ) {
343
+ // do nothing this is ok.
344
+ // no length no type blank spaces
345
+ } else if( '' !== type ){
346
+ continue;
347
+ }
348
+ }
349
+
350
+ /**
351
+ * Important:
352
+ * If there is an open module and the current type isn't allowed in the module we must close the open module first
353
+ */
354
+ if( type && opts.openElement ) {
355
+ // if openElement has restrictions
356
+ if(
357
+ allowedModules[opts.openElement] &&
358
+ ! allowedModules[opts.openElement].includes(type)
359
+ ) {
360
+ output += closeElement(opts.openElement, opts.limiter );
361
+ opts.openElement = false;
362
+ }
363
+ }
364
+
365
+ // element type rendering
366
+ switch( type ) {
367
+ case 'div':
368
+ // .container-fluid primarily used for fullwidth sections
369
+ if( classes.includes('container-fluid') && ! opts.inFullwidth) {
370
+ opts.inFullwidth = true;
371
+
372
+ let hasContainers = content.filter((r,i) => {
373
+ if(r.attributes.class.match(/container/g)){
374
+ // we add the raw attributes to the containers
375
+ // this allows the sections to use the fluid attributes
376
+ // we do remove the container-fluid class
377
+ r.rawAttrs = r.rawAttrs + ` ${rawAttrs.replace('container-fluid', '')}`;
378
+ return true;
379
+ }
380
+ }).length
381
+
382
+ if( ! hasContainers ) {
383
+ output += addElement(
384
+ 'et_pb_fullwidth_section',
385
+ {
386
+ limiter:opts.limiter,
387
+ content: generateShortcodes(content, opts)
388
+ }
389
+ );
390
+ }else{
391
+ opts.inFullwidth = false;
392
+
393
+ output += addElement(
394
+ '',
395
+ {
396
+ content: generateShortcodes(content, opts)
397
+ }
398
+ );
399
+
400
+ }
401
+
402
+
403
+ opts.inFullwidth = false;
404
+
405
+ // .container is used primarily for regular sections
406
+ }else if( classes.includes('container') && ! opts.inSection ) {
407
+ opts.inSection = true;
408
+
409
+ // sections don't need the container class
410
+ // we remove the container class from the attributes
411
+ attrs.class = attrs.class.replace('container', '');
412
+
413
+ output += addElement(
414
+ 'et_pb_section',
415
+ {
416
+ limiter:opts.limiter,
417
+ content: generateShortcodes(content, opts),
418
+ attrs: {
419
+ _builder_version: DIVI_VER,
420
+ ...attrs,
421
+ }
422
+ }
423
+ );
424
+
425
+ opts.inSection = false;
426
+
427
+ // .row is used for rows
428
+ }else if( classes.includes('row') && ! opts.inRow ) {
429
+ opts.inRow = true;
430
+ // if the div has a class of row, we want to get the total number of columns
431
+ opts.columnCount = content.filter((r,i) => r.attributes?.class?.match(/(col)[-\w\d]*/g) ).length;
432
+
433
+ // sections don't need the row class
434
+ // we remove the row class from the attributes
435
+ attrs.class = attrs.class.replace('row', '');
436
+
437
+ output += addElement(
438
+ 'et_pb_row',
439
+ {
440
+ limiter: opts.limiter,
441
+ content: generateShortcodes(content, opts)
442
+ }
443
+ );
444
+
445
+ // reset the column count
446
+ opts.columnCount = 0;
447
+ opts.inRow = false;
448
+
449
+ // columns
450
+ }else if( classes.filter(c => c.match( /(col)[-\w\d]*/g )).length && ! opts.inColumn ) {
451
+ opts.inColumn = true;
452
+
453
+ /**
454
+ * if the div has multiple column classes only the first one after being sorted is used
455
+ *
456
+ * Example:
457
+ * <div class="col-md-4 col-lg-3 col"></div>
458
+ *
459
+ * Sorted:
460
+ * - col
461
+ * - col-lg-3
462
+ * - col-md-4
463
+ *
464
+ * Just col would be used
465
+ */
466
+ let colClass = classes.filter(c => c.match( /(col)[-\w\d]*/g )).sort()[0];
467
+ let colSize = Number(colClass.split( '-' ).pop());
468
+
469
+ // if the colSize is not a number, we want to set it to 12,
470
+ // 12 is the max number for bootstrap columns
471
+ colSize = ! isNaN(Number( colSize )) ? colSize : 12 / opts.columnCount;
472
+
473
+ let colType = '';
474
+ // calculate the column type
475
+ switch( colSize ) {
476
+ // 1 column
477
+ case 12:
478
+ colType = '4_4';
479
+ break;
480
+ // 2 columns
481
+ case 6:
482
+ colType = '1_2';
483
+ break;
484
+ // 3 columns
485
+ case 4:
486
+ colType = '1_3';
487
+ break;
488
+ }
489
+
490
+ // columns don't need the col class
491
+ // we remove the col class from the attributes
492
+ attrs.class = attrs.class.replace(/(col)[-\w\d]*/g, '');
493
+
494
+
495
+ // if the div has a class of col, we want to convert it to a shortcode
496
+ opts.openElement = 'et_pb_column';
497
+
498
+ // we don't close the column here
499
+ output += addElement(
500
+ opts.openElement,
501
+ {
502
+ limiter: opts.limiter,
503
+ isUnclosed: true,
504
+ attrs: {
505
+ ...attrs,
506
+ type: colType
507
+ },
508
+ content: generateShortcodes(content, opts)
509
+ }
510
+ );
511
+
512
+ // we have to close any open elements before closing the column
513
+ if( opts.openElement && 'et_pb_column' !== opts.openElement ) {
514
+ output += closeElement(opts.openElement, opts.limiter );
515
+ opts.openElement = false;
516
+ }
517
+
518
+ // now we can close the column
519
+ output += closeElement('et_pb_column', opts.limiter );
520
+
521
+ opts.inColumn = false;
522
+ }else{
523
+ // fullwidth sections only allow specific elements
524
+ // divs are not allowed
525
+ if( opts.inFullwidth ) {
526
+ // output += addElement('', { limiter: 'none', content: generateShortcodes(content) });
527
+ }else{
528
+ if( classes.includes('card') ){
529
+ // this is Divi blurb module
530
+ if( classes.includes('blurb') ) {
531
+ output += generateModuleShortcode('blurb', htmlElement );
532
+ }else{
533
+ // this is our card module
534
+ output += generateModuleShortcode('ca_card', htmlElement );
535
+ }
536
+
537
+ }else{
538
+ // figure out what kind of div element we are dealing with
539
+ output += addElement(
540
+ type,
541
+ {
542
+ limiter: opts.limiter,
543
+ content: generateShortcodes(content, opts)
544
+ }
545
+ );
546
+
547
+ }
548
+ }
549
+ }
550
+
551
+ break;
552
+
553
+ // code module
554
+ case 'code':
555
+ output += addElement(setFullwidthTag('et_pb_code', opts.inFullwidth), {attrs, limiter: 'bracket', content: generateShortcodes(content)} );
556
+ break;
557
+ // header modules
558
+ case 'h1':
559
+ case 'h2':
560
+ case 'h3':
561
+ case 'h4':
562
+ case 'h5':
563
+ case 'h6':
564
+ // we let the h1-h6 process know element is opened
565
+ // this allows other elements to be added to the header modules
566
+ // opts.openElement = 'et_pb_heading';
567
+
568
+
569
+ output += generateModuleShortcode('heading', htmlElement);
570
+ // opts.openElement = false;
571
+ break;
572
+
573
+ case 'img':
574
+ output += addElement(
575
+ 'et_pb_image',
576
+ {
577
+ limiter: opts.limiter,
578
+ attrs,
579
+ isSelfClosing: true
580
+ }
581
+ );
582
+
583
+ break;
584
+ // all theses elements can go in a text module
585
+ case 'a':
586
+ case 'b':
587
+ case 'p':
588
+ case 'span':
589
+
590
+ if( opts.inFullwidth ) {
591
+ // figure out what to do with these elements if in a fullwidth section
592
+
593
+ // these elements get added to a text module
594
+ }else{
595
+ // if not already opened
596
+ // or the last opened element is a column
597
+ if( ! opts.openElement || 'et_pb_column' === opts.openElement ) {
598
+ opts.openElement = 'et_pb_text';
599
+
600
+ // this allows other elements to be added to the text modules
601
+ // render the text module by adding and element to an element
602
+ output += addElement(
603
+ opts.openElement,
604
+ {
605
+ limiter: opts.limiter,
606
+ isUnclosed: true,
607
+ content: addElement(
608
+ type,
609
+ {
610
+ attrs,
611
+ content: generateShortcodes(content, opts)
612
+ }
613
+ ),
614
+ }
615
+ );
616
+
617
+
618
+ // a text module has already been opened so we just add to it
619
+ }else {
620
+ output += addElement(
621
+ type, {
622
+ attrs,
623
+ content: generateShortcodes(content, opts)
624
+ }
625
+ );
626
+ }
627
+ }
628
+
629
+ break;
630
+ // default is a string element
631
+ default:
632
+ // console.log( htmlElement)
633
+ output += htmlElement;
634
+ break;
635
+
636
+ }
637
+
638
+
639
+ }
640
+
641
+ return output;
642
+ }
643
+
644
+ function generateModuleShortcode(module, element ){
645
+ let content = '';
646
+ let attrs = {};
647
+ let moduleName = 'et_pb_' + module;
648
+
649
+ switch( module ) {
650
+ case 'blurb': {
651
+ /**
652
+ * if blurb module is requested
653
+ * we try and make the shortcode as if the element
654
+ * was made with a Card Component
655
+ */
656
+
657
+ let header = element.querySelector('.card-header');
658
+ let title = element.querySelector('.card-title');
659
+ let img = element.children.filter( c => c.tagName.toLowerCase() === 'img' );
660
+
661
+ content = element.querySelector('.card-body');
662
+
663
+ // the link_option_url is determined by either the
664
+ // .card-header options link or the .card-title data-url
665
+ // .card-header will take precedence over the .card-title
666
+ if( header && header.querySelector('.options a') ) {
667
+ let link = header.querySelector('.options a');
668
+ attrs.link_option_url = link.getAttribute('href');
669
+ }
670
+
671
+ // if the element has a .card-title, we want to add it to the attributes
672
+ if( title ) {
673
+ attrs.title = title.text.trim();
674
+
675
+ // if the link_option_url is not set, we want to set it to the title's data-url
676
+ if( ! attrs.link_option_url && title.getAttribute('data-url') ) {
677
+ attrs.link_option_url = title.getAttribute('data-url');
678
+ }
679
+
680
+ // text orientation
681
+ attrs.text_orientation = 'left';
682
+
683
+ if( title.classList.contains('text-center') ){
684
+ attrs.text_orientation = 'center';
685
+ }else if( title.classList.contains('text-right') ){
686
+ attrs.text_orientation = 'right';
687
+ }
688
+
689
+ // remove the .card-title from the content
690
+ title.remove();
691
+ }
692
+
693
+ // if the element has an image, we want to add it to the attributes
694
+ if( img ){
695
+ attrs.image = img[0].getAttribute('src')
696
+ }
697
+
698
+ // if the element has a .card-body
699
+ if( content ){
700
+ // if the content has a class of text-light,
701
+ if(content.classList.contains('text-light')){
702
+ attrs.background_color = 'light';
703
+
704
+ // background_layout opposite background_color
705
+ attrs.background_layout = 'dark';
706
+ }else{
707
+
708
+ attrs.background_color = 'dark';
709
+
710
+ // background_layout opposite background_color
711
+ attrs.background_layout = 'light';
712
+ }
713
+
714
+ content = content.innerHTML.trim();
715
+ }
716
+
717
+ // remove the card layout class from class list
718
+ element.classList.remove('card');
719
+ element.classList.remove('blurb');
720
+ element.classList.remove('bg-transparent');
721
+ for( const c of element.classList.values() ) {
722
+ if( c.startsWith('card-') ) {
723
+ element.classList.remove(c);
724
+ }
725
+ }
726
+ break;
727
+ }
728
+ case 'ca_card': {
729
+ /**
730
+ * if card module is requested
731
+ */
732
+ let header = element.querySelector('.card-header');
733
+ let title = element.querySelector('.card-title');
734
+ let img = element.children.filter( c => c.tagName.toLowerCase() === 'img' );
735
+ let layout = element.classList.toString().match(/card-(\w+)/g)[0].replace('card-', '');
736
+
737
+ content = element.querySelector('.card-body');
738
+
739
+ // card layout
740
+ attrs.card_layout = layout || 'default';
741
+
742
+ // .card-header
743
+ if( header ) {
744
+ // the link_option_url is determined by either the
745
+ // .card-header options link or the .card-title data-url
746
+ if( header.querySelector('.options a') ){
747
+ let link = header.querySelector('.options a');
748
+
749
+ attrs.show_button = 'on';
750
+ attrs.button_link = link.getAttribute('href');
751
+ }
752
+
753
+ attrs.include_header = 'on';
754
+ attrs.title = header.text.trim();
755
+
756
+ }
757
+
758
+ // if the element has a .card-title
759
+ if( title ) {
760
+
761
+ // if the link_option_url is not set, we want to set it to the title's data-url
762
+ if( ! attrs.link_option_url && title.getAttribute('data-url') ) {
763
+
764
+ attrs.show_button = 'on';
765
+ attrs.button_link = link.getAttribute('href');
766
+ }
767
+ }
768
+
769
+ // if the element has an image, we want to add it to the attributes
770
+ if( img ){
771
+ attrs.show_image = 'on';
772
+ attrs.featured_image = img[0].getAttribute('src');
773
+ }
774
+
775
+ // if the element has a .card-body
776
+ if( content ){
777
+ // if the content has a class of text-light,
778
+ if(content.classList.contains('text-light')){
779
+ attrs.background_color = 'light';
780
+
781
+ // background_layout opposite background_color
782
+ attrs.background_layout = 'dark';
783
+ }else{
784
+
785
+ attrs.background_color = 'dark';
786
+
787
+ // background_layout opposite background_color
788
+ attrs.background_layout = 'light';
789
+ }
790
+
791
+ content = content.innerHTML.trim();
792
+ }
793
+
794
+ // remove the card layout class from class list
795
+ element.classList.remove('card');
796
+ for( const c of element.classList.values() ) {
797
+ if( c.startsWith('card-') ) {
798
+ element.classList.remove(c);
799
+ }
800
+ }
801
+ break;
802
+ }
803
+ case 'heading': {
804
+ let title_font = [];
805
+ let title_text_color = '';
806
+
807
+ attrs = {
808
+ title: element.innerHTML.trim(),
809
+ title_level: element.tagName.toLowerCase()
810
+ };
811
+
812
+ for( const c of element.classList.values() ) {
813
+ // font weight classes
814
+ if( c.startsWith('fw-') ) {
815
+ let fontWeight = {
816
+ lighter: '200',
817
+ light: '300',
818
+ normal: '400',
819
+ medium: '500',
820
+ semibold: '600',
821
+ bold: '700',
822
+ extrabold: '800',
823
+ }
824
+
825
+
826
+ let fontWeightValue = c.replace('fw-', '');
827
+
828
+ title_font[1] = fontWeight[fontWeightValue] || fontWeight.normal;
829
+
830
+ // remove the class from the class list
831
+ element.classList.remove(c);
832
+ // text classes
833
+ }else if( c.startsWith('text-') ) {
834
+ let fontColors = {
835
+ 'light': 'ededef',
836
+ 'dark': '3b3a48',
837
+ 'white': 'fff',
838
+ 'black': '000',
839
+ }
840
+
841
+ let fontColorValue = c.replace('text-', '');
842
+
843
+ if( fontColors[fontColorValue] ) {
844
+ title_text_color = `#${fontColors[fontColorValue]}`;
845
+ }
846
+
847
+
848
+ // remove the class from the class list
849
+ element.classList.remove(c);
850
+ }
851
+ }
852
+
853
+ if( title_font.length ) {
854
+ attrs.title_font = title_font.join('|');
855
+ }
856
+
857
+ if( title_text_color ) {
858
+ attrs.title_text_color = title_text_color;
859
+ }
860
+
861
+ content = element.innerHTML.trim()
862
+
863
+ break;
864
+ }
865
+ }
866
+
867
+ if( element.classList.length ){
868
+ attrs.class = [...element.classList.values()].join(' ').trim();
869
+ }
870
+
871
+ // return the module shortcode
872
+ return addElement(
873
+ moduleName,
874
+ {
875
+ limiter: 'bracket',
876
+ attrs: {
877
+ _builder_version: DIVI_VER,
878
+ ...attrs
879
+ },
880
+ content
881
+ }
882
+ );
883
+ }
884
+
885
+ /**
886
+ * Attempts to convert a site.
887
+ *
888
+ * @param {Object} options
889
+ * @param {Object} options.spinner A CLI spinner which indicates progress.
890
+ * @param {boolean} options.debug True if debug mode is enabled.
891
+ * @param {boolean} options.builder Editor style to use for the pages. Choices are 'plain', 'divi', or 'gutenberg'.
892
+ */
893
+ export default async function convertSite({
894
+ spinner,
895
+ debug} ) {
896
+ spinner.stop();
897
+
898
+ let buildPath = path.join( appPath, 'build' );
899
+ let siteData = fs.existsSync( appPath, 'caweb.json' ) ? JSON.parse( fs.readFileSync( path.join(appPath, 'caweb.json') ) ) : {};
900
+ let pages = [];
901
+
902
+ /**
903
+ * Return all .html files in the build directory
904
+ *
905
+ * exclusions:
906
+ * - serp.html - This a search engine results page, we don't need to parse this
907
+ */
908
+ let sitePages = fs.readdirSync( buildPath, { recursive: true } ).filter( file => {
909
+ return 'serp.html' !== file && file.endsWith( '.html' )
910
+ } );
911
+
912
+ for( const file of sitePages ) {
913
+ // get the file path
914
+ let filePath = path.join( buildPath, file );
915
+
916
+ // We use jsdom to emulate a browser environment and get the window.document
917
+ let fileMarkupJSon = await jsdom.JSDOM.fromFile( filePath );
918
+ let { document } = fileMarkupJSon.window;
919
+
920
+ validatePage( document );
921
+
922
+ // all we need is the document #page-container #main-content
923
+ let mainContent = document.querySelector( '#page-container #main-content' );
924
+
925
+ // if the default #page-container #main-content exists
926
+ if( mainContent ){
927
+ // use html-to-json-parser to convert the page container into json
928
+ // let mainContentJson = await HTMLToJSON( mainContent.outerHTML, false );
929
+ let mainContentJson = parse( mainContent.outerHTML );
930
+
931
+ // sanitize the json
932
+ mainContentJson = sanitizeJson( mainContentJson.childNodes )[0];
933
+
934
+ /**
935
+ * Main content is allowed 2 elements
936
+ * - main - renders the main content
937
+ * - aside - renders the sidebar content
938
+ */
939
+ mainContent = mainContentJson.childNodes.filter( e => 'main' === e.rawTagName )[0];
940
+
941
+ // if main content was found
942
+ if( mainContent && mainContent.childNodes ) {
943
+ let shortcodeContent = '';
944
+ // loop through the main content and convert it to shortcodes
945
+ // main-content should only have 1 element, div
946
+ // .container for regular sections
947
+ // .container-fluid for fullwidth sections
948
+ for( const e in mainContent.childNodes ) {
949
+ shortcodeContent += generateShortcodes( [mainContent.childNodes[e]] );
950
+ }
951
+
952
+ // site info
953
+ let slug = file.replace( '.html', '' );
954
+ let title = slug.replace(/[-_]/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
955
+
956
+ pages.push({
957
+ title,
958
+ slug,
959
+ shortcodeContent
960
+ });
961
+
962
+ }
963
+
964
+
965
+ }
966
+
967
+ }
968
+
969
+ // add sync entry and pages to siteData
970
+ siteData = {
971
+ ...siteData,
972
+ sync:{
973
+ ...siteData.sync || {},
974
+ static:{
975
+ user: 'static',
976
+ pwd: 'static',
977
+ url: 'static'
978
+ }
979
+ },
980
+ media: path.join( appPath, 'build', 'media' ),
981
+ pages,
982
+ }
983
+
984
+ // write the site data to the file
985
+ fs.writeFileSync(
986
+ path.join( appPath, 'caweb.json' ),
987
+ JSON.stringify( siteData, null, 4 )
988
+ );
989
+
990
+ writeLine( chalk.green( 'Site converted successfully.' ) );
991
+
992
+ };