ultimate-jekyll-manager 0.0.36 → 0.0.37

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 CHANGED
@@ -57,7 +57,27 @@ npm start -- --debug=true
57
57
  ```
58
58
  * `--ujPluginDevMode=true` - Enables the development mode for the [Ultimate Jekyll Ruby plugin](https://github.com/itw-creative-works/jekyll-uj-powertools).
59
59
  ```bash
60
+ npm start -- --ujPluginDevMode=true
61
+ ```
62
+
63
+ ## Running Specific Tasks
64
+ You can run specific tasks using the `npm run gulp` command with the appropriate task name.
65
+
66
+ Some of these require environment variables to be set and other tasks to be run first.
60
67
 
68
+ Here are some examples:
69
+
70
+ ### Run the `audit` task:
71
+ ```bash
72
+ npx uj audit
73
+ ```
74
+
75
+ ### Run the `translation` task:
76
+ ```bash
77
+ GH_TOKEN=XXX \
78
+ GITHUB_REPOSITORY=XXX \
79
+ npx uj translation
80
+ ```
61
81
  <!-- Developing -->
62
82
  ## 🛠 Developing
63
83
  1. Clone the repo to your local machine.
package/dist/cli.js CHANGED
@@ -10,6 +10,9 @@ const ALIASES = {
10
10
  install: ['-i', 'i', '--install'],
11
11
  setup: ['-s', '--setup'],
12
12
  version: ['-v', '--version'],
13
+ // Tasks
14
+ audit: ['-a', '--audit'],
15
+ translation: ['-t', '--translation'],
13
16
  };
14
17
 
15
18
  // Function to resolve command name from aliases
@@ -0,0 +1,15 @@
1
+ // Libraries
2
+ const Manager = new (require('../build.js'));
3
+ const logger = Manager.logger('audit');
4
+ const { execute } = require('node-powertools');
5
+
6
+ // Load package
7
+ const package = Manager.getPackage('main');
8
+ const project = Manager.getPackage('project');
9
+
10
+ module.exports = async function (options) {
11
+ // Log
12
+ logger.log(`Starting audit...`);
13
+
14
+ await execute('UJ_FORCE_AUDIT=true bundle exec npm run gulp -- audit', { log: true })
15
+ };
@@ -0,0 +1,15 @@
1
+ // Libraries
2
+ const Manager = new (require('../build.js'));
3
+ const logger = Manager.logger('translation');
4
+ const { execute } = require('node-powertools');
5
+
6
+ // Load package
7
+ const package = Manager.getPackage('main');
8
+ const project = Manager.getPackage('project');
9
+
10
+ module.exports = async function (options) {
11
+ // Log
12
+ logger.log(`Starting translation...`);
13
+
14
+ await execute('UJ_FORCE_TRANSLATION=true bundle exec npm run gulp -- translation', { log: true })
15
+ };
@@ -0,0 +1,33 @@
1
+ <!-- Alerts -->
2
+ <!-- Master - Outdated Alert Box -->
3
+ <div
4
+ class="main-alert main-alert-top main-alert-fixed main-alert-outdated bg-danger animation-fade-in"
5
+ role="alert" aria-live="polite" aria-label="Outdated browser"
6
+ hidden>
7
+ <span class="main-alert-close">&times;</span>
8
+ <div>
9
+ <i class="fa-solid fa-warning fa-bounce me-2"></i>
10
+ You are using an outdated browser that our site <strong>DOES NOT</strong> support. Please <a href="https://www.google.com/chrome" rel="nofollow" target="_blank">click here</a> to update your browser.
11
+ </div>
12
+ </div>
13
+ <div
14
+ class="main-alert main-alert-top main-alert-fixed main-alert-suspended bg-danger animation-fade-in"
15
+ role="alert" aria-live="polite" aria-label="Payment method issue"
16
+ hidden>
17
+ <span class="main-alert-close">&times;</span>
18
+ <div>
19
+ <i class="fa-solid fa-warning fa-bounce me-2"></i>
20
+ There is a <strong>problem with your payment method</strong>. To continue using <strong>{{ site.brand.name }}</strong>, please <a href="{{ site.url }}/account#billing" target="_blank">update your payment method</a>.
21
+ </div>
22
+ </div>
23
+ <div
24
+ class="main-alert main-alert-top main-alert-fixed main-alert-sale bg-primary animation-fade-in"
25
+ role="alert" aria-live="polite" aria-label="Flash sale"
26
+ hidden>
27
+ <span class="main-alert-close">&times;</span>
28
+ <div>
29
+ <i class="fa-solid fa-stopwatch fa-beat me-2"></i>
30
+ <strong>FLASH SALE!</strong>
31
+ Save <strong>15%</strong> at checkout—today only! <a href="{{ site.url }}/pricing" target="_blank">Claim discount</a>.
32
+ </div>
33
+ </div>
@@ -11,34 +11,34 @@
11
11
  {% endif %}
12
12
 
13
13
  <!-- Prefetch -->
14
- <link rel="dns-prefetch" href="https://fonts.googleapis.com">
15
- <link rel="dns-prefetch" href="https://fonts.gstatic.com" crossorigin>
16
- <link rel="dns-prefetch" href="https://cdn.jsdelivr.net">
17
- <link rel="dns-prefetch" href="https://cdnjs.cloudflare.com">
14
+ <link rel="dns-prefetch" href="https://fonts.googleapis.com"/>
15
+ <link rel="dns-prefetch" href="https://fonts.gstatic.com" crossorigin/>
16
+ <link rel="dns-prefetch" href="https://cdn.jsdelivr.net"/>
17
+ <link rel="dns-prefetch" href="https://cdnjs.cloudflare.com"/>
18
18
 
19
19
  <!-- Meta -->
20
- <meta charset="utf-8">
21
- <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
22
- <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
20
+ <meta charset="utf-8"/>
21
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
22
+ <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"/>
23
23
  {% if site.meta.viewport != '' and site.meta.viewport != false and site.meta.viewport != null %}
24
- <meta name="viewport" content="{{ site.meta.viewport }}">
24
+ <meta name="viewport" content="{{ site.meta.viewport }}"/>
25
25
  {% else %}
26
26
  <!-- <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no, minimum-scale=1, maximum-scale=5"> --> <!-- DISABLED 4/8/23 to prevent auto-zooming on text fields -->
27
- <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0, shrink-to-fit=no">
27
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0, shrink-to-fit=no"/>
28
28
  {% endif %}
29
- <meta name="HandheldFriendly" content="True">
30
- <meta name="MobileOptimized" content="320">
31
- <meta name="generator" content="Ultimate Jekyll">
29
+ <meta name="HandheldFriendly" content="True"/>
30
+ <meta name="MobileOptimized" content="320"/>
31
+ <meta name="generator" content="Ultimate Jekyll"/>
32
32
 
33
33
  <!-- Referrer -->
34
- <meta name="referrer" content="{{ page.meta.referrer | default: layout.meta.referrer | default: site.meta.referrer | default: 'strict-origin-when-cross-origin' }}">
34
+ <meta name="referrer" content="{{ page.meta.referrer | default: layout.meta.referrer | default: site.meta.referrer | default: 'strict-origin-when-cross-origin' }}"/>
35
35
 
36
36
  <!-- Canonical -->
37
- <link rel="canonical" href="{{ site.url }}{{ page-url-stripped }}">
37
+ <link rel="canonical" href="{{ site.url }}{{ page-url-stripped }}"/>
38
38
 
39
39
  <!-- Robots -->
40
40
  {%- if page.meta.index == false or layout.meta.index == false or site.meta.index == false -%}
41
- <meta name="robots" content="noindex">
41
+ <meta name="robots" content="noindex"/>
42
42
  {% endif %}
43
43
 
44
44
  <!-- Keywords -->
@@ -49,10 +49,10 @@
49
49
  {%- else -%}
50
50
  {% assign page-keywords = '' %}
51
51
  {%- endif -%}
52
- <meta name="keywods" content="{{ page-keywords }}">
52
+ <meta name="keywords" content="{{ page-keywords }}"/>
53
53
 
54
54
  <!-- Manifest.json -->
55
- <link rel="manifest" href="{{ site.url }}/manifest.json">
55
+ <link rel="manifest" href="{{ site.url }}/manifest.json"/>
56
56
 
57
57
  <!-- Title -->
58
58
  {%- if page-is-post and page.post.title != null and page.post.title != '' -%}
@@ -65,8 +65,8 @@
65
65
  {% assign page-title = site.meta.title | liquify %}
66
66
  {%- endif -%}
67
67
  <title>{{ page-title }}</title>
68
- <meta name="twitter:title" content="{{ page-title }}">
69
- <meta property="og:title" content="{{ page-title }}">
68
+ <meta name="twitter:title" content="{{ page-title }}"/>
69
+ <meta property="og:title" content="{{ page-title }}"/>
70
70
 
71
71
  <!-- Description -->
72
72
  {%- if page-is-post and page.post.excerpt != null and page.post.excerpt != '' -%}
@@ -78,9 +78,9 @@
78
78
  {%- else -%}
79
79
  {% assign page-description = site.meta.description | liquify %}
80
80
  {%- endif -%}
81
- <meta name="description" content="{{ page-description }}">
82
- <meta name="twitter:description" content="{{ page-description }}">
83
- <meta property="og:description" content="{{ page-description }}">
81
+ <meta name="description" content="{{ page-description }}"/>
82
+ <meta name="twitter:description" content="{{ page-description }}"/>
83
+ <meta property="og:description" content="{{ page-description }}"/>
84
84
 
85
85
  <!-- Image -->
86
86
  {%- if page-is-post and page.post.id != null and page.post.id != '' -%}
@@ -90,30 +90,29 @@
90
90
  {%- else -%}
91
91
  {% capture page-image %}{%- if site.meta.og-image contains '://' -%}{%- else -%}{{ site.url }}/assets/images/og{%- endif -%}{{ site.meta.og-image }}{% endcapture %}
92
92
  {%- endif -%}
93
- <meta name="twitter:image" content="{{ page-image }}">
94
- <meta property="og:image" content="{{ page-image }}">
93
+ <meta name="twitter:image" content="{{ page-image }}"/>
94
+ <meta property="og:image" content="{{ page-image }}"/>
95
95
 
96
96
  <!-- OG: Twitter -->
97
- <meta name="twitter:card" content="summary">
98
- <meta name="twitter:site" content="@{{ site.socials.twitter }}">
99
- <meta name="twitter:widgets:theme" content="light">
97
+ <meta name="twitter:card" content="summary"/>
98
+ <meta name="twitter:site" content="@{{ site.socials.twitter }}"/>
99
+ <meta name="twitter:widgets:theme" content="light"/>
100
100
 
101
101
  <!-- OG: Facebook / Open Graph -->
102
- <meta property="og:url" content="{{ site.url }}{{ page-url-stripped }}">
102
+ <meta property="og:url" content="{{ site.url }}{{ page-url-stripped }}"/>
103
103
  {%- if page-is-post -%}
104
104
  {% assign og-type = 'article' %}
105
105
  {%- else -%}
106
106
  {% assign og-type = 'website' %}
107
107
  {%- endif -%}
108
- <meta property="og:type" content="{{ og-type }}">
108
+ <meta property="og:type" content="{{ og-type }}"/>
109
109
 
110
110
  <!-- Main Feed -->
111
- <link href="{{ site.url }}/feeds/posts.xml" type="application/atom+xml" rel="alternate" title="{{ site.brand.name }} Feed">
111
+ <link href="{{ site.url }}/feeds/posts.xml" type="application/atom+xml" rel="alternate" title="{{ site.brand.name }} Feed"/>
112
112
 
113
113
  <!-- Language Tags -->
114
- <link rel="alternate" href="{{ site.url }}{{ page-url-stripped }}" hreflang="x-default">
115
- <link rel="alternate" href="{{ site.url }}{{ page-url-stripped }}" hreflang="en">
116
- <!-- <link rel="alternate" href="{{ site.url }}/es/{{ page-url-stripped }}" hreflang="es"> -->
114
+ <link rel="alternate" href="{{ site.url }}{{ page-url-stripped }}" hreflang="x-default"/>
115
+ <link rel="alternate" href="{{ site.url }}{{ page-url-stripped }}" hreflang="{{ site.translation.default | default: 'en' }}"/>
117
116
 
118
117
  <!-- Favicon -->
119
118
  {%- if site.favicon.path != null and site.favicon.path != '' -%}
@@ -121,24 +120,24 @@
121
120
  {%- else -%}
122
121
  {% assign favicon-path = site.url | append: '/assets/images/favicon' %}
123
122
  {%- endif -%}
124
- <link rel="apple-touch-icon" sizes="180x180" href="{{ favicon-path }}/apple-touch-icon.png?cb={{ site.uj.cache_breaker }}">
125
- <link rel="icon" type="image/png" sizes="32x32" href="{{ favicon-path }}/favicon-32x32.png?cb={{ site.uj.cache_breaker }}">
126
- <link rel="icon" type="image/png" sizes="16x16" href="{{ favicon-path }}/favicon-16x16.png?cb={{ site.uj.cache_breaker }}">
127
- <link rel="mask-icon" href="{{ favicon-path }}/safari-pinned-tab.svg" color="{{ site.favicon.safari-pinned-tab }}">
128
- <link rel="shortcut icon" type="image/x-icon" href="{{ favicon-path }}/favicon.ico?cb={{ site.uj.cache_breaker }}">
129
- <meta name="msapplication-TileColor" content="{{ site.favicon.msapp-tile-color }}">
130
- <meta name="msapplication-config" content="{{ favicon-path }}/browserconfig.xml?cb={{ site.uj.cache_breaker }}">
131
- <meta name="theme-color" content="{{ site.favicon.theme-color }}">
123
+ <link rel="apple-touch-icon" sizes="180x180" href="{{ favicon-path }}/apple-touch-icon.png?cb={{ site.uj.cache_breaker }}"/>
124
+ <link rel="icon" type="image/png" sizes="32x32" href="{{ favicon-path }}/favicon-32x32.png?cb={{ site.uj.cache_breaker }}"/>
125
+ <link rel="icon" type="image/png" sizes="16x16" href="{{ favicon-path }}/favicon-16x16.png?cb={{ site.uj.cache_breaker }}"/>
126
+ <link rel="mask-icon" href="{{ favicon-path }}/safari-pinned-tab.svg" color="{{ site.favicon.safari-pinned-tab }}"/>
127
+ <link rel="shortcut icon" type="image/x-icon" href="{{ favicon-path }}/favicon.ico?cb={{ site.uj.cache_breaker }}"/>
128
+ <meta name="msapplication-TileColor" content="{{ site.favicon.msapp-tile-color }}"/>
129
+ <meta name="msapplication-config" content="{{ favicon-path }}/browserconfig.xml?cb={{ site.uj.cache_breaker }}"/>
130
+ <meta name="theme-color" content="{{ site.favicon.theme-color }}"/>
132
131
 
133
132
  <!-- Browser Support -->
134
133
  <!--[if lte IE 9]>
135
- <link rel="stylesheet" type="text/css" href="/assets/css/main/lte-ie9.css?cb={{ site.uj.cache_breaker }}">
134
+ <link rel="stylesheet" type="text/css" href="/assets/css/main/lte-ie9.css?cb={{ site.uj.cache_breaker }}"/>
136
135
  <script src="https://cdnjs.cloudflare.com/ajax/libs/html5shiv/3.7.3/html5shiv.min.js"></script>
137
136
  <script src="https://cdnjs.cloudflare.com/ajax/libs/respond.js/1.4.2/respond.min.js"></script>
138
137
  <![endif]-->
139
138
 
140
139
  <!-- CSS Bundle -->
141
- <link rel="stylesheet" type="text/css" href="{{ site.url }}/assets/css/main.bundle.css?cb={{ site.uj.cache_breaker }}">
140
+ <link rel="stylesheet" type="text/css" href="{{ site.url }}/assets/css/main.bundle.css?cb={{ site.uj.cache_breaker }}"/>
142
141
 
143
142
  <!-- Style - Scripts are Disabled -->
144
143
  <noscript>
@@ -149,40 +148,6 @@
149
148
  </style>
150
149
  </noscript>
151
150
 
152
- <!-- Alerts -->
153
- <!-- Master - Outdated Alert Box -->
154
- <div
155
- class="main-alert main-alert-top main-alert-fixed main-alert-outdated bg-danger animation-fade-in"
156
- role="alert" aria-live="polite" aria-label="Outdated browser"
157
- hidden>
158
- <span class="main-alert-close">&times;</span>
159
- <div>
160
- <i class="fa-solid fa-warning fa-bounce me-2"></i>
161
- You are using an outdated browser that our site <strong>DOES NOT</strong> support. Please <a href="https://www.google.com/chrome" rel="nofollow" target="_blank">click here</a> to update your browser.
162
- </div>
163
- </div>
164
- <div
165
- class="main-alert main-alert-top main-alert-fixed main-alert-suspended bg-danger animation-fade-in"
166
- role="alert" aria-live="polite" aria-label="Payment method issue"
167
- hidden>
168
- <span class="main-alert-close">&times;</span>
169
- <div>
170
- <i class="fa-solid fa-warning fa-bounce me-2"></i>
171
- There is a <strong>problem with your payment method</strong>. To continue using <strong>{{ site.brand.name }}</strong>, please <a href="{{ site.url }}/account#billing" target="_blank">update your payment method</a>.
172
- </div>
173
- </div>
174
- <div
175
- class="main-alert main-alert-top main-alert-fixed main-alert-sale bg-primary animation-fade-in"
176
- role="alert" aria-live="polite" aria-label="Flash sale"
177
- hidden>
178
- <span class="main-alert-close">&times;</span>
179
- <div>
180
- <i class="fa-solid fa-stopwatch fa-beat me-2"></i>
181
- <strong>FLASH SALE!</strong>
182
- Save <strong>15%</strong> at checkout—today only! <a href="{{ site.url }}/pricing" target="_blank">Claim discount</a>.
183
- </div>
184
- </div>
185
-
186
151
  <!-- App - Head Content -->
187
152
  {%- if page.settings.include-app-head == false -%}
188
153
  {% elsif layout.settings.include-app-head == false %}
@@ -8,7 +8,7 @@ layout: null
8
8
 
9
9
  <!DOCTYPE html>
10
10
  <html
11
- lang="en"
11
+ lang="{{ site.translation.default | default: 'en' }}"
12
12
  class="{{ layout.html.class }} {{ page.html.class }}" {{ layout.html.attributes }} {{ page.html.attributes }}
13
13
 
14
14
  data-theme-id="{{ site.theme.id }}"
@@ -27,6 +27,9 @@ layout: null
27
27
  class="{{ layout.body.class }} {{ page.body.class }}" {{ layout.body.attributes }} {{ page.body.attributes }}
28
28
  >
29
29
 
30
+ <!-- Head -->
31
+ {%- include /main/global/body.html -%}
32
+
30
33
  <!-- Header -->
31
34
  {%- if page.settings.include-app-header == false -%}
32
35
  {% elsif layout.settings.include-app-header == false %}
@@ -3,9 +3,18 @@ layout: null
3
3
  permalink: /feeds/posts.xml
4
4
  ---
5
5
 
6
- <rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom">
6
+ <rss
7
+ version="2.0"
8
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
9
+ xmlns:content="http://purl.org/rss/1.0/modules/content/"
10
+ xmlns:atom="http://www.w3.org/2005/Atom"
11
+ >
7
12
  <channel>
8
- <atom:link href="{{ site.url }}{{ page.url }}" rel="self" type="application/rss+xml" />
13
+ <atom:link
14
+ href="{{ site.url }}{{ page.url }}"
15
+ rel="self"
16
+ type="application/rss+xml"
17
+ />
9
18
  <title>
10
19
  <![CDATA[
11
20
  {{ site.brand.name }} Blog
@@ -6,7 +6,12 @@ sitemap:
6
6
  ---
7
7
 
8
8
  <?xml version="1.0" encoding="UTF-8"?>
9
- <urlset xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd" xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
9
+ <urlset
10
+ xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
11
+ xmlns:xhtml="http://www.w3.org/1999/xhtml"
12
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
13
+ xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd" xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
14
+ >
10
15
  {% assign pages = site.pages %}
11
16
  {% assign default-time = site.time | date_to_xmlschema %}
12
17
 
package/dist/gulp/main.js CHANGED
@@ -5,6 +5,7 @@ const argv = Manager.getArguments();
5
5
  const { series, parallel, watch } = require('gulp');
6
6
  const path = require('path');
7
7
  const jetpack = require('fs-jetpack');
8
+ const glob = require('glob').globSync;
8
9
 
9
10
  // Load package
10
11
  const package = Manager.getPackage('main');
@@ -14,7 +15,7 @@ const project = Manager.getPackage('project');
14
15
  logger.log('Starting...', argv);
15
16
 
16
17
  // Load tasks
17
- const tasks = jetpack.list(path.join(__dirname, 'tasks'));
18
+ const tasks = glob('*.js', { cwd: `${__dirname}/tasks` });
18
19
 
19
20
  // Init global
20
21
  global.tasks = {};
@@ -47,6 +48,7 @@ exports.build = series(
47
48
  exports.distribute,
48
49
  parallel(exports.sass, exports.webpack, exports.imagemin),
49
50
  exports.jekyll,
51
+ exports.audit,
50
52
  exports.translation,
51
53
  );
52
54
 
@@ -0,0 +1,270 @@
1
+ // Libraries
2
+ const Manager = new (require('../../build.js'));
3
+ const logger = Manager.logger('audit');
4
+ const { series } = require('gulp');
5
+ const glob = require('glob').globSync;
6
+ const path = require('path');
7
+ const jetpack = require('fs-jetpack');
8
+ const spellchecker = require('spellchecker');
9
+ const cheerio = require('cheerio');
10
+ const { HtmlValidate } = require('html-validate');
11
+ const { XMLParser } = require('fast-xml-parser');
12
+
13
+ // Utils
14
+ const collectTextNodes = require('./utils/collectTextNodes');
15
+ const dictionary = require('./utils/dictionary');
16
+ const formatDocument = require('./utils/formatDocument');
17
+
18
+ // Load package
19
+ const package = Manager.getPackage('main');
20
+ const project = Manager.getPackage('project');
21
+ const config = Manager.getConfig('project');
22
+ const rootPathPackage = Manager.getRootPath('main');
23
+ const rootPathProject = Manager.getRootPath('project');
24
+
25
+ // Glob
26
+ const input = [
27
+ // Files to include
28
+ '_site/**/*.{html,xml}',
29
+ ];
30
+ const output = '';
31
+ const delay = 250;
32
+
33
+ // Task
34
+ async function audit(complete) {
35
+ // Log
36
+ logger.log('Starting...');
37
+
38
+ // Quit if NOT in build mode and UJ_FORCE_AUDIT is not true
39
+ if (!Manager.isBuildMode() && process.env.UJ_FORCE_AUDIT !== 'true') {
40
+ logger.log('Skipping audit in development mode');
41
+ return complete();
42
+ }
43
+
44
+ // Perform audit
45
+ await processAudit();
46
+
47
+ // Log
48
+ logger.log('Finished!');
49
+
50
+ // Complete
51
+ return complete();
52
+ };
53
+
54
+ // Default Task
55
+ module.exports = series(audit);
56
+
57
+ async function validateFormat(file, content) {
58
+ // Log
59
+ // logger.log(`➡️ Validating HTML in ${file}`);
60
+
61
+ // Initialize an array to hold formatted messages
62
+ let valid = true;
63
+ let formattedMessages = [];
64
+
65
+ // Get format
66
+ const format = file.endsWith('.html')
67
+ ? 'html'
68
+ : 'xml';
69
+
70
+ // Run pretty validation and HTML/XML validation in parallel
71
+ const [prettyValidationResult, validationResult] = await Promise.all([
72
+ (async () => {
73
+ try {
74
+ // Format the content using Prettier
75
+ const formatted = await formatDocument(content, format, true);
76
+
77
+ // Save the formatted content back to the file
78
+ jetpack.write(file, formatted);
79
+
80
+ return { valid: true, messages: [] };
81
+ } catch (e) {
82
+ return { valid: false, messages: [`[format] ${format.toUpperCase()} is not well-formatted @ ${file} \n${e.message}`] };
83
+ }
84
+ })(),
85
+ (async () => {
86
+ if (format === 'html') {
87
+ const validator = new HtmlValidate({
88
+ root: true,
89
+ extends: ['html-validate:recommended'],
90
+ rules: {
91
+ // Custom rules
92
+ 'no-inline-style': 'error',
93
+ 'void-style': ['error', { style: 'selfclosing' }],
94
+ 'prefer-button': 'warn',
95
+ 'doctype-style': 'error',
96
+ 'no-dup-id': 'error',
97
+
98
+ // Ignore certain rules for this audit
99
+ 'no-conditional-comment': 'off',
100
+ 'no-trailing-whitespace': 'off',
101
+ 'no-inline-style': 'off',
102
+ 'script-type': 'off',
103
+ }
104
+ });
105
+
106
+ const report = await validator.validateString(content);
107
+ const results = report.results[0];
108
+ const messages = results?.messages || [];
109
+
110
+ return {
111
+ valid: report.valid,
112
+ messages: messages.map(msg => {
113
+ return `[${msg.ruleId}] ${msg.message} @ ${file}:${msg.line}:${msg.column} (${msg.ruleUrl})`;
114
+ })
115
+ };
116
+ } else if (format === 'xml') {
117
+ try {
118
+ const parser = new XMLParser({
119
+ ignoreAttributes: false,
120
+ allowBooleanAttributes: true
121
+ });
122
+ parser.parse(content);
123
+ return { valid: true, messages: [] };
124
+ } catch (e) {
125
+ return { valid: false, messages: [`[format] ${format.toUpperCase()} is not well-formatted @ ${file} \n${e.message}`] };
126
+ }
127
+ }
128
+ })()
129
+ ]);
130
+
131
+ // Combine results
132
+ valid = prettyValidationResult.valid && validationResult.valid;
133
+ formattedMessages.push(...prettyValidationResult.messages, ...validationResult.messages);
134
+
135
+ // Return validation result
136
+ return {
137
+ valid,
138
+ messages: formattedMessages,
139
+ };
140
+ }
141
+
142
+ async function validateSpelling(file, content) {
143
+ // Log
144
+ // logger.log(`➡️ Validating spelling in ${file}`);
145
+
146
+ const $ = cheerio.load(content);
147
+ const textNodes = collectTextNodes($);
148
+
149
+ const brand = (config?.brand?.name || 'BrandName').toLowerCase();
150
+
151
+ const misspelledWords = textNodes.flatMap(({ text }) => {
152
+
153
+ // Split text into words using regex
154
+ const words = text.match(/\b[\w’']+\b/g) || [];
155
+
156
+ // Filter out words that are part of the brand name or are not misspelled
157
+ return words
158
+ .filter(word => {
159
+ const lowerWord = word.toLowerCase();
160
+ const baseWord = lowerWord.endsWith("'s") ? lowerWord.slice(0, -2) : lowerWord; // Remove possessive 's if present
161
+
162
+ if (
163
+ baseWord === brand ||
164
+ dictionary.includes(baseWord)
165
+ ) {
166
+ return false;
167
+ }
168
+
169
+ return spellchecker.isMisspelled(word);
170
+ })
171
+ .map(word => {
172
+ // Find the sentence containing the word
173
+ const lines = content.split('\n');
174
+ let lineIndex = 0;
175
+ let column = 0;
176
+
177
+ // Iterate through lines to find the full text
178
+ for (let i = 0; i < lines.length; i++) {
179
+ const line = lines[i];
180
+ const textIndex = line.indexOf(text);
181
+ if (textIndex !== -1) {
182
+ lineIndex = i + 1; // Convert to 1-based index
183
+ column = textIndex + 1; // Convert to 1-based index
184
+ break;
185
+ }
186
+ }
187
+
188
+ return `[spelling] ${word} in "${text}" @ ${file}:${lineIndex}:${column}`;
189
+ });
190
+ });
191
+
192
+ return {
193
+ valid: misspelledWords.length === 0,
194
+ misspelledWords,
195
+ };
196
+ }
197
+
198
+ async function processAudit() {
199
+ const htmlFiles = glob(input, {
200
+ nodir: true,
201
+ ignore: [
202
+ // Auth files
203
+ '_site/__/auth/**/*',
204
+
205
+ // Sitemap
206
+ '**/sitemap.html',
207
+ ]
208
+ });
209
+
210
+ // Run validations in parallel
211
+ const results = await Promise.all(
212
+ htmlFiles.map(async (file) => {
213
+ const content = jetpack.read(file);
214
+
215
+ // Run format and spellcheck in parallel
216
+ const [formatValidation, spellingValidation] = await Promise.all([
217
+ validateFormat(file, content),
218
+ validateSpelling(file, content)
219
+ ]);
220
+
221
+ return {
222
+ file,
223
+ formatValidation,
224
+ spellingValidation
225
+ };
226
+ })
227
+ );
228
+
229
+ // Log results
230
+ const summary = {
231
+ totalFiles: htmlFiles.length,
232
+ validFiles: 0,
233
+ invalidFiles: 0
234
+ };
235
+
236
+ results.forEach(({ file, formatValidation, spellingValidation }) => {
237
+ logger.log(`🔍 Results for file: ${file}`);
238
+
239
+ if (formatValidation.valid) {
240
+ logger.log(`✅ Format validation passed.`);
241
+ } else {
242
+ logger.log(`❌ Format validation failed:`);
243
+ console.log(format(formatValidation.messages));
244
+ }
245
+
246
+ if (spellingValidation.valid) {
247
+ logger.log(`✅ Spelling validation passed.`);
248
+ } else {
249
+ logger.log(`❌ Spelling validation failed:`);
250
+ console.log(format(spellingValidation.misspelledWords));
251
+ }
252
+
253
+ if (formatValidation.valid && spellingValidation.valid) {
254
+ summary.validFiles++;
255
+ } else {
256
+ summary.invalidFiles++;
257
+ }
258
+ });
259
+
260
+ // Log summary
261
+ logger.log('Audit Summary:', summary);
262
+ }
263
+
264
+ function format(messages) {
265
+ if (!Array.isArray(messages)) {
266
+ return messages;
267
+ }
268
+
269
+ return messages.map(msg => `- ${msg}`).join('\n');
270
+ }
@@ -3,6 +3,7 @@ const Manager = new (require('../../build.js'));
3
3
  const logger = Manager.logger('jekyll');
4
4
  const argv = Manager.getArguments();
5
5
  const { series, watch } = require('gulp');
6
+ const glob = require('glob').globSync;
6
7
  const path = require('path');
7
8
  const { execute } = require('node-powertools');
8
9
  const jetpack = require('fs-jetpack');
@@ -48,7 +49,7 @@ async function jekyll(complete) {
48
49
  }
49
50
 
50
51
  // Run buildpre hook
51
- await hook('build:pre', index)
52
+ await hook('build:pre', index);
52
53
 
53
54
  // Build Jekyll
54
55
  const command = [
@@ -76,7 +77,7 @@ async function jekyll(complete) {
76
77
  await execute(command.join(' '), {log: true});
77
78
 
78
79
  // Run buildpost hook
79
- await hook('build:post', index)
80
+ await hook('build:post', index);
80
81
 
81
82
  // Log
82
83
  logger.log('Finished!');
@@ -232,4 +233,3 @@ async function isBrowserTabOpen(url) {
232
233
  return false;
233
234
  }
234
235
  }
235
-
@@ -13,6 +13,10 @@ const { execute, wait } = require('node-powertools');
13
13
  const { Octokit } = require('@octokit/rest')
14
14
  const AdmZip = require('adm-zip') // npm install adm-zip
15
15
 
16
+ // Utils
17
+ const collectTextNodes = require('./utils/collectTextNodes');
18
+ const formatDocument = require('./utils/formatDocument');
19
+
16
20
  // Load package
17
21
  const package = Manager.getPackage('main');
18
22
  const project = Manager.getPackage('project');
@@ -29,11 +33,13 @@ const rootPathProject = Manager.getRootPath('project');
29
33
  // push the updated translation JSON to the branch uj-translations
30
34
 
31
35
  // Settings
32
- const CACHE_DIR = '.temp/translations'
33
- const RECHECK_DAYS = 30
36
+ const CACHE_DIR = '.temp/translations';
37
+ const RECHECK_DAYS = 0;
34
38
  // const AI_MODEL = 'gpt-4.1-nano';
35
39
  const AI_MODEL = 'gpt-4.1-mini';
36
40
  const TRANSLATION_BRANCH = 'uj-translations';
41
+ const LOUD = false;
42
+ // const LOUD = true;
37
43
 
38
44
  const TRANSLATION_DELAY_MS = 500; // wait between each translation
39
45
  const TRANSLATION_BATCH_SIZE = 25; // wait longer every N translations
@@ -58,8 +64,8 @@ async function translation(complete) {
58
64
  // Get ignored pages
59
65
  const ignoredPages = getIgnoredPages();
60
66
 
61
- // Quit if NOT in build mode and UJ_TRANSLATION_FORCE is not true
62
- if (!Manager.isBuildMode() && process.env.UJ_TRANSLATION_FORCE !== 'true') {
67
+ // Quit if NOT in build mode and UJ_FORCE_TRANSLATION is not true
68
+ if (!Manager.isBuildMode() && process.env.UJ_FORCE_TRANSLATION !== 'true') {
63
69
  logger.log('Skipping translation in development mode');
64
70
  return complete();
65
71
  }
@@ -124,9 +130,9 @@ async function processTranslation() {
124
130
  }
125
131
 
126
132
  // Pull latest cached translations from uj-translations branch
127
- if (Manager.isBuildMode()) {
133
+ // if (Manager.isBuildMode()) {
128
134
  await fetchTranslationsBranch();
129
- }
135
+ // }
130
136
 
131
137
  // Get files
132
138
  const allFiles = glob(input, {
@@ -165,36 +171,9 @@ async function processTranslation() {
165
171
  const relativePath = filePath.replace(/^_site[\\/]/, '');
166
172
  const originalHtml = jetpack.read(filePath);
167
173
  const $ = cheerio.load(originalHtml);
168
- const textNodes = [];
169
-
170
- // Get text nodes from body
171
- $('body *').each((_, el) => {
172
- const node = $(el);
173
-
174
- // Skip script tags or any other tags you want to ignore
175
- if (node.is('script')) {
176
- return;
177
- }
178
174
 
179
- // Find text nodes that are not empty
180
- node.contents().each((_, child) => {
181
- if (child.type === 'text' && child.data?.trim()) {
182
- const i = textNodes.length;
183
- const text = child.data
184
- .replace(/^(\s+)\s*/, '$1') // Preserve original leading whitespace
185
- .replace(/\s*(\s+)$/, '$1') // Preserve original trailing whitespace
186
- .replace(/\s+/g, ' ') // Normalize internal whitespace
187
-
188
- // Tag the text node with a unique index
189
- textNodes.push({
190
- node,
191
- originalNode: child,
192
- text: text,
193
- tagged: `[${i}]${text}[/${i}]`,
194
- });
195
- }
196
- });
197
- });
175
+ // Collect text nodes with tags
176
+ const textNodes = collectTextNodes($, { tag: true });
198
177
 
199
178
  // Build body text from tagged nodes
200
179
  const bodyText = textNodes.map(n => n.tagged).join('\n');
@@ -220,8 +199,13 @@ async function processTranslation() {
220
199
  const task = async () => {
221
200
  const meta = metas[lang].meta;
222
201
  const cachePath = path.join(CACHE_DIR, lang, 'pages', relativePath);
223
- const outPath = path.join('_site', lang, relativePath);
202
+ // const outPath = path.join('_site', lang, relativePath);
203
+ const isHomepage = relativePath === 'index.html';
204
+ const outPath = isHomepage
205
+ ? path.join('_site', `${lang}.html`)
206
+ : path.join('_site', lang, relativePath);
224
207
 
208
+ // Log
225
209
  logger.log(`🌐 Translating: ${relativePath} → [${lang}]`);
226
210
 
227
211
  // Skip if the file is not in the meta or if it has no text nodes
@@ -241,6 +225,10 @@ async function processTranslation() {
241
225
  try {
242
226
  const { result, usage } = await translateWithAPI(openAIKey, bodyText, lang);
243
227
 
228
+ // Log
229
+ const elapsedTime = ((Date.now() - startTime) / 1000).toFixed(2);
230
+ logger.log(`✅ Translated: ${relativePath} [${lang}] (Elapsed time: ${elapsedTime}s)`);
231
+
244
232
  // Set translated result
245
233
  translated = result;
246
234
 
@@ -276,28 +264,54 @@ async function processTranslation() {
276
264
  textNodes.forEach((n, i) => {
277
265
  const regex = new RegExp(`\\[${i}\\](.*?)\\[/${i}\\]`, 's');
278
266
  const match = translated.match(regex);
267
+ const translation = match?.[1];
268
+
269
+ if (!translation) {
270
+ return logger.warn(`⚠️ Could not find translated tag for index ${i}`);
271
+ }
279
272
 
280
- if (match && match[1]) {
281
- const translation = match[1];
282
- n.originalNode.data = translation;
283
- logger.log(`${i}: ${n.text} → ${translation}`);
284
- } else {
285
- logger.warn(`⚠️ Could not find translated tag for index ${i}`);
273
+ if (n.type === 'data') {
274
+ n.reference.data = translation;
275
+ } else if (n.type === 'text') {
276
+ n.node.text(translation);
277
+ } else if (n.type === 'attr') {
278
+ n.node.attr(n.attr, translation);
286
279
  }
280
+ if (LOUD) logger.log(`${i}: ${n.text} → ${translation}`);
287
281
  });
288
282
 
289
283
  // Rewrite links
290
284
  rewriteLinks($, lang);
291
285
 
286
+ // Set the lang attribute on the <html> tag
287
+ $('html').attr('lang', lang);
288
+
289
+ // Update <link rel="canonical">
290
+ const canonicalUrl = getCanonicalUrl(lang, relativePath);
291
+ $('link[rel="canonical"]').attr('href', canonicalUrl);
292
+
293
+ // Update <meta property="og:url">
294
+ $('meta[property="og:url"]').attr('content', canonicalUrl);
295
+
296
+ // Insert language tags on this translation
297
+ await insertLanguageTags($, languages, relativePath);
298
+
299
+ // Insert language tags in original file
300
+ await insertLanguageTags(cheerio.load(originalHtml), languages, relativePath, filePath);
301
+
302
+ // Insert language tags in sitemap.xml
303
+ const sitemapPath = path.join('_site', 'sitemap.xml');
304
+ const sitemapXml = jetpack.read(sitemapPath);
305
+ await insertLanguageTags(cheerio.load(sitemapXml, { xmlMode: true }), languages, relativePath, sitemapPath);
306
+
292
307
  // Save output
293
- jetpack.write(outPath, $.html());
294
- logger.log(`✅ Wrote: ${outPath}`);
308
+ jetpack.write(outPath, await formatDocument($.html(), undefined, false));
309
+ // logger.log(`✅ Wrote: ${outPath}`);
295
310
 
296
311
  // Track updated files only if it's new or updated
297
- if (!useCached || !entry || entry.hash !== hash) {
298
- // updatedFiles.add(cachePath);
299
- // updatedFiles.add(metas[lang].path);
300
- }
312
+ // if (!useCached || !entry || entry.hash !== hash) {
313
+ // }
314
+ // Track updated files
301
315
  updatedFiles.add(cachePath);
302
316
  updatedFiles.add(metas[lang].path);
303
317
  };
@@ -360,7 +374,8 @@ async function translateWithAPI(openAIKey, content, lang) {
360
374
  'Content-Type': 'application/json',
361
375
  },
362
376
  timeout: 60000 * 4,
363
- body: JSON.stringify({
377
+ tries: 2,
378
+ body: {
364
379
  // model: 'gpt-4o',
365
380
  model: AI_MODEL,
366
381
  messages: [
@@ -369,7 +384,7 @@ async function translateWithAPI(openAIKey, content, lang) {
369
384
  ],
370
385
  max_tokens: 1024 * 16,
371
386
  temperature: 0.2,
372
- }),
387
+ },
373
388
  });
374
389
 
375
390
  // Get result
@@ -410,13 +425,13 @@ function rewriteLinks($, lang) {
410
425
  || href.startsWith('!#')
411
426
  || href.startsWith('javascript:')
412
427
  ) {
413
- logger.log(`⚠️ Ignoring link: ${href} (empty or invalid)`);
428
+ if (LOUD) logger.log(`⚠️ Ignoring link: ${href} (empty or invalid)`);
414
429
  return;
415
430
  }
416
431
 
417
432
  // Quit early if the URL is external (not part of the current site)
418
433
  if (url.origin !== new URL(baseUrl).origin) {
419
- logger.log(`⚠️ Ignoring external link: ${href} (origin mismatch)`);
434
+ if (LOUD) logger.log(`⚠️ Ignoring external link: ${href} (origin mismatch)`)
420
435
  return;
421
436
  }
422
437
 
@@ -426,7 +441,7 @@ function rewriteLinks($, lang) {
426
441
  ignoredPages.files.includes(relativePath)
427
442
  || ignoredPages.folders.some(folder => relativePath.startsWith(folder + '/'))
428
443
  ) {
429
- logger.log(`⚠️ Ignoring link: ${href} (ignored page)`);
444
+ if (LOUD) logger.log(`⚠️ Ignoring link: ${href} (ignored page)`);
430
445
  return;
431
446
  }
432
447
 
@@ -437,14 +452,81 @@ function rewriteLinks($, lang) {
437
452
  $(el).attr('href', url.toString());
438
453
 
439
454
  // Log the rewritten link
440
- logger.log(`🔗 Rewrote link: ${href} → ${url.toString()}`);
455
+ if (LOUD) logger.log(`🔗 Rewrote link: ${href} → ${url.toString()}`);
441
456
  } catch (error) {
442
457
  // Log an error if the URL is invalid
443
- logger.warn(`⚠️ Invalid URL: ${href} — ${error.message}`);
458
+ if (LOUD) logger.warn(`⚠️ Invalid URL: ${href} — ${error.message}`);
444
459
  }
445
460
  });
446
461
  }
447
462
 
463
+ async function insertLanguageTags($, languages, relativePath, filePath) {
464
+ // Add <link rel="alternate"> tags for all languages
465
+ // Log whether $ is html or xml
466
+ const isHtml = $('html').length > 0;
467
+
468
+ if (isHtml) {
469
+ // Locate the existing language tags
470
+ const existingLanguageTags = $(`head link[rel="alternate"][hreflang="${config?.translation?.default}"]`);
471
+
472
+ // Insert new language tags directly after the existing ones
473
+ if (existingLanguageTags.length) {
474
+ let newLanguageTags = '';
475
+ for (const targetLang of languages) {
476
+ const alternateUrl = getCanonicalUrl(targetLang, relativePath);
477
+
478
+ // Check if the tag already exists
479
+ const tagExists = $(`head link[rel="alternate"][hreflang="${targetLang}"]`).length > 0;
480
+ if (!tagExists) {
481
+ newLanguageTags += `\n<link rel="alternate" href="${alternateUrl}" hreflang="${targetLang}">`;
482
+ }
483
+ }
484
+
485
+ // Insert new tags after the last existing language tag
486
+ existingLanguageTags.last().after(newLanguageTags);
487
+ }
488
+ } else {
489
+ // Locate the existing language tags
490
+ const existingLanguageTags = $(`loc`);
491
+
492
+ // Loop thru loc elements and find one that matches canonical URL
493
+ let matchingLoc = null;
494
+ existingLanguageTags.each((_, loc) => {
495
+ const locUrl = $(loc).text();
496
+
497
+ if (locUrl === getCanonicalUrl(null, relativePath)) {
498
+ matchingLoc = loc;
499
+ }
500
+ });
501
+
502
+ // Insert new language tags after the matching <loc> element
503
+ if (matchingLoc) {
504
+
505
+ let newLanguageTags = '';
506
+ for (const targetLang of languages) {
507
+ const alternateUrl = getCanonicalUrl(targetLang, relativePath);
508
+
509
+ // Check if the tag already exists
510
+ // const tagExists = existingLanguageTags.filter((_, loc) => $(loc).text() === alternateUrl).length > 0;
511
+ const tagExists = $(`xhtml\\:link[rel="alternate"][hreflang="${targetLang}"][href="${alternateUrl}"]`).length > 0;
512
+ if (!tagExists) {
513
+ newLanguageTags += `\n<xhtml:link rel="alternate" hreflang="${targetLang}" href="${alternateUrl}" />`;
514
+ }
515
+ }
516
+
517
+ // Insert new tags after the matching <loc> element
518
+ $(matchingLoc).after(newLanguageTags);
519
+ }
520
+ }
521
+
522
+ // Save the modified HTML back to the file if filePath
523
+ if (filePath) {
524
+ // const format = isHtml ? 'html' : 'xml';
525
+ const format = 'html';
526
+ jetpack.write(filePath, await formatDocument($.html(), format));
527
+ }
528
+ }
529
+
448
530
  function getIgnoredPages() {
449
531
  // Check if socials and downloads exist in the config
450
532
  const languages = config?.translation?.languages || [];
@@ -497,6 +579,7 @@ function getIgnoredPages() {
497
579
 
498
580
  // Other
499
581
  '404',
582
+ 'sitemap',
500
583
 
501
584
  // Redirects
502
585
  ...redirectPermalinks,
@@ -598,7 +681,7 @@ async function fetchTranslationsBranch() {
598
681
  // Get the name of the root folder from the extracted archive
599
682
  const extractedRoot = jetpack.list(extractDir).find(name => name.startsWith(`${owner}-${repo}-`))
600
683
  const extractedFullPath = path.join(extractDir, extractedRoot)
601
- const targetPath = path.join(extractDir, 'translations')
684
+ const targetPath = path.join(extractDir, 'translations');
602
685
 
603
686
  // Remove any existing 'translations' folder and move the extracted folder there
604
687
  if (jetpack.exists(targetPath)) jetpack.remove(targetPath)
@@ -617,7 +700,7 @@ async function pushTranslationBranch(updatedFiles) {
617
700
  // Convert Set to array
618
701
  const files = [...updatedFiles];
619
702
  logger.log(`📤 Pushing ${files.length} updated file(s) to '${TRANSLATION_BRANCH}'`);
620
- console.log(files);
703
+ // console.log(files);
621
704
 
622
705
  // Abort if .temp/translations doesn't exist
623
706
  if (!jetpack.exists(localRoot)) {
@@ -692,6 +775,7 @@ async function fetchOpenAIKey() {
692
775
  const response = await fetch(url, {
693
776
  method: 'GET',
694
777
  response: 'json',
778
+ tries: 2,
695
779
  headers: {
696
780
  'Authorization': `Bearer ${process.env.GH_TOKEN}`,
697
781
  },
@@ -701,7 +785,7 @@ async function fetchOpenAIKey() {
701
785
  });
702
786
 
703
787
  // Log
704
- logger.log('OpenAI API response:', response);
788
+ // logger.log('OpenAI API response:', response);
705
789
 
706
790
  // Return
707
791
  return response.openai.ultimate_jekyll.translation;
@@ -709,3 +793,27 @@ async function fetchOpenAIKey() {
709
793
  logger.error('Error:', error);
710
794
  }
711
795
  }
796
+
797
+ function getCanonicalUrl(lang, relativePath) {
798
+ const baseUrl = Manager.getWorkingUrl();
799
+
800
+ // Remove 'index.html' from the end
801
+ let cleanedPath = relativePath.replace(/index\.html$/, '');
802
+
803
+ // Remove '.html' from the end
804
+ cleanedPath = cleanedPath.replace(/\.html$/, '');
805
+
806
+ // Remove trailing slashes
807
+ cleanedPath = cleanedPath.replace(/\/+$/, '');
808
+
809
+ // Remove leading slashes
810
+ cleanedPath = cleanedPath.replace(/^\/+/, '');
811
+
812
+ // If no language is specified, return the base URL with the cleaned path
813
+ if (!lang) {
814
+ return `${baseUrl}/${cleanedPath}`;
815
+ }
816
+
817
+ // Return
818
+ return `${baseUrl}/${lang}/${cleanedPath}`;
819
+ }
@@ -0,0 +1,94 @@
1
+ const collectTextNodes = ($, options) => {
2
+ const textNodes = [];
3
+
4
+ // Fix options
5
+ options = options || {};
6
+
7
+ $('*').each((_, el) => {
8
+ const node = $(el);
9
+
10
+ // Skip scripts and style tags
11
+ if (
12
+ node.is('script')
13
+ || node.is('style')
14
+ || node.is('noscript') // @TODO: This is not foolproof because there can be text inside <noscript> tags
15
+ ) {
16
+ return;
17
+ }
18
+
19
+ // Handle <title>
20
+ if (node.is('title')) {
21
+ const i = textNodes.length;
22
+ const text = node.text().trim();
23
+ if (text) {
24
+ textNodes.push({
25
+ node,
26
+ type: 'text',
27
+ attr: null,
28
+ text,
29
+ tagged: `[${i}]${text}[/${i}]`,
30
+ line: el.startIndex || 0, // Add line information
31
+ column: 0 // Column is not directly available, default to 0
32
+ });
33
+ }
34
+ return;
35
+ }
36
+
37
+ // Handle meta tags with translatable content
38
+ if (node.is('meta')) {
39
+ const metaSelectors = [
40
+ 'description',
41
+ 'og:title',
42
+ 'og:description',
43
+ 'twitter:title',
44
+ 'twitter:description'
45
+ ];
46
+ const name = node.attr('name');
47
+ const property = node.attr('property');
48
+
49
+ const key = name || property;
50
+ if (metaSelectors.includes(key)) {
51
+ const text = node.attr('content')?.trim();
52
+ if (text) {
53
+ const i = textNodes.length;
54
+ textNodes.push({
55
+ node,
56
+ type: 'attr',
57
+ attr: 'content',
58
+ text,
59
+ tagged: `[${i}]${text}[/${i}]`,
60
+ line: el.startIndex || 0, // Add line information
61
+ column: 0 // Column is not directly available, default to 0
62
+ });
63
+ }
64
+ }
65
+ return;
66
+ }
67
+
68
+ // Handle regular DOM text nodes
69
+ node.contents().each((_, child) => {
70
+ if (child.type === 'text' && child.data?.trim()) {
71
+ const i = textNodes.length;
72
+ const text = child.data
73
+ .replace(/^\s+/, '')
74
+ .replace(/\s+$/, '')
75
+ .replace(/\s+/g, ' ');
76
+
77
+ textNodes.push({
78
+ node,
79
+ type: 'data',
80
+ attr: null,
81
+ reference: child,
82
+ text,
83
+ tagged: `[${i}]${text}[/${i}]`,
84
+ line: el.startIndex || 0, // Add line information
85
+ column: 0 // Column is not directly available, default to 0
86
+ });
87
+ }
88
+ });
89
+ });
90
+
91
+ return textNodes;
92
+ };
93
+
94
+ module.exports = collectTextNodes;
@@ -0,0 +1,50 @@
1
+ module.exports = [
2
+ // Tech words
3
+ 'webhook',
4
+ 'oauth2',
5
+ 'oauth',
6
+ 'api',
7
+
8
+ // Services
9
+ 'firebase',
10
+ 'firestore',
11
+ 'unsplash',
12
+ 'mailchimp',
13
+
14
+ // Auth words
15
+ 'signin',
16
+ 'signout',
17
+ 'signup',
18
+
19
+ // Names
20
+ 'ian',
21
+ 'alex',
22
+ 'raeburn',
23
+
24
+ // Abbreviations
25
+ // Days
26
+ 'mon',
27
+ 'tue',
28
+ 'wed',
29
+ 'thu',
30
+ 'fri',
31
+ 'sat',
32
+ 'sun',
33
+
34
+ // Months
35
+ 'jan',
36
+ 'feb',
37
+ 'mar',
38
+ 'apr',
39
+ 'may',
40
+ 'jun',
41
+ 'jul',
42
+ 'aug',
43
+ 'sep',
44
+ 'oct',
45
+ 'nov',
46
+ 'dec',
47
+
48
+ // Words that should be words
49
+ 'unpublish',
50
+ ]
@@ -0,0 +1,45 @@
1
+ const Manager = new (require('../../../build.js'));
2
+
3
+ const prettier = require('prettier');
4
+ const path = require('path');
5
+
6
+ // Load package
7
+ const rootPathPackage = Manager.getRootPath('main');
8
+
9
+ module.exports = async function formatHTML(content, format, throwError) {
10
+ // Set default format to 'html' if not provided
11
+ format = format || 'html';
12
+ throwError = typeof throwError === 'undefined' ? true : throwError;
13
+
14
+ // Setup Prettier options
15
+ const options = {
16
+ parser: format,
17
+ };
18
+
19
+ // If formatting XML, load plugin from UJ's node_modules
20
+ if (format === 'xml') {
21
+ options.plugins = [
22
+ require.resolve('@prettier/plugin-xml', {
23
+ paths: [rootPathPackage],
24
+ }),
25
+ ];
26
+ options.xmlWhitespaceSensitivity = 'ignore';
27
+ }
28
+
29
+ // Process the content with Prettier
30
+ return prettier
31
+ .format(content, options)
32
+ .then((formatted) => {
33
+ return removeMultipleNewlines(formatted);
34
+ })
35
+ .catch((e) => {
36
+ if (throwError) {
37
+ throw e;
38
+ }
39
+ return removeMultipleNewlines(content);
40
+ });
41
+ };
42
+
43
+ function removeMultipleNewlines(content) {
44
+ return content.replace(/\n\s*\n+/g, '\n');
45
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ultimate-jekyll-manager",
3
- "version": "0.0.36",
3
+ "version": "0.0.37",
4
4
  "description": "Ultimate Jekyll dependency manager",
5
5
  "main": "dist/index.js",
6
6
  "exports": {
@@ -56,34 +56,39 @@
56
56
  "sharp": "0.23.1 (sometime before 2021ish): Hard lock because later versions had issues. Possibly solved in higher node versions"
57
57
  },
58
58
  "dependencies": {
59
- "@babel/core": "^7.27.1",
59
+ "@babel/core": "^7.27.4",
60
60
  "@babel/preset-env": "^7.27.2",
61
61
  "@fortawesome/fontawesome-free": "^6.7.2",
62
62
  "@octokit/rest": "^22.0.0",
63
63
  "@popperjs/core": "^2.11.8",
64
+ "@prettier/plugin-xml": "^3.4.1",
64
65
  "adm-zip": "^0.5.16",
65
66
  "babel-loader": "^10.0.0",
66
67
  "browser-sync": "^3.0.4",
67
68
  "chalk": "^4.1.2",
68
69
  "cheerio": "^1.1.0",
70
+ "fast-xml-parser": "^5.2.5",
69
71
  "fs-jetpack": "^5.1.0",
70
- "glob": "^11.0.2",
72
+ "glob": "^11.0.3",
71
73
  "gulp-clean-css": "^4.3.0",
72
74
  "gulp-filter": "^9.0.1",
73
75
  "gulp-rename": "^2.0.0",
74
76
  "gulp-responsive-modern": "^1.0.0",
75
77
  "gulp-sass": "^6.0.1",
78
+ "html-validate": "^9.7.0",
76
79
  "itwcw-package-analytics": "^1.0.6",
77
80
  "js-yaml": "^4.1.0",
78
81
  "json5": "^2.2.3",
79
82
  "lodash": "^4.17.21",
80
- "minimatch": "^10.0.1",
83
+ "minimatch": "^10.0.3",
81
84
  "node-powertools": "^2.3.1",
82
85
  "npm-api": "^1.0.1",
83
- "sass": "^1.89.0",
86
+ "prettier": "^3.5.3",
87
+ "sass": "^1.89.2",
88
+ "spellchecker": "^3.7.1",
84
89
  "through2": "^4.0.2",
85
90
  "web-manager": "^3.2.73",
86
- "webpack": "^5.99.8",
91
+ "webpack": "^5.99.9",
87
92
  "wonderful-fetch": "^1.3.3",
88
93
  "wonderful-version": "^1.3.2",
89
94
  "yargs": "^17.7.2"