@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.
- package/README.md +1 -1
- package/commands/index.js +10 -5
- package/commands/sites/convert-site.js +992 -0
- package/commands/sites/create-site.js +102 -0
- package/commands/sites/prompts.js +614 -0
- package/commands/sites/validation.js +68 -0
- package/commands/sync/index.js +484 -234
- 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/lib/wordpress/api.js +4 -3
- package/package.json +16 -6
|
@@ -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
|
+
};
|