@caweb/cli 1.11.1 → 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.
@@ -3,29 +3,22 @@
3
3
  */
4
4
  import path from 'path';
5
5
  import fs from 'fs';
6
- import { confirm } from '@inquirer/prompts';
7
6
  import chalk from 'chalk';
8
- import { HTMLToJSON } from 'html-to-json-parser';
9
7
  import jsdom from 'jsdom';
10
8
  import { parse } from 'node-html-parser';
11
9
 
12
10
  /**
13
11
  * Internal dependencies
14
12
  */
15
- import { appPath, writeLine } from '../../lib/index.js';
13
+ import { appPath, projectPath, writeLine } from '../../lib/index.js';
16
14
 
17
- import {
18
- promptForGeneralInfo,
19
- promptForSocial,
20
- promptForGoogleOptions,
21
- promptForAlerts,
22
- promptForHeader,
23
- promptForFooter
24
- } from './prompts.js';
15
+ import validatePage from './validation.js';
25
16
 
26
17
  /**
27
18
  * Constants used during the conversion process.
28
19
  */
20
+ const { DIVI_VER } = JSON.parse( fs.readFileSync( path.join(projectPath, 'package.json') ) ).config;
21
+
29
22
  /**
30
23
  * Modules allow list
31
24
  */
@@ -57,37 +50,6 @@ function getLimiter( type ) {
57
50
  return limiter;
58
51
  }
59
52
 
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
53
  /**
92
54
  * This function is used to sanitize the JSON data.
93
55
  *
@@ -188,15 +150,58 @@ function getAttrString(attributes, limiter) {
188
150
 
189
151
  // style mappings
190
152
  switch( k ) {
153
+ case 'background-color':
154
+ k = 'background_color';
155
+ break;
191
156
  case 'background-image':
192
157
  k = 'background_image';
193
- s = s.replace(/url\((.*?)\).*/g, '$1').replace(/["']/g, '');
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, '');
194
171
  break;
195
172
 
196
173
  case 'background-repeat':
197
- k = 'background_position';
174
+ k = 'background_repeat';
198
175
  s = s.replace(/(.*?)(repeat|no-repeat)/g, '$2');
199
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;
200
205
  default:
201
206
  k = '';
202
207
  break;
@@ -210,8 +215,11 @@ function getAttrString(attributes, limiter) {
210
215
 
211
216
  }
212
217
  }
