@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.
- package/README.md +1 -1
- package/commands/index.js +10 -5
- package/commands/sites/convert-site.js +789 -0
- package/commands/sites/create-site.js +102 -0
- package/commands/sites/prompts.js +614 -0
- package/commands/webpack/webpack.js +2 -4
- package/lib/cli.js +121 -66
- package/lib/helpers.js +96 -6
- package/lib/index.js +7 -1
- package/package.json +14 -5
|
@@ -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
|
+
};
|