@caweb/cli 1.10.12 → 1.11.1

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,789 @@
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import path from 'path';
5
+ import fs from 'fs';
6
+ import { confirm } from '@inquirer/prompts';
7
+ import chalk from 'chalk';
8
+ import { HTMLToJSON } from 'html-to-json-parser';
9
+ import jsdom from 'jsdom';
10
+ import { parse } from 'node-html-parser';
11
+
12
+ /**
13
+ * Internal dependencies
14
+ */
15
+ import { appPath, writeLine } from '../../lib/index.js';
16
+
17
+ import {
18
+ promptForGeneralInfo,
19
+ promptForSocial,
20
+ promptForGoogleOptions,
21
+ promptForAlerts,
22
+ promptForHeader,
23
+ promptForFooter
24
+ } from './prompts.js';
25
+
26
+ /**
27
+ * Constants used during the conversion process.
28
+ */
29
+ /**
30
+ * Modules allow list
31
+ */
32
+ const allowedModules = {
33
+ 'et_pb_text': ['p', 'span', 'a', 'b'],
34
+ 'et_pb_heading': ['span', 'a', 'b']
35
+ }
36
+
37
+
38
+ /**
39
+ * Returns the limiter character based on the type.
40
+ *
41
+ * @param {string} type Choices are 'angle', 'bracket' or 'none. Default is 'angle'.
42
+ * @returns {object}
43
+ */
44
+ function getLimiter( type ) {
45
+ let limiter = {
46
+ open: '<',
47
+ close: '>'
48
+ }
49
+ if ('bracket' === type ) {
50
+ limiter.open = '[';
51
+ limiter.close = ']';
52
+ }else if ('none' === type ) {
53
+ limiter.open = '';
54
+ limiter.close = '';
55
+ }
56
+
57
+ return limiter;
58
+ }
59
+
60
+ function findElementByClass(node, search ) {
61
+ let found = false;
62
+
63
+ node.forEach( n => {
64
+ let classes = n.classList ? [...n.classList.values()] : [];
65
+
66
+ if( classes.includes(search) ) {
67
+ // we found the element we are looking for
68
+ // we want to return the element
69
+ found = n.childNodes[0].toString();
70
+ }else{
71
+ found = findElementByClass(n.childNodes, search);
72
+ }
73
+ });
74
+
75
+ return found;
76
+ }
77
+
78
+ function findSiblingElement(node, search) {
79
+ let found = false;
80
+ node.forEach( n => {
81
+ if( search === n.rawTagName ) {
82
+ // we found the element we are looking for
83
+ // we want to return the element
84
+ found = n;
85
+ }
86
+ });
87
+
88
+ return found;
89
+ }
90
+
91
+ /**
92
+ * This function is used to sanitize the JSON data.
93
+ *
94
+ * @param {object} json The JSON data to sanitize.
95
+ *
96
+ * @returns {object} The sanitized JSON data.
97
+ */
98
+ function sanitizeJson(json){
99
+ for( const j in json ) {
100
+ let obj = json[j];
101
+
102
+ // we want to iterate over the content of the json object
103
+ // if the value is content
104
+ // if the content is an object, we want to recursively sanitize it
105
+ if( obj.childNodes ) {
106
+ sanitizeJson( obj.childNodes );
107
+ obj.childNodes = obj.childNodes.filter( c => c );
108
+ }
109
+
110
+ // blank tag names indicate text nodes
111
+ if( '' === obj.rawTagName ) {
112
+ // blank lines are not needed
113
+ if( obj['_rawText'] && '' === obj['_rawText'].trim() ){
114
+ delete json[j];
115
+ }else{
116
+ // this will remove all new lines and double spacing
117
+ // this might also remove formatting. Need to test more
118
+ json[j]['_rawText'] = obj['_rawText'].replace(/([\n\r]|\s{2,})/g, '')
119
+ }
120
+ }
121
+
122
+
123
+ }
124
+
125
+ return json;
126
+ }
127
+
128
+ /**
129
+ * Converts the string attributes to a json object. Used when parsing attributes from the html
130
+ *
131
+ * @param {string} rawAttrs Raw attribute string to convert.
132
+ * @returns {object} The converted attributes.
133
+ */
134
+ function convertRawAttrs( rawAttrs ) {
135
+ if( ! rawAttrs || 'string' !== typeof rawAttrs ) {
136
+ return {};
137
+ }
138
+
139
+ let attrs = {};
140
+
141
+ // attributes follow the format of key="value" key="value"
142
+ // we want to split the string by "space
143
+ let attrArray = rawAttrs.split( '" ' );
144
+
145
+ attrArray.forEach( (a) => {
146
+ let key = a.split( '=' )[0].trim();
147
+ let value = a.split( '=' )[1].replace(/"/g, '').trim();
148
+
149
+ if( attrs[key] ) {
150
+ attrs[key] += ` ${value}`;
151
+ }else{
152
+ attrs[key] = value;
153
+ }
154
+ });
155
+
156
+ return attrs;
157
+
158
+ }
159
+
160
+ /**
161
+ * Converts attributes from json object to a string. Used when generating shortcodes.
162
+ *
163
+ * @param {*} attributes
164
+ * @returns
165
+ */
166
+ function getAttrString(attributes, limiter) {
167
+ let attrString = '';
168
+ for( let key in attributes ) {
169
+ if( attributes[key] ) {
170
+ let value = 'object' === typeof attributes[key] ?
171
+ JSON.stringify( attributes[key] ): attributes[key].trim();
172
+
173
+ if( 'bracket' === limiter ) {
174
+ // divi uses shortcode with different attributes
175
+ switch( key ) {
176
+ case 'class':
177
+ key = 'module_class';
178
+ break;
179
+ case 'id':
180
+ key = 'module_id';
181
+ break;
182
+ case 'style':
183
+ key = [];
184
+
185
+ value.split(';').forEach( (v) => {
186
+ let k = v.split(':')[0].trim();
187
+ let s = v.split(':')[1].trim();
188
+
189
+ // style mappings
190
+ switch( k ) {
191
+ case 'background-image':
192
+ k = 'background_image';
193
+ s = s.replace(/url\((.*?)\).*/g, '$1').replace(/["']/g, '');
194
+ break;
195
+
196
+ case 'background-repeat':
197
+ k = 'background_position';
198
+ s = s.replace(/(.*?)(repeat|no-repeat)/g, '$2');
199
+ break;
200
+ default:
201
+ k = '';
202
+ break;
203
+ }
204
+
205
+ if( k ) {
206
+ key.push(`${k}="${s}"`);
207
+ }
208
+ });
209
+ break;
210
+
211
+ }
212
+ }
213
+
214
+ if( '' !== key && 'string' === typeof key ) {
215
+ attrString += ` ${key}="${value}"`;
216
+ }else if( 'object' === typeof key ) {
217
+ attrString += ` ${key.join(' ')}`;
218
+ }
219
+ }
220
+ }
221
+
222
+ return attrString;
223
+ }
224
+
225
+ /**
226
+ * Generates an html element string.
227
+ *
228
+ * @param {string} tag
229
+ * @param {object} opts
230
+ * @param {object} opts.attrs The attributes to add to the element.
231
+ * @param {string} opts.content The content to add to the element.
232
+ * @param {string} opts.limiter The type of limiter to use. Choices are 'angle' or 'bracket'.
233
+ * @param {boolean} opts.isUnclosed True if the element is unclosed. Defaults to false.
234
+ * @param {boolean} opts.isSelfClosing True if the element is self closing. Defaults to false.
235
+ * @param {boolean} opts.closing True if the element is closing. Defaults to false.
236
+ * @returns {string}
237
+ */
238
+ function addElement( tag, opts = {} ) {
239
+ let defaultOpts = {
240
+ attrs : {},
241
+ content: '',
242
+ limiter: 'angle',
243
+ isUnclosed: false,
244
+ isSelfClosing: false,
245
+ closing: false
246
+ }
247
+
248
+ let { attrs, content, limiter, isUnclosed, isSelfClosing } = {...defaultOpts, ...opts};
249
+
250
+
251
+ let lmtrObj = getLimiter( limiter );
252
+
253
+ // generate attribute string
254
+ let attrString = getAttrString( attrs, limiter );
255
+
256
+ // if the tag is empty, we want to return the content
257
+ if( ! tag ) {
258
+ return content;
259
+ }
260
+
261
+ let output = `${lmtrObj.open}${tag}${attrString}${tag && isSelfClosing ? ' /' : ''}${lmtrObj.close}${content.trim()}`
262
+
263
+ if( ! isUnclosed && ! isSelfClosing && tag) {
264
+ output += closeElement(tag, lmtrObj );
265
+ }
266
+
267
+ return output;
268
+ }
269
+
270
+ /**
271
+ * Closes an element.
272
+ *
273
+ * @param {string} tag The tag to close.
274
+ * @param {object} limiter The type of limiter character.
275
+ */
276
+ function closeElement( tag, limiter ) {
277
+ let lmtrObj = 'string' === typeof limiter ? getLimiter( limiter ) : limiter;
278
+ return `${lmtrObj.open}/${tag}${lmtrObj.close}`
279
+ }
280
+
281
+ /**
282
+ * Generates shortcodes from the json object.
283
+ * Converts a json dom object into shortcodes.
284
+ *
285
+ *
286
+ * @param {array[object]} mainContent Array of json objects to convert.
287
+ * @param {object} opts Json object of configurations used during the conversion process.
288
+ * @param {string} opts.limiter The type of limiter to use. Choices are 'angle' or 'bracket'.
289
+ * @param {boolean} opts.openElement Current opened element. Defaults to false.
290
+ * @param {boolean} opts.inFullwidth True if the element is in a fullwidth section. Defaults to false.
291
+ * @param {boolean} opts.inSection True if the element is in a section. Defaults to false.
292
+ * @param {boolean} opts.inRow True if the element is in a row. Defaults to false.
293
+ * @param {boolean} opts.inColumn True if the element is in a column. Defaults to false.
294
+ * @param {number} opts.columnCount The number of columns in the row. Defaults to 0.
295
+ * @returns
296
+ */
297
+ function generateShortcodes( mainContent, opts = {
298
+ openElement: false,
299
+ limiter: 'bracket',
300
+ inFullwidth: false,
301
+ inSection: false,
302
+ inRow: false,
303
+ inColumn: false,
304
+ columnCount: 0,
305
+ }) {
306
+ const setFullwidthTag = ( value, fullwidth ) => {
307
+ return fullwidth ? value.replace('et_pb_', 'et_pb_fullwidth_') : value;
308
+ }
309
+
310
+ let output = '';
311
+
312
+ for( const element in mainContent ) {
313
+ let htmlElement = mainContent[element];
314
+
315
+ let {
316
+ rawTagName: type,
317
+ rawAttrs,
318
+ childNodes: content,
319
+ classList,
320
+ } = htmlElement;
321
+
322
+ // classList returns a DOMTokenList, we need to convert it to an array
323
+ let classes = classList ? [...classList.values()] : [];
324
+
325
+ // we convert the raw attributes to a json object
326
+ let attrs = convertRawAttrs( rawAttrs );
327
+
328
+ /**
329
+ * We dont want to convert empty elements
330
+ */
331
+ if( ! content.length ){
332
+ // image tags are self closing and don't have content this is ok.
333
+ if( 'img' === type ) {
334
+ // do nothing this is ok.
335
+ // no length no type blank spaces
336
+ } else if( '' !== type ){
337
+ continue;
338
+ }
339
+ }
340
+
341
+ /**
342
+ * Important:
343
+ * If there is an open module and the current type isn't allowed in the module we must close the open module first
344
+ */
345
+ if( type && opts.openElement ) {
346
+ // if openElement has restrictions
347
+ if(
348
+ allowedModules[opts.openElement] &&
349
+ ! allowedModules[opts.openElement].includes(type)
350
+ ) {
351
+ output += closeElement(opts.openElement, opts.limiter );
352
+ opts.openElement = false;
353
+ }
354
+ }
355
+
356
+ // element type rendering
357
+ switch( type ) {
358
+ case 'div':
359
+ // .container-fluid primarily used for fullwidth sections
360
+ if( classes.includes('container-fluid') && ! opts.inFullwidth) {
361
+ opts.inFullwidth = true;
362
+
363
+ let hasContainers = content.filter((r,i) => {
364
+ if(r.attributes.class.match(/container/g)){
365
+ // we add the raw attributes to the containers
366
+ // this allows the sections to use the fluid attributes
367
+ // we do remove the container-fluid class
368
+ r.rawAttrs = r.rawAttrs + ` ${rawAttrs.replace('container-fluid', '')}`;
369
+ return true;
370
+ }
371
+ }).length
372
+
373
+ if( ! hasContainers ) {
374
+ output += addElement(
375
+ 'et_pb_fullwidth_section',
376
+ {
377
+ limiter:opts.limiter,
378
+ content: generateShortcodes(content, opts)
379
+ }
380
+ );
381
+ }else{
382
+ opts.inFullwidth = false;
383
+
384
+ output += addElement(
385
+ '',
386
+ {
387
+ content: generateShortcodes(content, opts)
388
+ }
389
+ );
390
+
391
+ }
392
+
393
+
394
+ opts.inFullwidth = false;
395
+
396
+ // .container is used primarily for regular sections
397
+ }else if( classes.includes('container') && ! opts.inSection ) {
398
+ opts.inSection = true;
399
+
400
+ // sections don't need the container class
401
+ // we remove the container class from the attributes
402
+ attrs.class = attrs.class.replace('container', '');
403
+
404
+ output += addElement(
405
+ 'et_pb_section',
406
+ {
407
+ limiter:opts.limiter,
408
+ content: generateShortcodes(content, opts),
409
+ attrs
410
+ }
411
+ );
412
+
413
+ opts.inSection = false;
414
+
415
+ // .row is used for rows
416
+ }else if( classes.includes('row') && ! opts.inRow ) {
417
+ opts.inRow = true;
418
+ // if the div has a class of row, we want to get the total number of columns
419
+ opts.columnCount = content.filter((r,i) => r.attributes?.class?.match(/(col)[-\w\d]*/g) ).length;
420
+
421
+ // sections don't need the row class
422
+ // we remove the row class from the attributes
423
+ attrs.class = attrs.class.replace('row', '');
424
+
425
+ output += addElement(
426
+ 'et_pb_row',
427
+ {
428
+ limiter: opts.limiter,
429
+ content: generateShortcodes(content, opts)
430
+ }
431
+ );
432
+
433
+ // reset the column count
434
+ opts.columnCount = 0;
435
+ opts.inRow = false;
436
+
437
+ // columns
438
+ }else if( classes.filter(c => c.match( /(col)[-\w\d]*/g )).length && ! opts.inColumn ) {
439
+ opts.inColumn = true;
440
+
441
+ /**
442
+ * if the div has multiple column classes only the first one after being sorted is used
443
+ *
444
+ * Example:
445
+ * <div class="col-md-4 col-lg-3 col"></div>
446
+ *
447
+ * Sorted:
448
+ * - col
449
+ * - col-lg-3
450
+ * - col-md-4
451
+ *
452
+ * Just col would be used
453
+ */
454
+ let colClass = classes.filter(c => c.match( /(col)[-\w\d]*/g )).sort()[0];
455
+ let colSize = Number(colClass.split( '-' ).pop());
456
+
457
+ // if the colSize is not a number, we want to set it to 12,
458
+ // 12 is the max number for bootstrap columns
459
+ colSize = ! isNaN(Number( colSize )) ? colSize : 12 / opts.columnCount;
460
+
461
+ let colType = '';
462
+ // calculate the column type
463
+ switch( colSize ) {
464
+ // 1 column
465
+ case 12:
466
+ colType = '4_4';
467
+ break;
468
+ // 2 columns
469
+ case 6:
470
+ colType = '1_2';
471
+ break;
472
+ // 3 columns
473
+ case 4:
474
+ colType = '1_3';
475
+ break;
476
+ }
477
+
478
+ // columns don't need the col class
479
+ // we remove the col class from the attributes
480
+ attrs.class = attrs.class.replace(/(col)[-\w\d]*/g, '');
481
+
482
+
483
+ // if the div has a class of col, we want to convert it to a shortcode
484
+ opts.openElement = 'et_pb_column';
485
+
486
+ // we don't close the column here
487
+ output += addElement(
488
+ opts.openElement,
489
+ {
490
+ limiter: opts.limiter,
491
+ isUnclosed: true,
492
+ attrs: {
493
+ ...attrs,
494
+ type: colType
495
+ },
496
+ content: generateShortcodes(content, opts)
497
+ }
498
+ );
499
+
500
+ // we have to close any open elements before closing the column
501
+ if( opts.openElement && 'et_pb_column' !== opts.openElement ) {
502
+ output += closeElement(opts.openElement, opts.limiter );
503
+ opts.openElement = false;
504
+ }
505
+
506
+ // now we can close the column
507
+ output += closeElement('et_pb_column', opts.limiter );
508
+
509
+ opts.inColumn = false;
510
+ }else{
511
+ // fullwidth sections only allow specific elements
512
+ // divs are not allowed
513
+ if( opts.inFullwidth ) {
514
+ // output += addElement('', { limiter: 'none', content: generateShortcodes(content) });
515
+ }else{
516
+ if( classes.includes('card') && classes.includes('blurb') ){
517
+ // this is a blurb module
518
+ output += generateModuleShortcode('blurb', content);
519
+
520
+ }else{
521
+ // figure out what kind of div element we are dealing with
522
+ output += addElement(
523
+ type,
524
+ {
525
+ limiter: opts.limiter,
526
+ content: generateShortcodes(content, opts)
527
+ }
528
+ );
529
+
530
+ }
531
+ }
532
+ }
533
+
534
+ break;
535
+
536
+ // code module
537
+ case 'code':
538
+ output += addElement(setFullwidthTag('et_pb_code', opts.inFullwidth), {attrs, limiter: 'bracket', content: generateShortcodes(content)} );
539
+ break;
540
+ // header modules
541
+ case 'h1':
542
+ case 'h2':
543
+ case 'h3':
544
+ case 'h4':
545
+ case 'h5':
546
+ case 'h6':
547
+ // we let the h1-h6 process know element is opened
548
+ // this allows other elements to be added to the header modules
549
+ opts.openElement = 'et_pb_heading';
550
+
551
+ output += addElement(
552
+ opts.openElement,
553
+ {
554
+ limiter: opts.limiter,
555
+ content: generateShortcodes(content, opts)
556
+ }
557
+ );
558
+
559
+ opts.openElement = false;
560
+ break;
561
+
562
+ case 'img':
563
+ output += addElement(
564
+ 'et_pb_image',
565
+ {
566
+ limiter: opts.limiter,
567
+ attrs,
568
+ isSelfClosing: true
569
+ }
570
+ );
571
+
572
+ break;
573
+ // all theses elements can go in a text module
574
+ case 'a':
575
+ case 'b':
576
+ case 'p':
577
+ case 'span':
578
+
579
+ if( opts.inFullwidth ) {
580
+ // figure out what to do with these elements if in a fullwidth section
581
+
582
+ // these elements get added to a text module
583
+ }else{
584
+ // if not already opened
585
+ if( ! opts.openElement ) {
586
+ opts.openElement = 'et_pb_text';
587
+
588
+ // this allows other elements to be added to the text modules
589
+ // render the text module by adding and element to an element
590
+ output += addElement(
591
+ opts.openElement,
592
+ {
593
+ limiter: opts.limiter,
594
+ isUnclosed: true,
595
+ content: addElement(
596
+ type,
597
+ {
598
+ attrs,
599
+ content: generateShortcodes(content, opts)
600
+ }
601
+ ),
602
+ }
603
+ );
604
+
605
+
606
+ // a text module has already been opened so we just add to it
607
+ }else {
608
+ output += addElement(
609
+ type, {
610
+ attrs,
611
+ content: generateShortcodes(content, opts)
612
+ }
613
+ );
614
+ }
615
+ }
616
+
617
+ break;
618
+ // default is a string element
619
+ default:
620
+ // console.log( htmlElement)
621
+ output += htmlElement;
622
+ break;
623
+
624
+ }
625
+
626
+
627
+ }
628
+
629
+ return output;
630
+ }
631
+
632
+ function generateModuleShortcode(module, content ){
633
+
634
+ switch( module ) {
635
+ case 'blurb':
636
+ // blurb module
637
+ let attrs = {
638
+ title: findElementByClass(content, 'card-title'),
639
+ };
640
+
641
+ let img = findSiblingElement(content, 'img');
642
+
643
+ if( img ){
644
+ let imgAttrs = convertRawAttrs( img.rawAttrs );
645
+ attrs.image = imgAttrs.src
646
+ }
647
+
648
+ return addElement(
649
+ 'et_pb_blurb',
650
+ {
651
+ limiter: 'bracket',
652
+ attrs
653
+ }
654
+ );
655
+ }
656
+ }
657
+
658
+ /**
659
+ * Attempts to convert a site.
660
+ *
661
+ * @param {Object} options
662
+ * @param {Object} options.spinner A CLI spinner which indicates progress.
663
+ * @param {boolean} options.debug True if debug mode is enabled.
664
+ * @param {boolean} options.builder Editor style to use for the pages. Choices are 'plain', 'divi', or 'gutenberg'.
665
+ */
666
+ export default async function convertSite({
667
+ spinner,
668
+ debug} ) {
669
+ spinner.stop();
670
+
671
+ // let buildPath = path.join( appPath, 'content' );
672
+ let buildPath = path.join( appPath, 'build' );
673
+
674
+ /**
675
+ * Return all .htlm files in the build directory
676
+ *
677
+ * exclusions:
678
+ * - serp.html - This a search engine results page, we don't need to parse this
679
+ */
680
+ let sitePages = fs.readdirSync( buildPath, { recursive: true } ).filter( file => {
681
+ return 'serp.html' !== file && file.endsWith( '.html' )
682
+ } );
683
+
684
+ for( const file of sitePages ) {
685
+ // get the file path
686
+ let filePath = path.join( buildPath, file );
687
+ let fileMarkup = fs.readFileSync( filePath, 'utf8' );
688
+
689
+ // We use jsdom to emulate a browser environment and get the window.document
690
+ let fileMarkupJSon = await jsdom.JSDOM.fromFile( filePath );
691
+ let { document } = fileMarkupJSon.window;
692
+
693
+ // all we need is the document #page-container #main-content
694
+ let mainContent = document.querySelector( '#page-container #main-content' );
695
+
696
+ // if the default #page-container #main-content exists
697
+ if( mainContent ){
698
+ // use html-to-json-parser to convert the page container into json
699
+ // let mainContentJson = await HTMLToJSON( mainContent.outerHTML, false );
700
+ let mainContentJson = parse( mainContent.outerHTML );
701
+
702
+ // sanitize the json
703
+ mainContentJson = sanitizeJson( mainContentJson.childNodes )[0];
704
+ /**
705
+ * Main content is allowed 2 elements
706
+ * - main - renders the main content
707
+ * - aside - renders the sidebar content
708
+ */
709
+ mainContent = mainContentJson.childNodes.filter( e => 'main' === e.rawTagName )[0];
710
+
711
+ // if main content was found
712
+ if( mainContent && mainContent.childNodes ) {
713
+ let shortcodeContent = '';
714
+ // loop through the main content and convert it to shortcodes
715
+ // main-content should only have 1 element, div
716
+ // .container for regular sections
717
+ // .container-fluid for fullwidth sections
718
+ for( const e in mainContent.childNodes ) {
719
+ shortcodeContent += generateShortcodes( [mainContent.childNodes[e]] );
720
+ }
721
+
722
+ console.log( `shortcodeContent: ${shortcodeContent}` );
723
+ }
724
+
725
+
726
+ }
727
+
728
+ }
729
+
730
+ return
731
+ let siteData = {};
732
+
733
+ // check if the site data file exists
734
+ if( fs.existsSync( filePath ) ) {
735
+ let file = fs.readFileSync( filePath, 'utf8' );
736
+ let data = JSON.parse( file );
737
+
738
+ // check if the data file has site data
739
+ if( data.site ) {
740
+ const continueProcess = await confirm(
741
+ {
742
+ message: `Site data already exists, existing data will be overwritten.\nWould you like to continue?`,
743
+ default: true
744
+ },
745
+ {
746
+ clearPromptOnDone: true,
747
+ }
748
+ ).catch(() => {process.exit(1);});
749
+
750
+ // if the user wants to continue, set the site data
751
+ // otherwise exit the process
752
+ if( continueProcess ){
753
+ siteData = data.site;
754
+ }else{
755
+ spinner.fail( 'Site creation cancelled.' );
756
+ process.exit( 0 );
757
+ }
758
+ }
759
+ }
760
+
761
+ writeLine('CAWebPublishing Site Creation Process', {char: '#', borderColor: 'green'});
762
+ writeLine('This process will create a site configuration file for CAWebPublishing.', {color: 'cyan', prefix: 'i'});
763
+ writeLine('Please answer the following questions to create your site configuration file.', {color: 'cyan', prefix: 'i'});
764
+ writeLine('You can skip any question by pressing enter.', {color: 'cyan', prefix: 'i'});
765
+ writeLine('You can also edit the configuration file later.', {color: 'cyan', prefix: 'i'});
766
+
767
+ // populate the site data
768
+ siteData = {
769
+ ...await promptForGeneralInfo(siteTitle),
770
+ header: await promptForHeader(),
771
+ alerts: await promptForAlerts(),
772
+ social: await promptForSocial(),
773
+ google: await promptForGoogleOptions(),
774
+ footer: await promptForFooter(),
775
+ };
776
+
777
+ // write the site data to the file
778
+ fs.writeFileSync(
779
+ path.join( appPath, 'caweb.json' ),
780
+ JSON.stringify( {site:siteData}, null, 4 )
781
+ );
782
+
783
+ writeLine('CAWebPublishing Site Creation Process Complete', {char: '#', borderColor: 'green'});
784
+ writeLine('You can now start the site by running the following command:', {color: 'cyan', prefix: 'i'});
785
+ writeLine(`npm run caweb serve`, {color: 'cyan', prefix: 'i'});
786
+
787
+ spinner.start('CAWebPublishing Site Configuration file saved.');
788
+
789
+ };