213
-
218
+
214
219
  if( '' !== key && 'string' === typeof key ) {
220
+ // we have to encode certain characters
221
+ value = value.replaceAll('"', '%22');
222
+
215
223
  attrString += ` ${key}="${value}"`;
216
224
  }else if( 'object' === typeof key ) {
217
225
  attrString += ` ${key.join(' ')}`;
@@ -278,6 +286,7 @@ function closeElement( tag, limiter ) {
278
286
  return `${lmtrObj.open}/${tag}${lmtrObj.close}`
279
287
  }
280
288
 
289
+
281
290
  /**
282
291
  * Generates shortcodes from the json object.
283
292
  * Converts a json dom object into shortcodes.
@@ -406,7 +415,10 @@ function generateShortcodes( mainContent, opts = {
406
415
  {
407
416
  limiter:opts.limiter,
408
417
  content: generateShortcodes(content, opts),
409
- attrs
418
+ attrs: {
419
+ _builder_version: DIVI_VER,
420
+ ...attrs,
421
+ }
410
422
  }
411
423
  );
412
424
 
@@ -513,9 +525,14 @@ function generateShortcodes( mainContent, opts = {
513
525
  if( opts.inFullwidth ) {
514
526
  // output += addElement('', { limiter: 'none', content: generateShortcodes(content) });
515
527
  }else{
516
- if( classes.includes('card') && classes.includes('blurb') ){
517
- // this is a blurb module
518
- output += generateModuleShortcode('blurb', content);
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
+ }
519
536
 
520
537
  }else{
521
538
  // figure out what kind of div element we are dealing with
@@ -546,17 +563,11 @@ function generateShortcodes( mainContent, opts = {
546
563
  case 'h6':
547
564
  // we let the h1-h6 process know element is opened
548
565
  // this allows other elements to be added to the header modules
549
- opts.openElement = 'et_pb_heading';
566
+ // opts.openElement = 'et_pb_heading';
550
567
 
551
- output += addElement(
552
- opts.openElement,
553
- {
554
- limiter: opts.limiter,
555
- content: generateShortcodes(content, opts)
556
- }
557
- );
558
568
 
559
- opts.openElement = false;
569
+ output += generateModuleShortcode('heading', htmlElement);
570
+ // opts.openElement = false;
560
571
  break;
561
572
 
562
573
  case 'img':
@@ -582,7 +593,8 @@ function generateShortcodes( mainContent, opts = {
582
593
  // these elements get added to a text module
583
594
  }else{
584
595
  // if not already opened
585
- if( ! opts.openElement ) {
596
+ // or the last opened element is a column
597
+ if( ! opts.openElement || 'et_pb_column' === opts.openElement ) {
586
598
  opts.openElement = 'et_pb_text';
587
599
 
588
600
  // this allows other elements to be added to the text modules
@@ -629,30 +641,245 @@ function generateShortcodes( mainContent, opts = {
629
641
  return output;
630
642
  }
631
643
 
632
- function generateModuleShortcode(module, content ){
644
+ function generateModuleShortcode(module, element ){
645
+ let content = '';
646
+ let attrs = {};
647
+ let moduleName = 'et_pb_' + module;
633
648
 
634
649
  switch( module ) {
635
- case 'blurb':
636
- // blurb module
637
- let attrs = {
638
- title: findElementByClass(content, 'card-title'),
639
- };
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
+ }
640
752
 
641
- let img = findSiblingElement(content, 'img');
753
+ attrs.include_header = 'on';
754
+ attrs.title = header.text.trim();
755
+
756
+ }
642
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
643
770
  if( img ){
644
- let imgAttrs = convertRawAttrs( img.rawAttrs );
645
- attrs.image = imgAttrs.src
771
+ attrs.show_image = 'on';
772
+ attrs.featured_image = img[0].getAttribute('src');
646
773
  }
647
774
 
648
- return addElement(
649
- 'et_pb_blurb',
650
- {
651
- limiter: 'bracket',
652
- attrs
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';
653
789
  }
654
- );
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();
655
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
+ );
656
883
  }
657
884
 
658
885
  /**
@@ -668,11 +895,12 @@ export default async function convertSite({
668
895
  debug} ) {
669
896
  spinner.stop();
670
897
 
671
- // let buildPath = path.join( appPath, 'content' );
672
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 = [];
673
901
 
674
902
  /**
675
- * Return all .htlm files in the build directory
903
+ * Return all .html files in the build directory
676
904
  *
677
905
  * exclusions:
678
906
  * - serp.html - This a search engine results page, we don't need to parse this
@@ -684,12 +912,13 @@ export default async function convertSite({
684
912
  for( const file of sitePages ) {
685
913
  // get the file path
686
914
  let filePath = path.join( buildPath, file );
687
- let fileMarkup = fs.readFileSync( filePath, 'utf8' );
688
915
 
689
916
  // We use jsdom to emulate a browser environment and get the window.document
690
917
  let fileMarkupJSon = await jsdom.JSDOM.fromFile( filePath );
691
918
  let { document } = fileMarkupJSon.window;
692
919
 
920
+ validatePage( document );
921
+
693
922
  // all we need is the document #page-container #main-content
694
923
  let mainContent = document.querySelector( '#page-container #main-content' );
695
924
 
@@ -701,6 +930,7 @@ export default async function convertSite({
701
930
 
702
931
  // sanitize the json
703
932
  mainContentJson = sanitizeJson( mainContentJson.childNodes )[0];
933
+
704
934
  /**
705
935
  * Main content is allowed 2 elements
706
936
  * - main - renders the main content
@@ -719,7 +949,16 @@ export default async function convertSite({
719
949
  shortcodeContent += generateShortcodes( [mainContent.childNodes[e]] );
720
950
  }
721
951
 
722
- console.log( `shortcodeContent: ${shortcodeContent}` );
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
+
723
962
  }
724
963
 
725
964
 
@@ -727,63 +966,27 @@ export default async function convertSite({
727
966
 
728
967
  }
729
968
 
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 );
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'
757
978
  }
758
- }
979
+ },
980
+ media: path.join( appPath, 'build', 'media' ),
981
+ pages,
759
982
  }
760
983
 
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
984
  // write the site data to the file
778
985
  fs.writeFileSync(
779
986
  path.join( appPath, 'caweb.json' ),
780
- JSON.stringify( {site:siteData}, null, 4 )
987
+ JSON.stringify( siteData, null, 4 )
781
988
  );
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.');
989
+
990
+ writeLine( chalk.green( 'Site converted successfully.' ) );
788
991
 
789
992
  };
@@ -0,0 +1,68 @@
1
+ import { parse } from 'node-html-parser';
2
+ import { writeLine } from '../../lib/index.js';
3
+
4
+ /**
5
+ * Validates the HTML of a page.
6
+ * @param {JSDOM} document - Window document from JSDOM.
7
+ * @returns {boolean} - Returns true if the HTML is valid, false otherwise.
8
+ */
9
+ function validatePage( document ){
10
+ let mainContent = document.querySelector( '#page-container #main-content' );
11
+
12
+ // Check if document doesn't have a #page-container #main-content
13
+ if( ! mainContent ){
14
+ throw new Error("The page does not have a #page-container #main-content.");
15
+ }
16
+
17
+ let mainContentJson = parse( mainContent.innerHTML );
18
+ let mainJson = ''
19
+ let asideJson = '';
20
+
21
+ /**
22
+ * Main content is allowed 2 elements
23
+ * - main - renders the main content
24
+ * - aside - renders the sidebar content
25
+ */
26
+ for( const element of mainContentJson.children ){
27
+ if( element.tagName !== 'MAIN' && element.tagName !== 'ASIDE' ){
28
+ throw new Error("The main content must only contain <main> and <aside> elements.");
29
+ }else if( element.tagName === 'MAIN' ){
30
+ mainJson = element
31
+ }else if( element.tagName === 'ASIDE' ){
32
+ asideJson = element
33
+ }
34
+ }
35
+
36
+ /**
37
+ * Main should only consist of <div> elements.
38
+ * - .container-fluid - Used for full width containers.
39
+ * - .container - used for standard containers.
40
+ * -- Should only consist of div.row
41
+ * ---- Should only consist of div.col-#
42
+ * ------ All content should be in a column
43
+ */
44
+ for( const element of mainJson.children ){
45
+ if( element.tagName !== 'DIV' ){
46
+ throw new Error("The <main> element must only contain <div> elements.");
47
+ }else if( ! element.classList.contains( 'container-fluid' ) && ! element.classList.contains( 'container' ) ){
48
+ throw new Error("The <main> element must only contain <div> elements with class 'container-fluid' or 'container'.");
49
+ }else if( element.classList.contains( 'container' ) ){
50
+ for( const row of element.children ){
51
+ if( row.tagName !== 'DIV' || ! row.classList.contains( 'row' ) ){
52
+ throw new Error("The <div> element with class 'container' must only contain <div> elements with class 'row'.");
53
+ }else{
54
+ for( const col of row.children ){
55
+ if( col.tagName !== 'DIV' || ! col.classList.toString().match( /col[-\d]*/g ) ){
56
+ throw new Error("The <div> element with class 'row' must only contain <div> elements with class 'col-#'.");
57
+ }
58
+ }
59
+ }
60
+ }
61
+ }
62
+
63
+ }
64
+
65
+
66
+ }
67
+
68
+ export default validatePage;