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 +20 -0
- package/dist/cli.js +3 -0
- package/dist/commands/audit.js +15 -0
- package/dist/commands/translation.js +15 -0
- package/dist/defaults/dist/_includes/main/global/body.html +33 -0
- package/dist/defaults/dist/_includes/main/global/head.html +42 -77
- package/dist/defaults/dist/_layouts/main/global/default.html +4 -1
- package/dist/defaults/dist/feeds/posts.xml +11 -2
- package/dist/defaults/dist/sitemap.xml +6 -1
- package/dist/gulp/main.js +3 -1
- package/dist/gulp/tasks/audit.js +270 -0
- package/dist/gulp/tasks/jekyll.js +3 -3
- package/dist/gulp/tasks/translation.js +166 -58
- package/dist/gulp/tasks/utils/collectTextNodes.js +94 -0
- package/dist/gulp/tasks/utils/dictionary.js +50 -0
- package/dist/gulp/tasks/utils/formatDocument.js +45 -0
- package/package.json +11 -6
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">×</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">×</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">×</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="
|
|
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">×</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">×</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">×</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
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
62
|
-
if (!Manager.isBuildMode() && process.env.
|
|
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
|
-
|
|
180
|
-
|
|
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 (
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
} else {
|
|
285
|
-
|
|
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
|
-
|
|
299
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
83
|
+
"minimatch": "^10.0.3",
|
|
81
84
|
"node-powertools": "^2.3.1",
|
|
82
85
|
"npm-api": "^1.0.1",
|
|
83
|
-
"
|
|
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.
|
|
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"
|