@fulldecent/nice-checkers-plugin 0.1.0

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/LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2025 William Entriken
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all
11
+ copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,458 @@
1
+ # :cherry_blossom: Nice Checkers
2
+
3
+ [![npm version](https://badge.fury.io/js/html-validate-nice-checkers.svg)](https://www.npmjs.com/package/html-validate-nice-checkers)
4
+
5
+ [![CI](https://github.com/fulldecent/html-validate-nice-checkers/actions/workflows/ci.yml/badge.svg)](https://github.com/yourusername/html-validate-nice-checkers/actions/workflows/ci.yml)
6
+
7
+ [![codecov](https://codecov.io/gh/fulldecent/html-validate-nice-checkers/branch/main/graph/badge.svg)](https://codecov.io/gh/yourusername/html-validate-nice-checkers)
8
+
9
+ An opinionated collection of essential HTML validation rules that promote best practices™ for web development. Use this plugin with [HTML-validate](https://html-validate.org/).
10
+
11
+ ## Installation
12
+
13
+ These instructions assume you are using Nice Checkers as part of a web test suite running Node (20+) and HTML-proofer. See [github-pages-template](https://html-validate.org/) for an end-to-end example, including GitHub Actions continuous integration and GitHub Pages deployment.
14
+
15
+ ### Add package dev dependency
16
+
17
+ _This is a dev dependency for you because it required to test your website, not to build it._
18
+
19
+ ```sh
20
+ # Using Yarn
21
+ yarn add -D html-validate-nice-checkers
22
+
23
+ # Using npm
24
+ npm install --dev html-validate-nice-checkers
25
+ ```
26
+
27
+ Update your .htmlvalidate.json configuration
28
+
29
+ ```diff
30
+ {
31
+ "plugins": ["dist"],
32
+ - "extends": ["htmlvalidate:recommended"]
33
+ + "extends": ["htmlvalidate:recommended", "nice-checkers-plugin:recommended"]
34
+ }
35
+ ```
36
+
37
+ ### Usage and configuration
38
+
39
+ You can configure individual rules:
40
+
41
+ ```json
42
+ {
43
+ "plugins": ["nice-checkers"],
44
+ "extends": ["html-validate:recommended", "nice-checkers-plugin:recommended"],
45
+ "rules": {
46
+ "nice-checkers/canonical-link": "error",
47
+ "nice-checkers/external-links": [
48
+ "error",
49
+ {
50
+ "skipUrlPatterns": ["example.com", "localhost"],
51
+ "cacheExpiryFoundSeconds": 2592000
52
+ }
53
+ ],
54
+ "nice-checkers/https-links": "warn",
55
+ "nice-checkers/internal-links": [
56
+ "error",
57
+ {
58
+ "webRoot": "./dist",
59
+ "alternativeExtensions": [".html"]
60
+ }
61
+ ],
62
+ "nice-checkers/latest-packages": "warn",
63
+ "nice-checkers/mailto-awesome": [
64
+ "error",
65
+ {
66
+ "requiredParameters": ["subject", "body"]
67
+ }
68
+ ],
69
+ "nice-checkers/no-jquery": "error"
70
+ }
71
+ }
72
+ ```
73
+
74
+ ## Rules
75
+
76
+ All rules are enabled by default when you extend from `nice-checkers-plugin:recommended`.
77
+
78
+ ### `nice-checkers/canonical-link`
79
+
80
+ Ensures that each HTML document contains a single canonical link element pointing to the preferred URL for that page. Additionally, this requires that your canonical URL does not have a `.html` suffix as that is deprecated usage.
81
+
82
+ **Bad:**
83
+
84
+ ```html
85
+ <!doctype html>
86
+ <html lang="en">
87
+ <head>
88
+ <meta charset="utf-8" />
89
+ <title>My first website about horses</title>
90
+ <!-- Missing canonical link -->
91
+ </head>
92
+ ody>
93
+ This page is missing a required canonical link element in the head.
94
+ </body>
95
+ </html>
96
+ ```
97
+
98
+ **Good:**
99
+
100
+ ```html
101
+ <!doctype html>
102
+ <html lang="en">
103
+ <head>
104
+ <meta charset="utf-8" />
105
+ <title>My first website about horses</title>
106
+ <link rel="canonical" href="https://example.com/horses" />
107
+ </head>
108
+ <body>
109
+ This page has a proper canonical link element.
110
+ </body>
111
+ </html>
112
+ ```
113
+
114
+ **Configuration:**
115
+
116
+ ```json
117
+ {
118
+ "rules": {
119
+ "nice-checkers/canonical-link": "error"
120
+ }
121
+ }
122
+ ```
123
+
124
+ **Common violations:**
125
+
126
+ - Missing `<link rel="canonical">` in `<head>`
127
+ - Canonical URL has file extension (`.html`, `.php`, etc.)
128
+ - Canonical URL ends with `/index`
129
+
130
+ ### `nice-checkers/external-links`
131
+
132
+ Validates that all external links are live and accessible. Uses caching to avoid repeated requests to the same URLs.
133
+
134
+ **Bad:**
135
+
136
+ ```html
137
+ <!-- Broken external link -->
138
+ <a href="https://example.com/nonexistent-page">This link is broken</a>
139
+
140
+ <!-- Link that redirects -->
141
+ <a href="http://old-domain.example.com/page">This redirects</a>
142
+ ```
143
+
144
+ **Good:**
145
+
146
+ ```html
147
+ <!-- Working external link -->
148
+ <a href="https://example.com/working-page">This link works</a>
149
+ ```
150
+
151
+ **Configuration:**
152
+
153
+ ```json
154
+ {
155
+ "rules": {
156
+ "nice-checkers/external-links": [
157
+ "error",
158
+ {
159
+ "proxyUrl": "",
160
+ "skipUrlPatterns": ["example.com", "localhost"],
161
+ "cacheExpiryFoundSeconds": 2592000,
162
+ "cacheExpiryNotFoundSeconds": 259200,
163
+ "timeoutSeconds": 5,
164
+ "cacheDatabasePath": "cache/external-links.db",
165
+ "userAgent": "Mozilla/5.0 (compatible; html-validate-nice-checkers)"
166
+ }
167
+ ]
168
+ }
169
+ }
170
+ ```
171
+
172
+ **Options:**
173
+
174
+ - `skipUrlPatterns`: Array of regex patterns for URLs to skip checking
175
+ - `cacheExpiryFoundSeconds`: Cache duration for successful checks (default: 30 days)
176
+ - `cacheExpiryNotFoundSeconds`: Cache duration for failed checks (default: 3 days)
177
+ - `timeoutSeconds`: Request timeout (default: 5 seconds)
178
+
179
+ ### `nice-checkers/https-links`
180
+
181
+ Reports insecure HTTP links that are accessible via HTTPS, encouraging the use of secure connections.
182
+
183
+ **Bad:**
184
+
185
+ ```html
186
+ <!-- HTTP link that's also available over HTTPS -->
187
+ <a href="http://secure-site.com/page">Should use HTTPS</a>
188
+ <img src="http://cdn.example.com/image.jpg" alt="Image" />
189
+ ```
190
+
191
+ **Good:**
192
+
193
+ ``tml
194
+
195
+ <!-- HTTPS link -->
196
+
197
+ <a href="https://secure-site.com/page">Uses HTTPS</a>
198
+ <img src="https://cdn.example.com/image.jpg" alt="Image" />
199
+
200
+ ````
201
+
202
+ **Configuration:**
203
+
204
+ ```json
205
+ {
206
+ "rules": {
207
+ "nice-checkers/https-links": [
208
+ "warn",
209
+ {
210
+ "cacheExpiryFoundSeconds": 2592000,
211
+ "cacheExpiryNotFoundSeconds": 259200,
212
+ "timeoutSeconds": 10,
213
+ "cacheDatabasePath": "cache/https-availability.db"
214
+ }
215
+ ]
216
+ }
217
+ }
218
+ ````
219
+
220
+ ### `nice-checkers/internal-links`
221
+
222
+ Validates that all internal links point to existing files in your project.
223
+
224
+ **Bad:**
225
+
226
+ ```html
227
+ <!-- Link to non-existent page -->
228
+ <a href="/nonexistent-page">Broken internal link</a>
229
+
230
+ <!-- Link to non-existent image -->
231
+ <img src="../images/missing.jpg" alt="Missing image" />
232
+ ```
233
+
234
+ **Good:**
235
+
236
+ ```html
237
+ <!-- Link to existing page -->
238
+ <a href="/about">About page</a>
239
+
240
+ <!-- Link to existing image -->
241
+ <img src="../images/logo.jpg" alt="Company logo" />
242
+ ```
243
+
244
+ **Configuration:**
245
+
246
+ ```json
247
+ {
248
+ "rules": {
249
+ "nice-checkers/internal-links": [
250
+ "error",
251
+ {
252
+ "webRoot": "./build",
253
+ "alternativeExtensions": [".html", ".php"],
254
+ "indexFile": "index.html"
255
+ }
256
+ ]
257
+ }
258
+ }
259
+ ```
260
+
261
+ **Options:**
262
+
263
+ - `webRoot`: Root directory for resolving absolute links
264
+ - `alternativeExtensions`: Extensions to check for extensionless links
265
+ - `indexFile`: Default file to look for in directory links
266
+
267
+ ### `nice-checkers/latest-packages`
268
+
269
+ Ensures that package assets loaded from CDNs (like jsDelivr) are using the latest version and have proper SRI attributes.
270
+
271
+ **Bad:**
272
+
273
+ ````html
274
+ <!-- Outdated package without SRI -->
275
+ <script src="https://cdn.jsdelivr.net/npm/bootstrap@4.6.0/dist/js/bootstrap.min.js"></script>
276
+
277
+ <!-- Missing integrity and crossorigin -->
278
+ <link
279
+ rel="stylesheet"
280
+ href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css"
281
+ />`` **Good:** ```html
282
+ <!-- Latest package with SRI -->
283
+ <script
284
+ src="https://cdn.jsdelivr.net/npm/bootstrap@latest/dist/js/bootstrap.min.js"
285
+ integrity="sha384-..."
286
+ crossorigin="anonymous"
287
+ ></script>
288
+
289
+ <link
290
+ rel="stylesheet"
291
+ href="https://cdn.jsdelivr.net/npm/bootstrap@latest/dist/css/bootstrap.min.css"
292
+ integrity="sha384-..."
293
+ crossorigin="anonymous"
294
+ />
295
+ ````
296
+
297
+ **Configuration:**
298
+
299
+ ```json
300
+ {
301
+ "rules": {
302
+ "nice-checkers/latest-packages": [
303
+ "warn",
304
+ {
305
+ "cacheExpirySeconds": 172800,
306
+ "timeoutSeconds": 10,
307
+ "cacheDatabasePath": "cache/latest-packages.db",
308
+ "skipUrlPatterns": ["googletagmanager.com"]
309
+ }
310
+ ]
311
+ }
312
+ }
313
+ ```
314
+
315
+ ### `nice-checkers/mailto-awesome`
316
+
317
+ Enforces that mailto links contain specific parameters to improve user experience.
318
+
319
+ **Bad:**
320
+
321
+ ```html
322
+ <!-- Mailto without required parameters -->
323
+ <a href="mailto:contact@example.com">Send email</a>
324
+ ```
325
+
326
+ **Good:**
327
+
328
+ ```html
329
+ <! Mailto with subject and body -->
330
+ <a
331
+ href="mailto:contact@example.com?subject=Website%20Inquiry&body=Hello,%20I%20would%20like%20to..."
332
+ >
333
+ Send email
334
+ </a>
335
+ ```
336
+
337
+ **Configuration:**
338
+
339
+ ```json
340
+ {
341
+ "rules": {
342
+ "nice-checkers/mailto-awesome": [
343
+ "error",
344
+ {
345
+ "requiredParameters": ["subject", "body"]
346
+ }
347
+ ]
348
+ }
349
+ }
350
+ ```
351
+
352
+ **Options:**
353
+
354
+ - `requiredParameters`: Array of parameters that must be present (e.g., `["subject", "body", "cc"]`)
355
+
356
+ ### `nice-checkers/no-jquery`
357
+
358
+ Disallows the use of jQuery, encouraging modern vanilla JavaScript or more modern frameworks.
359
+
360
+ **Bad:**
361
+
362
+ ```html
363
+ <!-- jQuery script tag -->
364
+ <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
365
+ <script src="../js/jquery.min.js"></script>
366
+ ```
367
+
368
+ **Good:**
369
+
370
+ ``tml
371
+
372
+ <!-- Modern JavaScript or other libraries -->
373
+ <script src="https://unpkg.com/htmx.org@1.8.4"></script>
374
+ <script src="../js/vanilla-app.js"></script>
375
+
376
+ ````
377
+
378
+ ## Development
379
+
380
+ This package is built with TypeScript and supports both ESM and CommonJS module systems.
381
+
382
+ ### Setup
383
+
384
+ ```sh
385
+ # Clone the repository
386
+ git clone https://github.com/yourusername/html-validate-nice-checkers.git
387
+ cd html-validate-nice-checkers
388
+
389
+ # Setup Node, for example using nvm
390
+ nvm use
391
+
392
+ # Enable Yarn Berry
393
+ corepack enable
394
+
395
+ # Install dependencies
396
+ yarn install
397
+ ````
398
+
399
+ ### Hint: VS Code setup for Yarn Berry
400
+
401
+ These notes are [from the Yarn project](https://yarnpkg.com/getting-started/editor-sdks#).
402
+
403
+ ```sh
404
+ yarn dlx @yarnpkg/sdks vscode
405
+ ```
406
+
407
+ and YES use workspace TypeScript version.
408
+
409
+ ### Development Scripts
410
+
411
+ ```sh
412
+ # Build the package
413
+ yarn build
414
+
415
+ # Run tests
416
+ yarn test
417
+
418
+ # Run tests in watch mode
419
+ yarn test:watch
420
+
421
+ # Run tests with coverage
422
+ yarn test:coverage
423
+
424
+ # Type checking
425
+ yarn lint
426
+
427
+ # Build in watch mode
428
+ yarn build:watch
429
+ ```
430
+
431
+ ## Publishing
432
+
433
+ The package uses GitHub Actions for CI/CD:
434
+
435
+ 1. **Tests** run on all versions of Node.js that the Node.js project actively supports (now 20+)
436
+ 2. **Package validation** tests ESM/CJS imports
437
+ 3. **Publishing** to npm on tagged releases
438
+
439
+ ## Package features
440
+
441
+ - âś… **Dual Module Support**: Works with both ESM (`import`) and CJS (`require`)
442
+ - âś… **TypeScript**: Full type definitions included
443
+ - âś… **Tree Shakeable**: Import only what you need
444
+ - âś… **Zero Dependencies**: No runtime dependencies
445
+ - âś… **Modern Tooling**: Built with tsup, tested with Vitest
446
+ - âś… **Comprehensive Testing**: High test coverage with realistic fixtures
447
+
448
+ ## Browser support
449
+
450
+ This is a Node.js library designed for build-time HTML validation. For browser usage, ensure your bundler supports the module format you're using.
451
+
452
+ ## Contributing
453
+
454
+ Ensure your changes pass `yarn test`, `yarn lint` and `yarn format`.
455
+
456
+ ## Deploying
457
+
458
+ @fulldecent will deploy by making GitHub releases, this triggers the release workflow.
package/dist/index.cjs ADDED
@@ -0,0 +1,22 @@
1
+ 'use strict';var htmlValidate=require('html-validate'),L=require('better-sqlite3'),p=require('fs'),child_process=require('child_process'),shellQuote=require('shell-quote'),d=require('path');function _interopDefault(e){return e&&e.__esModule?e:{default:e}}var L__default=/*#__PURE__*/_interopDefault(L);var p__default=/*#__PURE__*/_interopDefault(p);var d__default=/*#__PURE__*/_interopDefault(d);var U=Object.defineProperty;var o=(R,i)=>U(R,"name",{value:i,configurable:true});var k=class k extends htmlValidate.Rule{documentation(){return {description:"Requires a <link rel='canonical'> in the <head> with a specific href format.",url:"https://github.com/fulldecent/html-validate-nice-checkers/blob/main/README.m#rules"}}setup(){this.on("dom:ready",i=>{this.domReady(i);});}domReady(i){let{document:e}=i,t=e.querySelector('head > link[rel="canonical"]');if(t===null){this.report({node:e.querySelector("head"),message:'<head> is missing <link rel="canonical" ...>'});return}let r=t.getAttribute("href")?.value;if(typeof r!="string"||!r){this.report({node:t,message:"<link rel='canonical'> is missing a non-empty href attribute"});return}/\.\w+$/.test(r)&&this.report({node:t,message:"Canonical link href should be extensionless (no .html, .php, etc.)"}),r.toLowerCase().endsWith("/index")&&this.report({node:t,message:'Canonical link href should be "/" instead of ending with "/index"'});}};o(k,"CanonicalLinkRule");var h=k;var j={proxyUrl:"",skipUrls:[],cacheExpiryFoundSeconds:720*60*60,cacheExpiryNotFoundSeconds:4320*60,timeoutSeconds:5,cacheDatabasePath:"cache/external-links.db",userAgent:"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.9999.999 Safari/537.36"};function P(R){try{let i=new URL(R);return i.hostname=i.hostname.toLowerCase(),i.toString()}catch{return R}}o(P,"normalizeUrl");var S=class S extends htmlValidate.Rule{constructor(e){super({...j,...e});this.skipUrlsRegex=[];this.skipUrlsRegex=this.initializeSkipRegex();}static schema(){return {proxyUrl:{type:"string",description:"URL of a proxy server to check external links."},skipUrls:{type:"array",items:{type:"string"},description:"An array of regex patterns (as strings) for URLs to skip."},cacheExpiryFoundSeconds:{type:"number",description:"Number of seconds to cache successful (2xx) responses."},cacheExpiryNotFoundSeconds:{type:"number",description:"Number of seconds to cache failed (non-2xx) responses."},timeoutSeconds:{type:"number",description:"Maximum time in seconds to wait for a response."},cacheDatabasePath:{type:"string",description:"File path for the SQLite cache database."},userAgent:{type:"string",description:"User-Agent string to use for requests."}}}documentation(){return {description:"Require all external links to be live.",url:"https://github.com/fulldecent/html-validate-nice-checkers/blob/main/README.m#rules"}}setup(){this.db=this.setupDatabase(),this.on("tag:ready",e=>this.tagReady(e));}setupDatabase(){let e=d__default.default.dirname(this.options.cacheDatabasePath);p__default.default.mkdirSync(e,{recursive:true});let t=new L__default.default(this.options.cacheDatabasePath);return t.exec(`
2
+ CREATE TABLE IF NOT EXISTS urls (url TEXT UNIQUE NOT NULL, status INTEGER, redirect_to TEXT, time INTEGER);
3
+ CREATE INDEX IF NOT EXISTS time_idx ON urls (time);
4
+ `),t.exec(`DELETE FROM urls WHERE status >= 200 AND status < 300 AND time < unixepoch() - ${this.options.cacheExpiryFoundSeconds}`),t.exec(`DELETE FROM urls WHERE (status < 200 OR status >= 300) AND time < unixepoch() - ${this.options.cacheExpiryNotFoundSeconds}`),t}initializeSkipRegex(){let e=this.options?.skipUrls;return Array.isArray(e)?e.map(t=>{try{return new RegExp(t)}catch{return console.error(`[html-validate-nice-checkers] Invalid regex pattern in 'skipUrls' configuration: "${t}"`),null}}).filter(t=>t!==null):[]}performCheck(e,t,r){let s=P(e),a=shellQuote.quote([r??e]),n=`curl --head --silent --max-time ${this.options.timeoutSeconds} --max-redirs 0 --user-agent "${this.options.userAgent}" --header "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9" --write-out "%{http_code}" --dump-header - --output /dev/null ${a} || true`,u=child_process.execSync(n).toString(),c=u.match(/(\d{3})$/m),l=c&&c[1]?parseInt(c[1],10):500,x=u.match(/^Location: (.+)/im),v=x&&x[1]?x[1].trim():null;this.db.prepare("REPLACE INTO urls (url, status, redirect_to, time) VALUES (?, ?, ?, unixepoch())").run(s,l,v),(l<200||l>=300)&&(v?this.report({node:t,message:`External link ${e} redirects to: ${v}`}):this.report({node:t,message:`External link is broken with status ${l}: ${e}`}));}tagReady(e){let{target:t}=e,{tagName:r}=t,s=null;if(r==="a"||r==="link")s="href";else if(r==="script"||r==="img")s="src";else return;let a=t.getAttribute(s)?.value;if(typeof a!="string"||!a)return;let n=a.replace(/&amp;/g,"&").replace(/&lt;/g,"<").replace(/&gt;/g,">");if(!n.startsWith("http")||this.skipUrlsRegex.some(l=>l.test(n)))return;let u=P(n),c=this.db.prepare("SELECT * FROM urls WHERE url = ?").get(u);if(c){if(c.status>=200&&c.status<300)return;c.redirect_to?this.report({node:t,message:`External link ${n} redirects to: ${c.redirect_to}`}):this.report({node:t,message:`External link is broken with status ${c.status}: ${n}`});return}this.options.proxyUrl?this.performCheck(n,t,`${this.options.proxyUrl}?url=${encodeURIComponent(n)}`):this.performCheck(n,t);}};o(S,"ExternalLinksRule");var m=S;var _={cacheExpiryFoundSeconds:720*60*60,cacheExpiryNotFoundSeconds:4320*60,timeoutSeconds:10,cacheDatabasePath:"cache/https-availability.db"},T=class T extends htmlValidate.Rule{constructor(i){super({..._,...i});}static schema(){return {cacheExpiryFoundSeconds:{type:"number",description:"Number of seconds to cache that a URL is available over HTTPS."},cacheExpiryNotFoundSeconds:{type:"number",description:"Number of seconds to cache that a URL is NOT available over HTTPS."},timeoutSeconds:{type:"number",description:"Maximum time in seconds to wait for a response when checking for HTTPS."},cacheDatabasePath:{type:"string",description:"File path for the SQLite cache database."}}}documentation(){return {description:"Report insecure HTTP links that are accessible via HTTPS.",url:"https://github.com/fulldecent/html-validate-nice-checkers/blob/main/README.m#rules"}}setup(){this.db=this.setupDatabase(),this.on("tag:ready",i=>this.tagReady(i));}setupDatabase(){let i=d__default.default.dirname(this.options.cacheDatabasePath);p__default.default.mkdirSync(i,{recursive:true});let e=new L__default.default(this.options.cacheDatabasePath);return e.exec(`
5
+ CREATE TABLE IF NOT EXISTS urls (
6
+ url TEXT UNIQUE NOT NULL,
7
+ found INTEGER NOT NULL,
8
+ time INTEGER NOT NULL
9
+ );
10
+ CREATE INDEX IF NOT EXISTS time_idx ON urls (time);
11
+ `),e.exec(`DELETE FROM urls WHERE found = 1 AND time < unixepoch() - ${this.options.cacheExpiryFoundSeconds}`),e.exec(`DELETE FROM urls WHERE found = 0 AND time < unixepoch() - ${this.options.cacheExpiryNotFoundSeconds}`),e}performHttpsCheck(i,e){let t=i.replace(/^http:/,"https:"),r=shellQuote.quote([t]),s=`curl --head --silent --fail --location --max-time ${this.options.timeoutSeconds} ${r} > /dev/null 2>&1`;try{child_process.execSync(s),this.db.prepare("REPLACE INTO urls (url, found, time) VALUES (?, 1, unixepoch())").run(i),this.report({node:e,message:`Insecure link can be upgraded to HTTPS: ${i}`});}catch{this.db.prepare("REPLACE INTO urls (url, found, time) VALUES (?, 0, unixepoch())").run(i);}}tagReady(i){let{target:e}=i,{tagName:t}=e,r=null;if(t==="a"||t==="link")r="href";else if(t==="script"||t==="img")r="src";else return;let s=e.getAttribute(r)?.value;if(typeof s!="string"||!s.startsWith("http://"))return;let a=s.replace(/&amp;/g,"&"),n=this.db.prepare("SELECT found FROM urls WHERE url = ?").get(a);if(n){n.found===1&&this.report({node:e,message:`Insecure link can be upgraded to HTTPS: ${a}`});return}this.performHttpsCheck(a,e);}};o(T,"HttpsLinksRule");var g=T;var V={webRoot:d__default.default.join(process.cwd(),"build"),alternativeExtensions:[".html",".php"],indexFile:"index.html"},D=class D extends htmlValidate.Rule{constructor(e){super({...V,...e});this.fileExistsCache=new Map;}static schema(){return {webRoot:{type:"string",description:'The root directory for resolving absolute links (starting with "/").'},alternativeExtensions:{type:"array",items:{type:"string"},description:'List of file extensions to check for extensionless links (e.g., [".html"]).'},indexFile:{type:"string",description:"The file to look for in directory links (e.g., 'index.html')."}}}documentation(){return {description:"Validate that all internal links point to existing files.",url:"https://github.com/fulldecent/html-validate-nice-checkers/blob/main/README.m#rules"}}setup(){this.on("tag:ready",e=>this.tagReady(e));}doesFileExist(e){if(this.fileExistsCache.has(e))return this.fileExistsCache.get(e);let t=p__default.default.existsSync(e)&&p__default.default.lstatSync(e).isFile();return this.fileExistsCache.set(e,t),t}checkLink(e,t){let r;if(e.startsWith("/"))r=d__default.default.join(this.options.webRoot,e);else {let s=d__default.default.dirname(t.location.filename);r=d__default.default.resolve(s,e);}if(e.endsWith("/")){let s=r;if(p__default.default.existsSync(s)&&p__default.default.lstatSync(s).isDirectory()){let a=d__default.default.join(s,this.options.indexFile);if(this.doesFileExist(a))return}}else {if(this.doesFileExist(r)||this.options.alternativeExtensions.some(s=>this.doesFileExist(r+s)))return;if(p__default.default.existsSync(r)&&p__default.default.lstatSync(r).isDirectory()){let s=d__default.default.join(r,this.options.indexFile);if(this.doesFileExist(s))return}}this.report({node:t,message:`Internal link to "${e}" is broken.`});}tagReady(e){let{target:t}=e,{tagName:r}=t,s=null;if(r==="a"||r==="link")s="href";else if(r==="script"||r==="img")s="src";else return;let a=t.getAttribute(s)?.value;if(typeof a!="string"||!a)return;let n=a.replace(/[#?].*$/,"");n&&(/^[a-z][a-z0-9+.-]*:/i.test(n)||this.checkLink(n,t));}};o(D,"InternalLinksRule");var f=D;var te={cacheExpirySeconds:2880*60,timeoutSeconds:10,cacheDatabasePath:"cache/latest-packages.db",skipUrlPatterns:["googletagmanager.com"]},N=class N extends htmlValidate.Rule{constructor(i){super({...te,...i});}static schema(){return {cacheExpirySeconds:{type:"number",description:"Number of seconds to cache package version lookup results."},timeoutSeconds:{type:"number",description:"Maximum time in seconds to wait for a response from the package registry API."},cacheDatabasePath:{type:"string",description:"File path for the SQLite cache database."},skipUrlPatterns:{type:"array",items:{type:"string"},description:"An array of substrings to identify URLs that should be ignored."}}}documentation(){return {description:"Ensures that package assets loaded from a CDN are the latest version and have SRI attributes.",url:"https://github.com/fulldecent/html-validate-nice-checkers/blob/main/README.m#rules"}}setup(){this.db=this.setupDatabase(),this.on("tag:ready",i=>this.tagReady(i));}setupDatabase(){let i=d__default.default.dirname(this.options.cacheDatabasePath);p__default.default.mkdirSync(i,{recursive:true});let e=new L__default.default(this.options.cacheDatabasePath);return e.exec(`
12
+ CREATE TABLE IF NOT EXISTS latest_packages (
13
+ url TEXT UNIQUE NOT NULL,
14
+ current INTEGER NOT NULL,
15
+ time INTEGER NOT NULL
16
+ );
17
+ CREATE INDEX IF NOT EXISTS time_idx ON latest_packages (time);
18
+ `),e.exec(`DELETE FROM latest_packages WHERE time < unixepoch() - ${this.options.cacheExpirySeconds}`),e}performPackageCheck(i,e){let t=i.match(/cdn\.jsdelivr\.net\/npm\/(@?[^/]+)@([^/]+)/);if(!t)return;let r=t[1],s=t[2],a=`https://data.jsdelivr.com/v1/package/npm/${r}`,n=shellQuote.quote([a]),u=`curl --silent --fail --location --max-time ${this.options.timeoutSeconds} ${n}`;try{let c=child_process.execSync(u).toString(),l=JSON.parse(c);l&&l.tags&&Object.values(l.tags).includes(s)?this.db.prepare("REPLACE INTO latest_packages (url, current, time) VALUES (?, 1, unixepoch())").run(i):(this.db.prepare("REPLACE INTO latest_packages (url, current, time) VALUES (?, 0, unixepoch())").run(i),this.report({node:e,message:`Package "${r}" is not using a current version tag. Found "${s}", but latest is "${l.tags.latest}".`}));}catch(c){console.error(`[html-validate-latest-packages] Error checking package version for ${i}: ${c.message}`);}}tagReady(i){let{target:e}=i,{tagName:t}=i.target,r;if(t==="script"){let a=e.getAttribute("src")?.value;typeof a=="string"&&(r=a);}else if(t==="link"&&e.getAttribute("rel")?.value==="stylesheet"){let a=e.getAttribute("href")?.value;typeof a=="string"&&(r=a);}else return;if(!r||!r.startsWith("http")||this.options.skipUrlPatterns.some(a=>r.includes(a)))return;(!e.hasAttribute("integrity")||!e.hasAttribute("crossorigin"))&&this.report({node:e,message:`Package resource is missing required "integrity" and "crossorigin" attributes: ${r}`});let s=this.db.prepare("SELECT current FROM latest_packages WHERE url = ?").get(r);if(s){s.current===0&&this.report({node:e,message:`Package is using an outdated version: ${r}`});return}this.performPackageCheck(r,e);}};o(N,"LatestPackagesRule");var b=N;var ie={requiredParameters:["subject","body"]},O=class O extends htmlValidate.Rule{constructor(i){super({...ie,...i});}static schema(){return {requiredParameters:{type:"array",items:{type:"string"},description:'An array of parameters that must be present in a mailto: link (e.g., ["subject", "body"]).'}}}documentation(){return {description:"Enforce that mailto: links contain specific parameters.",url:"https://github.com/fulldecent/html-validate-nice-checkers/blob/main/README.m#rules"}}setup(){this.on("tag:ready",i=>this.tagReady(i));}tagReady(i){let{target:e}=i;if(e.tagName!=="a")return;let t=e.getAttribute("href")?.value;if(!(typeof t!="string"||!t)&&t.startsWith("mailto:"))try{let r=new URL(t),s=this.options.requiredParameters.filter(a=>!r.searchParams.has(a));s.length>0&&this.report({node:e,message:`mailto: link is missing required parameters: ${s.join(", ")}`});}catch{this.report({node:e,message:"mailto: link is malformed and could not be parsed."});}}};o(O,"MailtoAwesomeRule");var E=O;var A=class A extends htmlValidate.Rule{documentation(){return {description:"Disallow script tag with src including jQuery",url:"https://github.com/fulldecent/html-validate-nice-checkers/blob/main/README.m#rules"}}setup(){this.on("tag:ready",i=>{let{target:e}=i;if(e.tagName!=="script")return;let t=e.getAttribute("src")?.value;typeof t!="string"||!t||t.toLowerCase().includes("jquery")&&this.report({node:e,message:"Script tag with src including jQuery is not allowed"});});}};o(A,"NoJqueryRule");var y=A;var ae={name:"nice-checkers-plugin",rules:{"nice-checkers/canonical-link":h,"nice-checkers/external-links":m,"nice-checkers/https-links":g,"nice-checkers/internal-links":f,"nice-checkers/latest-packages":b,"nice-checkers/mailto-awesome":E,"nice-checkers/no-jquery":y},configs:{recommended:{rules:{"nice-checkers/canonical-link":["error"],"nice-checkers/external-links":["error"],"nice-checkers/https-links":["error"],"nice-checkers/internal-links":["error"],"nice-checkers/latest-packages":["warn"],"nice-checkers/mailto-awesome":["error"],"nice-checkers/no-jquery":["error"]}}}},Ye=ae;
19
+ module.exports=Ye;//# sourceMappingURL=index.cjs.map
20
+
21
+ module.exports = exports.default;
22
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/rules/CanonicalLinkRule.ts","../src/rules/ExternalLinksRule.ts","../src/rules/HttpsLinksRule.ts","../src/rules/InternalLinksRule.ts","../src/rules/LatestPackagesRule.ts","../src/rules/MailtoAwesomeRule.ts","../src/rules/NoJqueryRule.ts","../src/index.ts"],"names":["_CanonicalLinkRule","Rule","event","document","linkCanonical","href","__name","CanonicalLinkRule","defaults","normalizeUrl","url","urlObj","_ExternalLinksRule","options","dir","path","fs","db","Database","patterns","pattern","regex","originalUrl","element","requestUrl","normalizedUrl","escapedUrl","shellEscape","command","output","execSync","statusCodeMatch","statusCode","locationMatch","redirectTo","target","tagName","urlAttribute","rawUrl","row","ExternalLinksRule","_HttpsLinksRule","httpsUrl","HttpsLinksRule","_InternalLinksRule","filePath","exists","internalLink","resolvedPath","basePath","directoryPath","indexPath","ext","InternalLinksRule","_LatestPackagesRule","match","packageName","packageVersion","apiUrl","result","data","e","src","LatestPackagesRule","_MailtoAwesomeRule","missingParams","param","MailtoAwesomeRule","_NoJqueryRule","NoJqueryRule","plugin","index_default"],"mappings":"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","file":"index.cjs","sourcesContent":["import { type RuleDocumentation, type DOMReadyEvent, Rule } from 'html-validate'\n\nexport default class CanonicalLinkRule extends Rule {\n public override documentation(): RuleDocumentation {\n return {\n description: \"Requires a <link rel='canonical'> in the <head> with a specific href format.\",\n url: 'https://github.com/fulldecent/html-validate-nice-checkers/blob/main/README.m#rules',\n }\n }\n\n public override setup(): void {\n // When the DOM is fully parsed, run the domReady method.\n this.on('dom:ready', (event: DOMReadyEvent) => {\n this.domReady(event)\n })\n }\n\n /**\n * Helper method to validate the document's canonical link.\n *\n * @param event - The dom:ready event object.\n */\n private domReady(event: DOMReadyEvent): void {\n const { document } = event\n const linkCanonical = document.querySelector('head > link[rel=\"canonical\"]')\n\n if (linkCanonical === null) {\n // Report error if the canonical link is missing entirely.\n this.report({\n node: document.querySelector('head')!,\n message: '<head> is missing <link rel=\"canonical\" ...>',\n })\n return\n }\n\n // Safely get the href attribute's value. It will be undefined if href is missing.\n const href = linkCanonical.getAttribute('href')?.value\n if (typeof href !== 'string' || !href) {\n this.report({\n node: linkCanonical,\n message: \"<link rel='canonical'> is missing a non-empty href attribute\",\n })\n return\n }\n\n // Check for file extensions like .html, .php, etc.\n if (/\\.\\w+$/.test(href)) {\n this.report({\n node: linkCanonical,\n message: 'Canonical link href should be extensionless (no .html, .php, etc.)',\n })\n }\n\n // Check if the link unnecessarily ends with /index.\n if (href.toLowerCase().endsWith('/index')) {\n this.report({\n node: linkCanonical,\n message: 'Canonical link href should be \"/\" instead of ending with \"/index\"',\n })\n }\n }\n}\n","import {\n type RuleDocumentation,\n type TagReadyEvent,\n type HtmlElement,\n type SchemaObject,\n Rule,\n} from 'html-validate'\nimport Database from 'better-sqlite3'\nimport fs from 'node:fs'\nimport { execSync } from 'node:child_process'\nimport { quote as shellEscape } from 'shell-quote'\nimport path from 'node:path'\n\ninterface UrlCacheRow {\n url: string\n status: number\n redirect_to: string | null\n time: number\n}\n\ninterface RuleOptions {\n proxyUrl: string\n skipUrls: string[]\n cacheExpiryFoundSeconds: number\n cacheExpiryNotFoundSeconds: number\n timeoutSeconds: number\n cacheDatabasePath: string\n userAgent: string\n}\n\nconst defaults: RuleOptions = {\n proxyUrl: '',\n skipUrls: [],\n cacheExpiryFoundSeconds: 30 * 24 * 60 * 60, // Default: 30 days\n cacheExpiryNotFoundSeconds: 3 * 24 * 60 * 60, // Default: 3 days\n timeoutSeconds: 5,\n cacheDatabasePath: 'cache/external-links.db',\n userAgent:\n 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.9999.999 Safari/537.36',\n}\n\nfunction normalizeUrl(url: string): string {\n try {\n const urlObj = new URL(url)\n urlObj.hostname = urlObj.hostname.toLowerCase()\n return urlObj.toString()\n } catch (e) {\n return url\n }\n}\n\nexport default class ExternalLinksRule extends Rule<void, RuleOptions> {\n private readonly skipUrlsRegex: RegExp[] = []\n private db!: Database.Database\n\n public constructor(options: Partial<RuleOptions>) {\n /* assign default values if not provided by user */\n super({ ...defaults, ...options })\n this.skipUrlsRegex = this.initializeSkipRegex()\n }\n\n public static override schema(): SchemaObject {\n return {\n proxyUrl: {\n type: 'string',\n description: 'URL of a proxy server to check external links.',\n },\n skipUrls: {\n type: 'array',\n items: {\n type: 'string',\n },\n description: 'An array of regex patterns (as strings) for URLs to skip.',\n },\n cacheExpiryFoundSeconds: {\n type: 'number',\n description: 'Number of seconds to cache successful (2xx) responses.',\n },\n cacheExpiryNotFoundSeconds: {\n type: 'number',\n description: 'Number of seconds to cache failed (non-2xx) responses.',\n },\n timeoutSeconds: {\n type: 'number',\n description: 'Maximum time in seconds to wait for a response.',\n },\n cacheDatabasePath: {\n type: 'string',\n description: 'File path for the SQLite cache database.',\n },\n userAgent: {\n type: 'string',\n description: 'User-Agent string to use for requests.',\n },\n }\n }\n\n public override documentation(): RuleDocumentation {\n return {\n description: 'Require all external links to be live.',\n url: 'https://github.com/fulldecent/html-validate-nice-checkers/blob/main/README.m#rules',\n }\n }\n\n public override setup(): void {\n this.db = this.setupDatabase()\n this.on('tag:ready', (event: TagReadyEvent) => this.tagReady(event))\n }\n\n private setupDatabase(): Database.Database {\n const dir = path.dirname(this.options.cacheDatabasePath)\n fs.mkdirSync(dir, { recursive: true })\n\n const db = new Database(this.options.cacheDatabasePath)\n db.exec(`\n CREATE TABLE IF NOT EXISTS urls (url TEXT UNIQUE NOT NULL, status INTEGER, redirect_to TEXT, time INTEGER);\n CREATE INDEX IF NOT EXISTS time_idx ON urls (time);\n `)\n\n db.exec(\n `DELETE FROM urls WHERE status >= 200 AND status < 300 AND time < unixepoch() - ${this.options.cacheExpiryFoundSeconds}`\n )\n db.exec(\n `DELETE FROM urls WHERE (status < 200 OR status >= 300) AND time < unixepoch() - ${this.options.cacheExpiryNotFoundSeconds}`\n )\n return db\n }\n\n private initializeSkipRegex(): RegExp[] {\n const patterns = this.options?.skipUrls\n if (!Array.isArray(patterns)) {\n return []\n }\n return patterns\n .map(pattern => {\n try {\n return new RegExp(pattern)\n } catch (e) {\n console.error(\n `[html-validate-nice-checkers] Invalid regex pattern in 'skipUrls' configuration: \"${pattern}\"`\n )\n return null\n }\n })\n .filter((regex): regex is RegExp => regex !== null)\n }\n\n private performCheck(originalUrl: string, element: HtmlElement, requestUrl?: string): void {\n const normalizedUrl = normalizeUrl(originalUrl)\n const escapedUrl = shellEscape([requestUrl ?? originalUrl])\n\n const command = `curl --head --silent --max-time ${this.options.timeoutSeconds} --max-redirs 0 \\\n --user-agent \"${this.options.userAgent}\" \\\n --header \"Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9\" \\\n --write-out \"%{http_code}\" --dump-header - --output /dev/null ${escapedUrl} || true`\n\n const output = execSync(command).toString()\n const statusCodeMatch = output.match(/(\\d{3})$/m)\n const statusCode =\n statusCodeMatch && statusCodeMatch[1] ? parseInt(statusCodeMatch[1], 10) : 500\n const locationMatch = output.match(/^Location: (.+)/im)\n const redirectTo = locationMatch && locationMatch[1] ? locationMatch[1].trim() : null\n this.db\n .prepare('REPLACE INTO urls (url, status, redirect_to, time) VALUES (?, ?, ?, unixepoch())')\n .run(normalizedUrl, statusCode, redirectTo)\n if (statusCode < 200 || statusCode >= 300) {\n if (redirectTo) {\n this.report({\n node: element,\n message: `External link ${originalUrl} redirects to: ${redirectTo}`,\n })\n } else {\n this.report({\n node: element,\n message: `External link is broken with status ${statusCode}: ${originalUrl}`,\n })\n }\n }\n }\n\n private tagReady(event: TagReadyEvent): void {\n const { target } = event\n const { tagName } = target\n\n let urlAttribute: string | null = null\n if (tagName === 'a' || tagName === 'link') {\n urlAttribute = 'href'\n } else if (tagName === 'script' || tagName === 'img') {\n urlAttribute = 'src'\n } else {\n return\n }\n\n const rawUrl = target.getAttribute(urlAttribute)?.value\n if (typeof rawUrl !== 'string' || !rawUrl) {\n return\n }\n\n const url = rawUrl.replace(/&amp;/g, '&').replace(/&lt;/g, '<').replace(/&gt;/g, '>')\n\n if (!url.startsWith('http')) {\n return\n }\n\n if (this.skipUrlsRegex.some(regex => regex.test(url))) {\n return\n }\n\n const normalizedUrl = normalizeUrl(url)\n const row = this.db.prepare('SELECT * FROM urls WHERE url = ?').get(normalizedUrl) as\n | UrlCacheRow\n | undefined\n\n if (row) {\n if (row.status >= 200 && row.status < 300) {\n return\n }\n if (row.redirect_to) {\n this.report({\n node: target,\n message: `External link ${url} redirects to: ${row.redirect_to}`,\n })\n } else {\n this.report({\n node: target,\n message: `External link is broken with status ${row.status}: ${url}`,\n })\n }\n return\n }\n\n if (this.options.proxyUrl) {\n this.performCheck(url, target, `${this.options.proxyUrl}?url=${encodeURIComponent(url)}`)\n } else {\n this.performCheck(url, target)\n }\n }\n}\n","import {\n type RuleDocumentation,\n type TagReadyEvent,\n type HtmlElement,\n type SchemaObject,\n Rule,\n} from 'html-validate'\nimport Database from 'better-sqlite3'\nimport fs from 'node:fs'\nimport { execSync } from 'node:child_process'\nimport { quote as shellEscape } from 'shell-quote'\nimport path from 'node:path'\n\ninterface HttpsCacheRow {\n url: string\n found: 0 | 1\n time: number\n}\n\ninterface RuleOptions {\n cacheExpiryFoundSeconds: number\n cacheExpiryNotFoundSeconds: number\n timeoutSeconds: number\n cacheDatabasePath: string\n}\n\nconst defaults: RuleOptions = {\n cacheExpiryFoundSeconds: 30 * 24 * 60 * 60, // Default: 30 days\n cacheExpiryNotFoundSeconds: 3 * 24 * 60 * 60, // Default: 3 days\n timeoutSeconds: 10,\n cacheDatabasePath: 'cache/https-availability.db',\n}\n\nexport default class HttpsLinksRule extends Rule<void, RuleOptions> {\n private db!: Database.Database\n\n public constructor(options: Partial<RuleOptions>) {\n /* assign default values if not provided by user */\n super({ ...defaults, ...options })\n }\n\n public static override schema(): SchemaObject {\n return {\n cacheExpiryFoundSeconds: {\n type: 'number',\n description: 'Number of seconds to cache that a URL is available over HTTPS.',\n },\n cacheExpiryNotFoundSeconds: {\n type: 'number',\n description: 'Number of seconds to cache that a URL is NOT available over HTTPS.',\n },\n timeoutSeconds: {\n type: 'number',\n description: 'Maximum time in seconds to wait for a response when checking for HTTPS.',\n },\n cacheDatabasePath: {\n type: 'string',\n description: 'File path for the SQLite cache database.',\n },\n }\n }\n\n public override documentation(): RuleDocumentation {\n return {\n description: 'Report insecure HTTP links that are accessible via HTTPS.',\n url: 'https://github.com/fulldecent/html-validate-nice-checkers/blob/main/README.m#rules',\n }\n }\n\n public override setup(): void {\n this.db = this.setupDatabase()\n this.on('tag:ready', (event: TagReadyEvent) => this.tagReady(event))\n }\n\n private setupDatabase(): Database.Database {\n const dir = path.dirname(this.options.cacheDatabasePath)\n fs.mkdirSync(dir, { recursive: true })\n\n const db = new Database(this.options.cacheDatabasePath)\n db.exec(`\n CREATE TABLE IF NOT EXISTS urls (\n url TEXT UNIQUE NOT NULL,\n found INTEGER NOT NULL,\n time INTEGER NOT NULL\n );\n CREATE INDEX IF NOT EXISTS time_idx ON urls (time);\n `)\n\n db.exec(\n `DELETE FROM urls WHERE found = 1 AND time < unixepoch() - ${this.options.cacheExpiryFoundSeconds}`\n )\n db.exec(\n `DELETE FROM urls WHERE found = 0 AND time < unixepoch() - ${this.options.cacheExpiryNotFoundSeconds}`\n )\n return db\n }\n\n private performHttpsCheck(url: string, element: HtmlElement): void {\n const httpsUrl = url.replace(/^http:/, 'https:')\n const escapedUrl = shellEscape([httpsUrl])\n\n // Use --fail to make curl exit with an error on 4xx/5xx responses.\n const command = `curl --head --silent --fail --location --max-time ${this.options.timeoutSeconds} ${escapedUrl} > /dev/null 2>&1`\n\n try {\n execSync(command)\n // If execSync does NOT throw, the command was successful (exit code 0).\n // This means the URL is available over HTTPS.\n this.db.prepare('REPLACE INTO urls (url, found, time) VALUES (?, 1, unixepoch())').run(url)\n this.report({\n node: element,\n message: `Insecure link can be upgraded to HTTPS: ${url}`,\n })\n } catch (error) {\n // If execSync throws, the command failed.\n // This means the URL is NOT available over HTTPS. Cache this result to avoid re-checking.\n this.db.prepare('REPLACE INTO urls (url, found, time) VALUES (?, 0, unixepoch())').run(url)\n }\n }\n\n private tagReady(event: TagReadyEvent): void {\n const { target } = event\n const { tagName } = target\n\n let urlAttribute: string | null = null\n if (tagName === 'a' || tagName === 'link') {\n urlAttribute = 'href'\n } else if (tagName === 'script' || tagName === 'img') {\n urlAttribute = 'src'\n } else {\n return\n }\n\n const rawUrl = target.getAttribute(urlAttribute)?.value\n if (typeof rawUrl !== 'string' || !rawUrl.startsWith('http://')) {\n return\n }\n\n // A simple decoder. More complex entities would require a library.\n const url = rawUrl.replace(/&amp;/g, '&')\n\n const row = this.db.prepare('SELECT found FROM urls WHERE url = ?').get(url) as\n | Pick<HttpsCacheRow, 'found'>\n | undefined\n\n if (row) {\n if (row.found === 1) {\n // Cache hit: we already know it's upgradable, so report it.\n this.report({\n node: target,\n message: `Insecure link can be upgraded to HTTPS: ${url}`,\n })\n }\n // If row.found is 0, we know it's not upgradable, so we do nothing.\n return\n }\n\n // If not in cache, perform the live check.\n this.performHttpsCheck(url, target)\n }\n}\n","import {\n type RuleDocumentation,\n type TagReadyEvent,\n type HtmlElement,\n type SchemaObject,\n Rule,\n} from 'html-validate'\nimport fs from 'node:fs'\nimport path from 'node:path'\n\ninterface RuleOptions {\n webRoot: string\n alternativeExtensions: string[]\n indexFile: string\n}\n\nconst defaults: RuleOptions = {\n webRoot: path.join(process.cwd(), 'build'),\n alternativeExtensions: ['.html', '.php'],\n indexFile: 'index.html',\n}\n\nexport default class InternalLinksRule extends Rule<void, RuleOptions> {\n private readonly fileExistsCache = new Map<string, boolean>()\n\n public constructor(options: Partial<RuleOptions>) {\n /* assign default values if not provided by user */\n super({ ...defaults, ...options })\n }\n\n public static override schema(): SchemaObject {\n return {\n webRoot: {\n type: 'string',\n description: 'The root directory for resolving absolute links (starting with \"/\").',\n },\n alternativeExtensions: {\n type: 'array',\n items: {\n type: 'string',\n },\n description: 'List of file extensions to check for extensionless links (e.g., [\".html\"]).',\n },\n indexFile: {\n type: 'string',\n description: \"The file to look for in directory links (e.g., 'index.html').\",\n },\n }\n }\n\n public override documentation(): RuleDocumentation {\n return {\n description: 'Validate that all internal links point to existing files.',\n url: 'https://github.com/fulldecent/html-validate-nice-checkers/blob/main/README.m#rules',\n }\n }\n\n public override setup(): void {\n this.on('tag:ready', (event: TagReadyEvent) => this.tagReady(event))\n }\n\n private doesFileExist(filePath: string): boolean {\n if (this.fileExistsCache.has(filePath)) {\n return this.fileExistsCache.get(filePath) as boolean\n }\n\n const exists = fs.existsSync(filePath) && fs.lstatSync(filePath).isFile()\n this.fileExistsCache.set(filePath, exists)\n return exists\n }\n\n private checkLink(internalLink: string, element: HtmlElement): void {\n let resolvedPath: string\n\n // Resolve absolute paths from the web root, relative paths from the current file's location.\n if (internalLink.startsWith('/')) {\n resolvedPath = path.join(this.options.webRoot, internalLink)\n } else {\n const basePath = path.dirname(element.location.filename)\n resolvedPath = path.resolve(basePath, internalLink)\n }\n\n // Case 1: Link ends with '/', check for an index file in that directory.\n if (internalLink.endsWith('/')) {\n const directoryPath = resolvedPath\n if (fs.existsSync(directoryPath) && fs.lstatSync(directoryPath).isDirectory()) {\n const indexPath = path.join(directoryPath, this.options.indexFile)\n if (this.doesFileExist(indexPath)) {\n return // Link is valid.\n }\n }\n } else {\n // Case 2: Link does not end with '/'.\n // Check for a direct file match or a file with an alternative extension.\n if (this.doesFileExist(resolvedPath)) {\n return // Direct file match is valid.\n }\n if (this.options.alternativeExtensions.some(ext => this.doesFileExist(resolvedPath + ext))) {\n return // Extensionless link is valid.\n }\n\n // Also check if it's a directory containing an index file.\n // E.g., a link to \"/about\" can resolve to \"/about/index.html\".\n if (fs.existsSync(resolvedPath) && fs.lstatSync(resolvedPath).isDirectory()) {\n const indexPath = path.join(resolvedPath, this.options.indexFile)\n if (this.doesFileExist(indexPath)) {\n return // Directory link is valid.\n }\n }\n }\n\n // If no valid file was found after all checks, report the error.\n this.report({\n node: element,\n message: `Internal link to \"${internalLink}\" is broken.`,\n })\n }\n\n private tagReady(event: TagReadyEvent): void {\n const { target } = event\n const { tagName } = target\n\n let urlAttribute: string | null = null\n if (tagName === 'a' || tagName === 'link') {\n urlAttribute = 'href'\n } else if (tagName === 'script' || tagName === 'img') {\n urlAttribute = 'src'\n } else {\n return\n }\n\n const rawUrl = target.getAttribute(urlAttribute)?.value\n if (typeof rawUrl !== 'string' || !rawUrl) {\n return\n }\n\n // Strip fragment and query parameters.\n const url = rawUrl.replace(/[#?].*$/, '')\n if (!url) {\n return\n }\n\n // Skip absolute URLs.\n // An internal link does not have a protocol (http:, mailto:, etc.).\n if (/^[a-z][a-z0-9+.-]*:/i.test(url)) {\n return\n }\n\n // The URL is internal, now check if it's valid.\n this.checkLink(url, target)\n }\n}\n","import {\n type RuleDocumentation,\n type TagReadyEvent,\n type HtmlElement,\n type SchemaObject,\n Rule,\n} from 'html-validate'\nimport Database from 'better-sqlite3'\nimport fs from 'node:fs'\nimport { execSync } from 'node:child_process'\nimport { quote as shellEscape } from 'shell-quote'\nimport path from 'node:path'\n\ninterface PackageCacheRow {\n url: string\n current: 0 | 1\n time: number\n}\n\ninterface RuleOptions {\n cacheExpirySeconds: number\n timeoutSeconds: number\n cacheDatabasePath: string\n skipUrlPatterns: string[]\n}\n\nconst defaults: RuleOptions = {\n cacheExpirySeconds: 2 * 24 * 60 * 60, // Default: 2 days\n timeoutSeconds: 10,\n cacheDatabasePath: 'cache/latest-packages.db',\n skipUrlPatterns: ['googletagmanager.com'],\n}\n\nexport default class LatestPackagesRule extends Rule<void, RuleOptions> {\n private db!: Database.Database\n\n public constructor(options: Partial<RuleOptions>) {\n /* assign default values if not provided by user */\n super({ ...defaults, ...options })\n }\n\n public static override schema(): SchemaObject {\n return {\n cacheExpirySeconds: {\n type: 'number',\n description: 'Number of seconds to cache package version lookup results.',\n },\n timeoutSeconds: {\n type: 'number',\n description:\n 'Maximum time in seconds to wait for a response from the package registry API.',\n },\n cacheDatabasePath: {\n type: 'string',\n description: 'File path for the SQLite cache database.',\n },\n skipUrlPatterns: {\n type: 'array',\n items: {\n type: 'string',\n },\n description: 'An array of substrings to identify URLs that should be ignored.',\n },\n }\n }\n\n public override documentation(): RuleDocumentation {\n return {\n description:\n 'Ensures that package assets loaded from a CDN are the latest version and have SRI attributes.',\n url: 'https://github.com/fulldecent/html-validate-nice-checkers/blob/main/README.m#rules',\n }\n }\n\n public override setup(): void {\n this.db = this.setupDatabase()\n this.on('tag:ready', (event: TagReadyEvent) => this.tagReady(event))\n }\n\n private setupDatabase(): Database.Database {\n const dir = path.dirname(this.options.cacheDatabasePath)\n fs.mkdirSync(dir, { recursive: true })\n\n const db = new Database(this.options.cacheDatabasePath)\n db.exec(`\n CREATE TABLE IF NOT EXISTS latest_packages (\n url TEXT UNIQUE NOT NULL,\n current INTEGER NOT NULL,\n time INTEGER NOT NULL\n );\n CREATE INDEX IF NOT EXISTS time_idx ON latest_packages (time);\n `)\n\n db.exec(\n `DELETE FROM latest_packages WHERE time < unixepoch() - ${this.options.cacheExpirySeconds}`\n )\n return db\n }\n\n private performPackageCheck(url: string, element: HtmlElement): void {\n // Robustly parse jsDelivr NPM URLs, supporting scoped packages.\n const match = url.match(/cdn\\.jsdelivr\\.net\\/npm\\/(@?[^/]+)@([^/]+)/)\n if (!match) {\n return // Not a jsDelivr NPM URL we can check.\n }\n\n const packageName = match[1]\n const packageVersion = match[2]\n const apiUrl = `https://data.jsdelivr.com/v1/package/npm/${packageName}`\n const escapedUrl = shellEscape([apiUrl])\n const command = `curl --silent --fail --location --max-time ${this.options.timeoutSeconds} ${escapedUrl}`\n\n try {\n const result = execSync(command).toString()\n const data = JSON.parse(result)\n\n if (data && data.tags && Object.values(data.tags).includes(packageVersion)) {\n // The version in the URL is a valid tag (e.g., 'latest', 'beta', or a specific version tag).\n this.db\n .prepare('REPLACE INTO latest_packages (url, current, time) VALUES (?, 1, unixepoch())')\n .run(url)\n } else {\n // The version is not a recognized tag, so it's likely outdated or incorrect.\n this.db\n .prepare('REPLACE INTO latest_packages (url, current, time) VALUES (?, 0, unixepoch())')\n .run(url)\n this.report({\n node: element,\n message: `Package \"${packageName}\" is not using a current version tag. Found \"${packageVersion}\", but latest is \"${data.tags.latest}\".`,\n })\n }\n } catch (e: any) {\n // Log errors related to the check itself (e.g., network issues) but don't fail validation.\n console.error(\n `[html-validate-latest-packages] Error checking package version for ${url}: ${e.message}`\n )\n }\n }\n\n private tagReady(event: TagReadyEvent): void {\n const { target } = event\n const { tagName } = event.target\n let url: string | undefined\n\n if (tagName === 'script') {\n const src = target.getAttribute('src')?.value\n if (typeof src === 'string') {\n url = src\n }\n } else if (tagName === 'link' && target.getAttribute('rel')?.value === 'stylesheet') {\n const href = target.getAttribute('href')?.value\n if (typeof href === 'string') {\n url = href\n }\n } else {\n return\n }\n if (\n !url ||\n !url.startsWith('http') ||\n this.options.skipUrlPatterns.some(pattern => url!.includes(pattern))\n ) {\n return\n }\n\n // Enforce Subresource Integrity (SRI)\n if (!target.hasAttribute('integrity') || !target.hasAttribute('crossorigin')) {\n this.report({\n node: target,\n message: `Package resource is missing required \"integrity\" and \"crossorigin\" attributes: ${url}`,\n })\n }\n\n const row = this.db.prepare('SELECT current FROM latest_packages WHERE url = ?').get(url) as\n | Pick<PackageCacheRow, 'current'>\n | undefined\n\n if (row) {\n if (row.current === 0) {\n // Cache hit: we already know it's outdated, report it.\n this.report({\n node: target,\n message: `Package is using an outdated version: ${url}`,\n })\n }\n return // Cache hit, do nothing more.\n }\n\n // If not in cache, perform the live check.\n this.performPackageCheck(url, target)\n }\n}\n","import { type RuleDocumentation, type TagReadyEvent, type SchemaObject, Rule } from 'html-validate'\n\ninterface RuleOptions {\n requiredParameters: string[]\n}\n\nconst defaults: RuleOptions = {\n requiredParameters: ['subject', 'body'],\n}\n\nexport default class MailtoAwesomeRule extends Rule<void, RuleOptions> {\n public constructor(options: Partial<RuleOptions>) {\n /* assign default values if not provided by user */\n super({ ...defaults, ...options })\n }\n\n public static override schema(): SchemaObject {\n return {\n requiredParameters: {\n type: 'array',\n items: {\n type: 'string',\n },\n description:\n 'An array of parameters that must be present in a mailto: link (e.g., [\"subject\", \"body\"]).',\n },\n }\n }\n\n public override documentation(): RuleDocumentation {\n return {\n description: 'Enforce that mailto: links contain specific parameters.',\n url: 'https://github.com/fulldecent/html-validate-nice-checkers/blob/main/README.m#rules',\n }\n }\n\n public override setup(): void {\n this.on('tag:ready', (event: TagReadyEvent) => this.tagReady(event))\n }\n\n private tagReady(event: TagReadyEvent): void {\n const { target } = event\n\n // This rule only applies to <a> tags.\n if (target.tagName !== 'a') {\n return\n }\n\n const href = target.getAttribute('href')?.value\n if (typeof href !== 'string' || !href) {\n return\n }\n if (!href.startsWith('mailto:')) {\n return\n }\n\n try {\n const url = new URL(href)\n const missingParams = this.options.requiredParameters.filter(param => {\n return !url.searchParams.has(param)\n })\n\n if (missingParams.length > 0) {\n this.report({\n node: target,\n message: `mailto: link is missing required parameters: ${missingParams.join(', ')}`,\n })\n }\n } catch (e) {\n // Handle cases where the mailto: URL is malformed and cannot be parsed.\n this.report({\n node: target,\n message: 'mailto: link is malformed and could not be parsed.',\n })\n }\n }\n}\n","import { type TagReadyEvent, Rule, type RuleDocumentation } from 'html-validate'\n\nexport default class NoJqueryRule extends Rule {\n public override documentation(): RuleDocumentation {\n return {\n description: 'Disallow script tag with src including jQuery',\n url: 'https://github.com/fulldecent/html-validate-nice-checkers/blob/main/README.m#rules',\n }\n }\n\n public override setup(): void {\n this.on('tag:ready', (event: TagReadyEvent) => {\n const { target } = event\n if (target.tagName !== 'script') {\n return\n }\n const src = target.getAttribute('src')?.value\n if (typeof src !== 'string' || !src) {\n return\n }\n if (!src.toLowerCase().includes('jquery')) {\n return\n }\n this.report({\n node: target,\n message: 'Script tag with src including jQuery is not allowed',\n })\n })\n }\n}\n","/**\n * Nice Checkers plugin\n * A collection of essential HTML validation rules\n */\n\nimport type { Plugin } from 'html-validate'\nimport CanonicalLinkRule from './rules/CanonicalLinkRule'\nimport ExternalLinksRule from './rules/ExternalLinksRule'\nimport HttpsLinksRule from './rules/HttpsLinksRule'\nimport InternalLinksRule from './rules/InternalLinksRule'\nimport LatestPackagesRule from './rules/LatestPackagesRule'\nimport MailtoAwesomeRule from './rules/MailtoAwesomeRule'\nimport NoJqueryRule from './rules/NoJqueryRule'\n\nconst plugin: Plugin = {\n name: 'nice-checkers-plugin',\n\n rules: {\n 'nice-checkers/canonical-link': CanonicalLinkRule,\n 'nice-checkers/external-links': ExternalLinksRule,\n 'nice-checkers/https-links': HttpsLinksRule,\n 'nice-checkers/internal-links': InternalLinksRule,\n 'nice-checkers/latest-packages': LatestPackagesRule,\n 'nice-checkers/mailto-awesome': MailtoAwesomeRule,\n 'nice-checkers/no-jquery': NoJqueryRule,\n },\n\n configs: {\n recommended: {\n rules: {\n 'nice-checkers/canonical-link': ['error'],\n 'nice-checkers/external-links': ['error'],\n 'nice-checkers/https-links': ['error'],\n 'nice-checkers/internal-links': ['error'],\n 'nice-checkers/latest-packages': ['warn'],\n 'nice-checkers/mailto-awesome': ['error'],\n 'nice-checkers/no-jquery': ['error'],\n },\n },\n },\n}\n\nexport default plugin\n"]}
@@ -0,0 +1,10 @@
1
+ import { Plugin } from 'html-validate';
2
+
3
+ /**
4
+ * Nice Checkers plugin
5
+ * A collection of essential HTML validation rules
6
+ */
7
+
8
+ declare const plugin: Plugin;
9
+
10
+ export = plugin;
@@ -0,0 +1,10 @@
1
+ import { Plugin } from 'html-validate';
2
+
3
+ /**
4
+ * Nice Checkers plugin
5
+ * A collection of essential HTML validation rules
6
+ */
7
+
8
+ declare const plugin: Plugin;
9
+
10
+ export { plugin as default };
package/dist/index.js ADDED
@@ -0,0 +1,20 @@
1
+ import {Rule}from'html-validate';import L from'better-sqlite3';import p from'fs';import {execSync}from'child_process';import {quote}from'shell-quote';import d from'path';var U=Object.defineProperty;var o=(R,i)=>U(R,"name",{value:i,configurable:true});var k=class k extends Rule{documentation(){return {description:"Requires a <link rel='canonical'> in the <head> with a specific href format.",url:"https://github.com/fulldecent/html-validate-nice-checkers/blob/main/README.m#rules"}}setup(){this.on("dom:ready",i=>{this.domReady(i);});}domReady(i){let{document:e}=i,t=e.querySelector('head > link[rel="canonical"]');if(t===null){this.report({node:e.querySelector("head"),message:'<head> is missing <link rel="canonical" ...>'});return}let r=t.getAttribute("href")?.value;if(typeof r!="string"||!r){this.report({node:t,message:"<link rel='canonical'> is missing a non-empty href attribute"});return}/\.\w+$/.test(r)&&this.report({node:t,message:"Canonical link href should be extensionless (no .html, .php, etc.)"}),r.toLowerCase().endsWith("/index")&&this.report({node:t,message:'Canonical link href should be "/" instead of ending with "/index"'});}};o(k,"CanonicalLinkRule");var h=k;var j={proxyUrl:"",skipUrls:[],cacheExpiryFoundSeconds:720*60*60,cacheExpiryNotFoundSeconds:4320*60,timeoutSeconds:5,cacheDatabasePath:"cache/external-links.db",userAgent:"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.9999.999 Safari/537.36"};function P(R){try{let i=new URL(R);return i.hostname=i.hostname.toLowerCase(),i.toString()}catch{return R}}o(P,"normalizeUrl");var S=class S extends Rule{constructor(e){super({...j,...e});this.skipUrlsRegex=[];this.skipUrlsRegex=this.initializeSkipRegex();}static schema(){return {proxyUrl:{type:"string",description:"URL of a proxy server to check external links."},skipUrls:{type:"array",items:{type:"string"},description:"An array of regex patterns (as strings) for URLs to skip."},cacheExpiryFoundSeconds:{type:"number",description:"Number of seconds to cache successful (2xx) responses."},cacheExpiryNotFoundSeconds:{type:"number",description:"Number of seconds to cache failed (non-2xx) responses."},timeoutSeconds:{type:"number",description:"Maximum time in seconds to wait for a response."},cacheDatabasePath:{type:"string",description:"File path for the SQLite cache database."},userAgent:{type:"string",description:"User-Agent string to use for requests."}}}documentation(){return {description:"Require all external links to be live.",url:"https://github.com/fulldecent/html-validate-nice-checkers/blob/main/README.m#rules"}}setup(){this.db=this.setupDatabase(),this.on("tag:ready",e=>this.tagReady(e));}setupDatabase(){let e=d.dirname(this.options.cacheDatabasePath);p.mkdirSync(e,{recursive:true});let t=new L(this.options.cacheDatabasePath);return t.exec(`
2
+ CREATE TABLE IF NOT EXISTS urls (url TEXT UNIQUE NOT NULL, status INTEGER, redirect_to TEXT, time INTEGER);
3
+ CREATE INDEX IF NOT EXISTS time_idx ON urls (time);
4
+ `),t.exec(`DELETE FROM urls WHERE status >= 200 AND status < 300 AND time < unixepoch() - ${this.options.cacheExpiryFoundSeconds}`),t.exec(`DELETE FROM urls WHERE (status < 200 OR status >= 300) AND time < unixepoch() - ${this.options.cacheExpiryNotFoundSeconds}`),t}initializeSkipRegex(){let e=this.options?.skipUrls;return Array.isArray(e)?e.map(t=>{try{return new RegExp(t)}catch{return console.error(`[html-validate-nice-checkers] Invalid regex pattern in 'skipUrls' configuration: "${t}"`),null}}).filter(t=>t!==null):[]}performCheck(e,t,r){let s=P(e),a=quote([r??e]),n=`curl --head --silent --max-time ${this.options.timeoutSeconds} --max-redirs 0 --user-agent "${this.options.userAgent}" --header "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9" --write-out "%{http_code}" --dump-header - --output /dev/null ${a} || true`,u=execSync(n).toString(),c=u.match(/(\d{3})$/m),l=c&&c[1]?parseInt(c[1],10):500,x=u.match(/^Location: (.+)/im),v=x&&x[1]?x[1].trim():null;this.db.prepare("REPLACE INTO urls (url, status, redirect_to, time) VALUES (?, ?, ?, unixepoch())").run(s,l,v),(l<200||l>=300)&&(v?this.report({node:t,message:`External link ${e} redirects to: ${v}`}):this.report({node:t,message:`External link is broken with status ${l}: ${e}`}));}tagReady(e){let{target:t}=e,{tagName:r}=t,s=null;if(r==="a"||r==="link")s="href";else if(r==="script"||r==="img")s="src";else return;let a=t.getAttribute(s)?.value;if(typeof a!="string"||!a)return;let n=a.replace(/&amp;/g,"&").replace(/&lt;/g,"<").replace(/&gt;/g,">");if(!n.startsWith("http")||this.skipUrlsRegex.some(l=>l.test(n)))return;let u=P(n),c=this.db.prepare("SELECT * FROM urls WHERE url = ?").get(u);if(c){if(c.status>=200&&c.status<300)return;c.redirect_to?this.report({node:t,message:`External link ${n} redirects to: ${c.redirect_to}`}):this.report({node:t,message:`External link is broken with status ${c.status}: ${n}`});return}this.options.proxyUrl?this.performCheck(n,t,`${this.options.proxyUrl}?url=${encodeURIComponent(n)}`):this.performCheck(n,t);}};o(S,"ExternalLinksRule");var m=S;var _={cacheExpiryFoundSeconds:720*60*60,cacheExpiryNotFoundSeconds:4320*60,timeoutSeconds:10,cacheDatabasePath:"cache/https-availability.db"},T=class T extends Rule{constructor(i){super({..._,...i});}static schema(){return {cacheExpiryFoundSeconds:{type:"number",description:"Number of seconds to cache that a URL is available over HTTPS."},cacheExpiryNotFoundSeconds:{type:"number",description:"Number of seconds to cache that a URL is NOT available over HTTPS."},timeoutSeconds:{type:"number",description:"Maximum time in seconds to wait for a response when checking for HTTPS."},cacheDatabasePath:{type:"string",description:"File path for the SQLite cache database."}}}documentation(){return {description:"Report insecure HTTP links that are accessible via HTTPS.",url:"https://github.com/fulldecent/html-validate-nice-checkers/blob/main/README.m#rules"}}setup(){this.db=this.setupDatabase(),this.on("tag:ready",i=>this.tagReady(i));}setupDatabase(){let i=d.dirname(this.options.cacheDatabasePath);p.mkdirSync(i,{recursive:true});let e=new L(this.options.cacheDatabasePath);return e.exec(`
5
+ CREATE TABLE IF NOT EXISTS urls (
6
+ url TEXT UNIQUE NOT NULL,
7
+ found INTEGER NOT NULL,
8
+ time INTEGER NOT NULL
9
+ );
10
+ CREATE INDEX IF NOT EXISTS time_idx ON urls (time);
11
+ `),e.exec(`DELETE FROM urls WHERE found = 1 AND time < unixepoch() - ${this.options.cacheExpiryFoundSeconds}`),e.exec(`DELETE FROM urls WHERE found = 0 AND time < unixepoch() - ${this.options.cacheExpiryNotFoundSeconds}`),e}performHttpsCheck(i,e){let t=i.replace(/^http:/,"https:"),r=quote([t]),s=`curl --head --silent --fail --location --max-time ${this.options.timeoutSeconds} ${r} > /dev/null 2>&1`;try{execSync(s),this.db.prepare("REPLACE INTO urls (url, found, time) VALUES (?, 1, unixepoch())").run(i),this.report({node:e,message:`Insecure link can be upgraded to HTTPS: ${i}`});}catch{this.db.prepare("REPLACE INTO urls (url, found, time) VALUES (?, 0, unixepoch())").run(i);}}tagReady(i){let{target:e}=i,{tagName:t}=e,r=null;if(t==="a"||t==="link")r="href";else if(t==="script"||t==="img")r="src";else return;let s=e.getAttribute(r)?.value;if(typeof s!="string"||!s.startsWith("http://"))return;let a=s.replace(/&amp;/g,"&"),n=this.db.prepare("SELECT found FROM urls WHERE url = ?").get(a);if(n){n.found===1&&this.report({node:e,message:`Insecure link can be upgraded to HTTPS: ${a}`});return}this.performHttpsCheck(a,e);}};o(T,"HttpsLinksRule");var g=T;var V={webRoot:d.join(process.cwd(),"build"),alternativeExtensions:[".html",".php"],indexFile:"index.html"},D=class D extends Rule{constructor(e){super({...V,...e});this.fileExistsCache=new Map;}static schema(){return {webRoot:{type:"string",description:'The root directory for resolving absolute links (starting with "/").'},alternativeExtensions:{type:"array",items:{type:"string"},description:'List of file extensions to check for extensionless links (e.g., [".html"]).'},indexFile:{type:"string",description:"The file to look for in directory links (e.g., 'index.html')."}}}documentation(){return {description:"Validate that all internal links point to existing files.",url:"https://github.com/fulldecent/html-validate-nice-checkers/blob/main/README.m#rules"}}setup(){this.on("tag:ready",e=>this.tagReady(e));}doesFileExist(e){if(this.fileExistsCache.has(e))return this.fileExistsCache.get(e);let t=p.existsSync(e)&&p.lstatSync(e).isFile();return this.fileExistsCache.set(e,t),t}checkLink(e,t){let r;if(e.startsWith("/"))r=d.join(this.options.webRoot,e);else {let s=d.dirname(t.location.filename);r=d.resolve(s,e);}if(e.endsWith("/")){let s=r;if(p.existsSync(s)&&p.lstatSync(s).isDirectory()){let a=d.join(s,this.options.indexFile);if(this.doesFileExist(a))return}}else {if(this.doesFileExist(r)||this.options.alternativeExtensions.some(s=>this.doesFileExist(r+s)))return;if(p.existsSync(r)&&p.lstatSync(r).isDirectory()){let s=d.join(r,this.options.indexFile);if(this.doesFileExist(s))return}}this.report({node:t,message:`Internal link to "${e}" is broken.`});}tagReady(e){let{target:t}=e,{tagName:r}=t,s=null;if(r==="a"||r==="link")s="href";else if(r==="script"||r==="img")s="src";else return;let a=t.getAttribute(s)?.value;if(typeof a!="string"||!a)return;let n=a.replace(/[#?].*$/,"");n&&(/^[a-z][a-z0-9+.-]*:/i.test(n)||this.checkLink(n,t));}};o(D,"InternalLinksRule");var f=D;var te={cacheExpirySeconds:2880*60,timeoutSeconds:10,cacheDatabasePath:"cache/latest-packages.db",skipUrlPatterns:["googletagmanager.com"]},N=class N extends Rule{constructor(i){super({...te,...i});}static schema(){return {cacheExpirySeconds:{type:"number",description:"Number of seconds to cache package version lookup results."},timeoutSeconds:{type:"number",description:"Maximum time in seconds to wait for a response from the package registry API."},cacheDatabasePath:{type:"string",description:"File path for the SQLite cache database."},skipUrlPatterns:{type:"array",items:{type:"string"},description:"An array of substrings to identify URLs that should be ignored."}}}documentation(){return {description:"Ensures that package assets loaded from a CDN are the latest version and have SRI attributes.",url:"https://github.com/fulldecent/html-validate-nice-checkers/blob/main/README.m#rules"}}setup(){this.db=this.setupDatabase(),this.on("tag:ready",i=>this.tagReady(i));}setupDatabase(){let i=d.dirname(this.options.cacheDatabasePath);p.mkdirSync(i,{recursive:true});let e=new L(this.options.cacheDatabasePath);return e.exec(`
12
+ CREATE TABLE IF NOT EXISTS latest_packages (
13
+ url TEXT UNIQUE NOT NULL,
14
+ current INTEGER NOT NULL,
15
+ time INTEGER NOT NULL
16
+ );
17
+ CREATE INDEX IF NOT EXISTS time_idx ON latest_packages (time);
18
+ `),e.exec(`DELETE FROM latest_packages WHERE time < unixepoch() - ${this.options.cacheExpirySeconds}`),e}performPackageCheck(i,e){let t=i.match(/cdn\.jsdelivr\.net\/npm\/(@?[^/]+)@([^/]+)/);if(!t)return;let r=t[1],s=t[2],a=`https://data.jsdelivr.com/v1/package/npm/${r}`,n=quote([a]),u=`curl --silent --fail --location --max-time ${this.options.timeoutSeconds} ${n}`;try{let c=execSync(u).toString(),l=JSON.parse(c);l&&l.tags&&Object.values(l.tags).includes(s)?this.db.prepare("REPLACE INTO latest_packages (url, current, time) VALUES (?, 1, unixepoch())").run(i):(this.db.prepare("REPLACE INTO latest_packages (url, current, time) VALUES (?, 0, unixepoch())").run(i),this.report({node:e,message:`Package "${r}" is not using a current version tag. Found "${s}", but latest is "${l.tags.latest}".`}));}catch(c){console.error(`[html-validate-latest-packages] Error checking package version for ${i}: ${c.message}`);}}tagReady(i){let{target:e}=i,{tagName:t}=i.target,r;if(t==="script"){let a=e.getAttribute("src")?.value;typeof a=="string"&&(r=a);}else if(t==="link"&&e.getAttribute("rel")?.value==="stylesheet"){let a=e.getAttribute("href")?.value;typeof a=="string"&&(r=a);}else return;if(!r||!r.startsWith("http")||this.options.skipUrlPatterns.some(a=>r.includes(a)))return;(!e.hasAttribute("integrity")||!e.hasAttribute("crossorigin"))&&this.report({node:e,message:`Package resource is missing required "integrity" and "crossorigin" attributes: ${r}`});let s=this.db.prepare("SELECT current FROM latest_packages WHERE url = ?").get(r);if(s){s.current===0&&this.report({node:e,message:`Package is using an outdated version: ${r}`});return}this.performPackageCheck(r,e);}};o(N,"LatestPackagesRule");var b=N;var ie={requiredParameters:["subject","body"]},O=class O extends Rule{constructor(i){super({...ie,...i});}static schema(){return {requiredParameters:{type:"array",items:{type:"string"},description:'An array of parameters that must be present in a mailto: link (e.g., ["subject", "body"]).'}}}documentation(){return {description:"Enforce that mailto: links contain specific parameters.",url:"https://github.com/fulldecent/html-validate-nice-checkers/blob/main/README.m#rules"}}setup(){this.on("tag:ready",i=>this.tagReady(i));}tagReady(i){let{target:e}=i;if(e.tagName!=="a")return;let t=e.getAttribute("href")?.value;if(!(typeof t!="string"||!t)&&t.startsWith("mailto:"))try{let r=new URL(t),s=this.options.requiredParameters.filter(a=>!r.searchParams.has(a));s.length>0&&this.report({node:e,message:`mailto: link is missing required parameters: ${s.join(", ")}`});}catch{this.report({node:e,message:"mailto: link is malformed and could not be parsed."});}}};o(O,"MailtoAwesomeRule");var E=O;var A=class A extends Rule{documentation(){return {description:"Disallow script tag with src including jQuery",url:"https://github.com/fulldecent/html-validate-nice-checkers/blob/main/README.m#rules"}}setup(){this.on("tag:ready",i=>{let{target:e}=i;if(e.tagName!=="script")return;let t=e.getAttribute("src")?.value;typeof t!="string"||!t||t.toLowerCase().includes("jquery")&&this.report({node:e,message:"Script tag with src including jQuery is not allowed"});});}};o(A,"NoJqueryRule");var y=A;var ae={name:"nice-checkers-plugin",rules:{"nice-checkers/canonical-link":h,"nice-checkers/external-links":m,"nice-checkers/https-links":g,"nice-checkers/internal-links":f,"nice-checkers/latest-packages":b,"nice-checkers/mailto-awesome":E,"nice-checkers/no-jquery":y},configs:{recommended:{rules:{"nice-checkers/canonical-link":["error"],"nice-checkers/external-links":["error"],"nice-checkers/https-links":["error"],"nice-checkers/internal-links":["error"],"nice-checkers/latest-packages":["warn"],"nice-checkers/mailto-awesome":["error"],"nice-checkers/no-jquery":["error"]}}}},Ye=ae;
19
+ export{Ye as default};//# sourceMappingURL=index.js.map
20
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/rules/CanonicalLinkRule.ts","../src/rules/ExternalLinksRule.ts","../src/rules/HttpsLinksRule.ts","../src/rules/InternalLinksRule.ts","../src/rules/LatestPackagesRule.ts","../src/rules/MailtoAwesomeRule.ts","../src/rules/NoJqueryRule.ts","../src/index.ts"],"names":["_CanonicalLinkRule","Rule","event","document","linkCanonical","href","__name","CanonicalLinkRule","defaults","normalizeUrl","url","urlObj","_ExternalLinksRule","options","dir","path","fs","db","Database","patterns","pattern","regex","originalUrl","element","requestUrl","normalizedUrl","escapedUrl","shellEscape","command","output","execSync","statusCodeMatch","statusCode","locationMatch","redirectTo","target","tagName","urlAttribute","rawUrl","row","ExternalLinksRule","_HttpsLinksRule","httpsUrl","HttpsLinksRule","_InternalLinksRule","filePath","exists","internalLink","resolvedPath","basePath","directoryPath","indexPath","ext","InternalLinksRule","_LatestPackagesRule","match","packageName","packageVersion","apiUrl","result","data","e","src","LatestPackagesRule","_MailtoAwesomeRule","missingParams","param","MailtoAwesomeRule","_NoJqueryRule","NoJqueryRule","plugin","index_default"],"mappings":"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","file":"index.js","sourcesContent":["import { type RuleDocumentation, type DOMReadyEvent, Rule } from 'html-validate'\n\nexport default class CanonicalLinkRule extends Rule {\n public override documentation(): RuleDocumentation {\n return {\n description: \"Requires a <link rel='canonical'> in the <head> with a specific href format.\",\n url: 'https://github.com/fulldecent/html-validate-nice-checkers/blob/main/README.m#rules',\n }\n }\n\n public override setup(): void {\n // When the DOM is fully parsed, run the domReady method.\n this.on('dom:ready', (event: DOMReadyEvent) => {\n this.domReady(event)\n })\n }\n\n /**\n * Helper method to validate the document's canonical link.\n *\n * @param event - The dom:ready event object.\n */\n private domReady(event: DOMReadyEvent): void {\n const { document } = event\n const linkCanonical = document.querySelector('head > link[rel=\"canonical\"]')\n\n if (linkCanonical === null) {\n // Report error if the canonical link is missing entirely.\n this.report({\n node: document.querySelector('head')!,\n message: '<head> is missing <link rel=\"canonical\" ...>',\n })\n return\n }\n\n // Safely get the href attribute's value. It will be undefined if href is missing.\n const href = linkCanonical.getAttribute('href')?.value\n if (typeof href !== 'string' || !href) {\n this.report({\n node: linkCanonical,\n message: \"<link rel='canonical'> is missing a non-empty href attribute\",\n })\n return\n }\n\n // Check for file extensions like .html, .php, etc.\n if (/\\.\\w+$/.test(href)) {\n this.report({\n node: linkCanonical,\n message: 'Canonical link href should be extensionless (no .html, .php, etc.)',\n })\n }\n\n // Check if the link unnecessarily ends with /index.\n if (href.toLowerCase().endsWith('/index')) {\n this.report({\n node: linkCanonical,\n message: 'Canonical link href should be \"/\" instead of ending with \"/index\"',\n })\n }\n }\n}\n","import {\n type RuleDocumentation,\n type TagReadyEvent,\n type HtmlElement,\n type SchemaObject,\n Rule,\n} from 'html-validate'\nimport Database from 'better-sqlite3'\nimport fs from 'node:fs'\nimport { execSync } from 'node:child_process'\nimport { quote as shellEscape } from 'shell-quote'\nimport path from 'node:path'\n\ninterface UrlCacheRow {\n url: string\n status: number\n redirect_to: string | null\n time: number\n}\n\ninterface RuleOptions {\n proxyUrl: string\n skipUrls: string[]\n cacheExpiryFoundSeconds: number\n cacheExpiryNotFoundSeconds: number\n timeoutSeconds: number\n cacheDatabasePath: string\n userAgent: string\n}\n\nconst defaults: RuleOptions = {\n proxyUrl: '',\n skipUrls: [],\n cacheExpiryFoundSeconds: 30 * 24 * 60 * 60, // Default: 30 days\n cacheExpiryNotFoundSeconds: 3 * 24 * 60 * 60, // Default: 3 days\n timeoutSeconds: 5,\n cacheDatabasePath: 'cache/external-links.db',\n userAgent:\n 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.9999.999 Safari/537.36',\n}\n\nfunction normalizeUrl(url: string): string {\n try {\n const urlObj = new URL(url)\n urlObj.hostname = urlObj.hostname.toLowerCase()\n return urlObj.toString()\n } catch (e) {\n return url\n }\n}\n\nexport default class ExternalLinksRule extends Rule<void, RuleOptions> {\n private readonly skipUrlsRegex: RegExp[] = []\n private db!: Database.Database\n\n public constructor(options: Partial<RuleOptions>) {\n /* assign default values if not provided by user */\n super({ ...defaults, ...options })\n this.skipUrlsRegex = this.initializeSkipRegex()\n }\n\n public static override schema(): SchemaObject {\n return {\n proxyUrl: {\n type: 'string',\n description: 'URL of a proxy server to check external links.',\n },\n skipUrls: {\n type: 'array',\n items: {\n type: 'string',\n },\n description: 'An array of regex patterns (as strings) for URLs to skip.',\n },\n cacheExpiryFoundSeconds: {\n type: 'number',\n description: 'Number of seconds to cache successful (2xx) responses.',\n },\n cacheExpiryNotFoundSeconds: {\n type: 'number',\n description: 'Number of seconds to cache failed (non-2xx) responses.',\n },\n timeoutSeconds: {\n type: 'number',\n description: 'Maximum time in seconds to wait for a response.',\n },\n cacheDatabasePath: {\n type: 'string',\n description: 'File path for the SQLite cache database.',\n },\n userAgent: {\n type: 'string',\n description: 'User-Agent string to use for requests.',\n },\n }\n }\n\n public override documentation(): RuleDocumentation {\n return {\n description: 'Require all external links to be live.',\n url: 'https://github.com/fulldecent/html-validate-nice-checkers/blob/main/README.m#rules',\n }\n }\n\n public override setup(): void {\n this.db = this.setupDatabase()\n this.on('tag:ready', (event: TagReadyEvent) => this.tagReady(event))\n }\n\n private setupDatabase(): Database.Database {\n const dir = path.dirname(this.options.cacheDatabasePath)\n fs.mkdirSync(dir, { recursive: true })\n\n const db = new Database(this.options.cacheDatabasePath)\n db.exec(`\n CREATE TABLE IF NOT EXISTS urls (url TEXT UNIQUE NOT NULL, status INTEGER, redirect_to TEXT, time INTEGER);\n CREATE INDEX IF NOT EXISTS time_idx ON urls (time);\n `)\n\n db.exec(\n `DELETE FROM urls WHERE status >= 200 AND status < 300 AND time < unixepoch() - ${this.options.cacheExpiryFoundSeconds}`\n )\n db.exec(\n `DELETE FROM urls WHERE (status < 200 OR status >= 300) AND time < unixepoch() - ${this.options.cacheExpiryNotFoundSeconds}`\n )\n return db\n }\n\n private initializeSkipRegex(): RegExp[] {\n const patterns = this.options?.skipUrls\n if (!Array.isArray(patterns)) {\n return []\n }\n return patterns\n .map(pattern => {\n try {\n return new RegExp(pattern)\n } catch (e) {\n console.error(\n `[html-validate-nice-checkers] Invalid regex pattern in 'skipUrls' configuration: \"${pattern}\"`\n )\n return null\n }\n })\n .filter((regex): regex is RegExp => regex !== null)\n }\n\n private performCheck(originalUrl: string, element: HtmlElement, requestUrl?: string): void {\n const normalizedUrl = normalizeUrl(originalUrl)\n const escapedUrl = shellEscape([requestUrl ?? originalUrl])\n\n const command = `curl --head --silent --max-time ${this.options.timeoutSeconds} --max-redirs 0 \\\n --user-agent \"${this.options.userAgent}\" \\\n --header \"Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9\" \\\n --write-out \"%{http_code}\" --dump-header - --output /dev/null ${escapedUrl} || true`\n\n const output = execSync(command).toString()\n const statusCodeMatch = output.match(/(\\d{3})$/m)\n const statusCode =\n statusCodeMatch && statusCodeMatch[1] ? parseInt(statusCodeMatch[1], 10) : 500\n const locationMatch = output.match(/^Location: (.+)/im)\n const redirectTo = locationMatch && locationMatch[1] ? locationMatch[1].trim() : null\n this.db\n .prepare('REPLACE INTO urls (url, status, redirect_to, time) VALUES (?, ?, ?, unixepoch())')\n .run(normalizedUrl, statusCode, redirectTo)\n if (statusCode < 200 || statusCode >= 300) {\n if (redirectTo) {\n this.report({\n node: element,\n message: `External link ${originalUrl} redirects to: ${redirectTo}`,\n })\n } else {\n this.report({\n node: element,\n message: `External link is broken with status ${statusCode}: ${originalUrl}`,\n })\n }\n }\n }\n\n private tagReady(event: TagReadyEvent): void {\n const { target } = event\n const { tagName } = target\n\n let urlAttribute: string | null = null\n if (tagName === 'a' || tagName === 'link') {\n urlAttribute = 'href'\n } else if (tagName === 'script' || tagName === 'img') {\n urlAttribute = 'src'\n } else {\n return\n }\n\n const rawUrl = target.getAttribute(urlAttribute)?.value\n if (typeof rawUrl !== 'string' || !rawUrl) {\n return\n }\n\n const url = rawUrl.replace(/&amp;/g, '&').replace(/&lt;/g, '<').replace(/&gt;/g, '>')\n\n if (!url.startsWith('http')) {\n return\n }\n\n if (this.skipUrlsRegex.some(regex => regex.test(url))) {\n return\n }\n\n const normalizedUrl = normalizeUrl(url)\n const row = this.db.prepare('SELECT * FROM urls WHERE url = ?').get(normalizedUrl) as\n | UrlCacheRow\n | undefined\n\n if (row) {\n if (row.status >= 200 && row.status < 300) {\n return\n }\n if (row.redirect_to) {\n this.report({\n node: target,\n message: `External link ${url} redirects to: ${row.redirect_to}`,\n })\n } else {\n this.report({\n node: target,\n message: `External link is broken with status ${row.status}: ${url}`,\n })\n }\n return\n }\n\n if (this.options.proxyUrl) {\n this.performCheck(url, target, `${this.options.proxyUrl}?url=${encodeURIComponent(url)}`)\n } else {\n this.performCheck(url, target)\n }\n }\n}\n","import {\n type RuleDocumentation,\n type TagReadyEvent,\n type HtmlElement,\n type SchemaObject,\n Rule,\n} from 'html-validate'\nimport Database from 'better-sqlite3'\nimport fs from 'node:fs'\nimport { execSync } from 'node:child_process'\nimport { quote as shellEscape } from 'shell-quote'\nimport path from 'node:path'\n\ninterface HttpsCacheRow {\n url: string\n found: 0 | 1\n time: number\n}\n\ninterface RuleOptions {\n cacheExpiryFoundSeconds: number\n cacheExpiryNotFoundSeconds: number\n timeoutSeconds: number\n cacheDatabasePath: string\n}\n\nconst defaults: RuleOptions = {\n cacheExpiryFoundSeconds: 30 * 24 * 60 * 60, // Default: 30 days\n cacheExpiryNotFoundSeconds: 3 * 24 * 60 * 60, // Default: 3 days\n timeoutSeconds: 10,\n cacheDatabasePath: 'cache/https-availability.db',\n}\n\nexport default class HttpsLinksRule extends Rule<void, RuleOptions> {\n private db!: Database.Database\n\n public constructor(options: Partial<RuleOptions>) {\n /* assign default values if not provided by user */\n super({ ...defaults, ...options })\n }\n\n public static override schema(): SchemaObject {\n return {\n cacheExpiryFoundSeconds: {\n type: 'number',\n description: 'Number of seconds to cache that a URL is available over HTTPS.',\n },\n cacheExpiryNotFoundSeconds: {\n type: 'number',\n description: 'Number of seconds to cache that a URL is NOT available over HTTPS.',\n },\n timeoutSeconds: {\n type: 'number',\n description: 'Maximum time in seconds to wait for a response when checking for HTTPS.',\n },\n cacheDatabasePath: {\n type: 'string',\n description: 'File path for the SQLite cache database.',\n },\n }\n }\n\n public override documentation(): RuleDocumentation {\n return {\n description: 'Report insecure HTTP links that are accessible via HTTPS.',\n url: 'https://github.com/fulldecent/html-validate-nice-checkers/blob/main/README.m#rules',\n }\n }\n\n public override setup(): void {\n this.db = this.setupDatabase()\n this.on('tag:ready', (event: TagReadyEvent) => this.tagReady(event))\n }\n\n private setupDatabase(): Database.Database {\n const dir = path.dirname(this.options.cacheDatabasePath)\n fs.mkdirSync(dir, { recursive: true })\n\n const db = new Database(this.options.cacheDatabasePath)\n db.exec(`\n CREATE TABLE IF NOT EXISTS urls (\n url TEXT UNIQUE NOT NULL,\n found INTEGER NOT NULL,\n time INTEGER NOT NULL\n );\n CREATE INDEX IF NOT EXISTS time_idx ON urls (time);\n `)\n\n db.exec(\n `DELETE FROM urls WHERE found = 1 AND time < unixepoch() - ${this.options.cacheExpiryFoundSeconds}`\n )\n db.exec(\n `DELETE FROM urls WHERE found = 0 AND time < unixepoch() - ${this.options.cacheExpiryNotFoundSeconds}`\n )\n return db\n }\n\n private performHttpsCheck(url: string, element: HtmlElement): void {\n const httpsUrl = url.replace(/^http:/, 'https:')\n const escapedUrl = shellEscape([httpsUrl])\n\n // Use --fail to make curl exit with an error on 4xx/5xx responses.\n const command = `curl --head --silent --fail --location --max-time ${this.options.timeoutSeconds} ${escapedUrl} > /dev/null 2>&1`\n\n try {\n execSync(command)\n // If execSync does NOT throw, the command was successful (exit code 0).\n // This means the URL is available over HTTPS.\n this.db.prepare('REPLACE INTO urls (url, found, time) VALUES (?, 1, unixepoch())').run(url)\n this.report({\n node: element,\n message: `Insecure link can be upgraded to HTTPS: ${url}`,\n })\n } catch (error) {\n // If execSync throws, the command failed.\n // This means the URL is NOT available over HTTPS. Cache this result to avoid re-checking.\n this.db.prepare('REPLACE INTO urls (url, found, time) VALUES (?, 0, unixepoch())').run(url)\n }\n }\n\n private tagReady(event: TagReadyEvent): void {\n const { target } = event\n const { tagName } = target\n\n let urlAttribute: string | null = null\n if (tagName === 'a' || tagName === 'link') {\n urlAttribute = 'href'\n } else if (tagName === 'script' || tagName === 'img') {\n urlAttribute = 'src'\n } else {\n return\n }\n\n const rawUrl = target.getAttribute(urlAttribute)?.value\n if (typeof rawUrl !== 'string' || !rawUrl.startsWith('http://')) {\n return\n }\n\n // A simple decoder. More complex entities would require a library.\n const url = rawUrl.replace(/&amp;/g, '&')\n\n const row = this.db.prepare('SELECT found FROM urls WHERE url = ?').get(url) as\n | Pick<HttpsCacheRow, 'found'>\n | undefined\n\n if (row) {\n if (row.found === 1) {\n // Cache hit: we already know it's upgradable, so report it.\n this.report({\n node: target,\n message: `Insecure link can be upgraded to HTTPS: ${url}`,\n })\n }\n // If row.found is 0, we know it's not upgradable, so we do nothing.\n return\n }\n\n // If not in cache, perform the live check.\n this.performHttpsCheck(url, target)\n }\n}\n","import {\n type RuleDocumentation,\n type TagReadyEvent,\n type HtmlElement,\n type SchemaObject,\n Rule,\n} from 'html-validate'\nimport fs from 'node:fs'\nimport path from 'node:path'\n\ninterface RuleOptions {\n webRoot: string\n alternativeExtensions: string[]\n indexFile: string\n}\n\nconst defaults: RuleOptions = {\n webRoot: path.join(process.cwd(), 'build'),\n alternativeExtensions: ['.html', '.php'],\n indexFile: 'index.html',\n}\n\nexport default class InternalLinksRule extends Rule<void, RuleOptions> {\n private readonly fileExistsCache = new Map<string, boolean>()\n\n public constructor(options: Partial<RuleOptions>) {\n /* assign default values if not provided by user */\n super({ ...defaults, ...options })\n }\n\n public static override schema(): SchemaObject {\n return {\n webRoot: {\n type: 'string',\n description: 'The root directory for resolving absolute links (starting with \"/\").',\n },\n alternativeExtensions: {\n type: 'array',\n items: {\n type: 'string',\n },\n description: 'List of file extensions to check for extensionless links (e.g., [\".html\"]).',\n },\n indexFile: {\n type: 'string',\n description: \"The file to look for in directory links (e.g., 'index.html').\",\n },\n }\n }\n\n public override documentation(): RuleDocumentation {\n return {\n description: 'Validate that all internal links point to existing files.',\n url: 'https://github.com/fulldecent/html-validate-nice-checkers/blob/main/README.m#rules',\n }\n }\n\n public override setup(): void {\n this.on('tag:ready', (event: TagReadyEvent) => this.tagReady(event))\n }\n\n private doesFileExist(filePath: string): boolean {\n if (this.fileExistsCache.has(filePath)) {\n return this.fileExistsCache.get(filePath) as boolean\n }\n\n const exists = fs.existsSync(filePath) && fs.lstatSync(filePath).isFile()\n this.fileExistsCache.set(filePath, exists)\n return exists\n }\n\n private checkLink(internalLink: string, element: HtmlElement): void {\n let resolvedPath: string\n\n // Resolve absolute paths from the web root, relative paths from the current file's location.\n if (internalLink.startsWith('/')) {\n resolvedPath = path.join(this.options.webRoot, internalLink)\n } else {\n const basePath = path.dirname(element.location.filename)\n resolvedPath = path.resolve(basePath, internalLink)\n }\n\n // Case 1: Link ends with '/', check for an index file in that directory.\n if (internalLink.endsWith('/')) {\n const directoryPath = resolvedPath\n if (fs.existsSync(directoryPath) && fs.lstatSync(directoryPath).isDirectory()) {\n const indexPath = path.join(directoryPath, this.options.indexFile)\n if (this.doesFileExist(indexPath)) {\n return // Link is valid.\n }\n }\n } else {\n // Case 2: Link does not end with '/'.\n // Check for a direct file match or a file with an alternative extension.\n if (this.doesFileExist(resolvedPath)) {\n return // Direct file match is valid.\n }\n if (this.options.alternativeExtensions.some(ext => this.doesFileExist(resolvedPath + ext))) {\n return // Extensionless link is valid.\n }\n\n // Also check if it's a directory containing an index file.\n // E.g., a link to \"/about\" can resolve to \"/about/index.html\".\n if (fs.existsSync(resolvedPath) && fs.lstatSync(resolvedPath).isDirectory()) {\n const indexPath = path.join(resolvedPath, this.options.indexFile)\n if (this.doesFileExist(indexPath)) {\n return // Directory link is valid.\n }\n }\n }\n\n // If no valid file was found after all checks, report the error.\n this.report({\n node: element,\n message: `Internal link to \"${internalLink}\" is broken.`,\n })\n }\n\n private tagReady(event: TagReadyEvent): void {\n const { target } = event\n const { tagName } = target\n\n let urlAttribute: string | null = null\n if (tagName === 'a' || tagName === 'link') {\n urlAttribute = 'href'\n } else if (tagName === 'script' || tagName === 'img') {\n urlAttribute = 'src'\n } else {\n return\n }\n\n const rawUrl = target.getAttribute(urlAttribute)?.value\n if (typeof rawUrl !== 'string' || !rawUrl) {\n return\n }\n\n // Strip fragment and query parameters.\n const url = rawUrl.replace(/[#?].*$/, '')\n if (!url) {\n return\n }\n\n // Skip absolute URLs.\n // An internal link does not have a protocol (http:, mailto:, etc.).\n if (/^[a-z][a-z0-9+.-]*:/i.test(url)) {\n return\n }\n\n // The URL is internal, now check if it's valid.\n this.checkLink(url, target)\n }\n}\n","import {\n type RuleDocumentation,\n type TagReadyEvent,\n type HtmlElement,\n type SchemaObject,\n Rule,\n} from 'html-validate'\nimport Database from 'better-sqlite3'\nimport fs from 'node:fs'\nimport { execSync } from 'node:child_process'\nimport { quote as shellEscape } from 'shell-quote'\nimport path from 'node:path'\n\ninterface PackageCacheRow {\n url: string\n current: 0 | 1\n time: number\n}\n\ninterface RuleOptions {\n cacheExpirySeconds: number\n timeoutSeconds: number\n cacheDatabasePath: string\n skipUrlPatterns: string[]\n}\n\nconst defaults: RuleOptions = {\n cacheExpirySeconds: 2 * 24 * 60 * 60, // Default: 2 days\n timeoutSeconds: 10,\n cacheDatabasePath: 'cache/latest-packages.db',\n skipUrlPatterns: ['googletagmanager.com'],\n}\n\nexport default class LatestPackagesRule extends Rule<void, RuleOptions> {\n private db!: Database.Database\n\n public constructor(options: Partial<RuleOptions>) {\n /* assign default values if not provided by user */\n super({ ...defaults, ...options })\n }\n\n public static override schema(): SchemaObject {\n return {\n cacheExpirySeconds: {\n type: 'number',\n description: 'Number of seconds to cache package version lookup results.',\n },\n timeoutSeconds: {\n type: 'number',\n description:\n 'Maximum time in seconds to wait for a response from the package registry API.',\n },\n cacheDatabasePath: {\n type: 'string',\n description: 'File path for the SQLite cache database.',\n },\n skipUrlPatterns: {\n type: 'array',\n items: {\n type: 'string',\n },\n description: 'An array of substrings to identify URLs that should be ignored.',\n },\n }\n }\n\n public override documentation(): RuleDocumentation {\n return {\n description:\n 'Ensures that package assets loaded from a CDN are the latest version and have SRI attributes.',\n url: 'https://github.com/fulldecent/html-validate-nice-checkers/blob/main/README.m#rules',\n }\n }\n\n public override setup(): void {\n this.db = this.setupDatabase()\n this.on('tag:ready', (event: TagReadyEvent) => this.tagReady(event))\n }\n\n private setupDatabase(): Database.Database {\n const dir = path.dirname(this.options.cacheDatabasePath)\n fs.mkdirSync(dir, { recursive: true })\n\n const db = new Database(this.options.cacheDatabasePath)\n db.exec(`\n CREATE TABLE IF NOT EXISTS latest_packages (\n url TEXT UNIQUE NOT NULL,\n current INTEGER NOT NULL,\n time INTEGER NOT NULL\n );\n CREATE INDEX IF NOT EXISTS time_idx ON latest_packages (time);\n `)\n\n db.exec(\n `DELETE FROM latest_packages WHERE time < unixepoch() - ${this.options.cacheExpirySeconds}`\n )\n return db\n }\n\n private performPackageCheck(url: string, element: HtmlElement): void {\n // Robustly parse jsDelivr NPM URLs, supporting scoped packages.\n const match = url.match(/cdn\\.jsdelivr\\.net\\/npm\\/(@?[^/]+)@([^/]+)/)\n if (!match) {\n return // Not a jsDelivr NPM URL we can check.\n }\n\n const packageName = match[1]\n const packageVersion = match[2]\n const apiUrl = `https://data.jsdelivr.com/v1/package/npm/${packageName}`\n const escapedUrl = shellEscape([apiUrl])\n const command = `curl --silent --fail --location --max-time ${this.options.timeoutSeconds} ${escapedUrl}`\n\n try {\n const result = execSync(command).toString()\n const data = JSON.parse(result)\n\n if (data && data.tags && Object.values(data.tags).includes(packageVersion)) {\n // The version in the URL is a valid tag (e.g., 'latest', 'beta', or a specific version tag).\n this.db\n .prepare('REPLACE INTO latest_packages (url, current, time) VALUES (?, 1, unixepoch())')\n .run(url)\n } else {\n // The version is not a recognized tag, so it's likely outdated or incorrect.\n this.db\n .prepare('REPLACE INTO latest_packages (url, current, time) VALUES (?, 0, unixepoch())')\n .run(url)\n this.report({\n node: element,\n message: `Package \"${packageName}\" is not using a current version tag. Found \"${packageVersion}\", but latest is \"${data.tags.latest}\".`,\n })\n }\n } catch (e: any) {\n // Log errors related to the check itself (e.g., network issues) but don't fail validation.\n console.error(\n `[html-validate-latest-packages] Error checking package version for ${url}: ${e.message}`\n )\n }\n }\n\n private tagReady(event: TagReadyEvent): void {\n const { target } = event\n const { tagName } = event.target\n let url: string | undefined\n\n if (tagName === 'script') {\n const src = target.getAttribute('src')?.value\n if (typeof src === 'string') {\n url = src\n }\n } else if (tagName === 'link' && target.getAttribute('rel')?.value === 'stylesheet') {\n const href = target.getAttribute('href')?.value\n if (typeof href === 'string') {\n url = href\n }\n } else {\n return\n }\n if (\n !url ||\n !url.startsWith('http') ||\n this.options.skipUrlPatterns.some(pattern => url!.includes(pattern))\n ) {\n return\n }\n\n // Enforce Subresource Integrity (SRI)\n if (!target.hasAttribute('integrity') || !target.hasAttribute('crossorigin')) {\n this.report({\n node: target,\n message: `Package resource is missing required \"integrity\" and \"crossorigin\" attributes: ${url}`,\n })\n }\n\n const row = this.db.prepare('SELECT current FROM latest_packages WHERE url = ?').get(url) as\n | Pick<PackageCacheRow, 'current'>\n | undefined\n\n if (row) {\n if (row.current === 0) {\n // Cache hit: we already know it's outdated, report it.\n this.report({\n node: target,\n message: `Package is using an outdated version: ${url}`,\n })\n }\n return // Cache hit, do nothing more.\n }\n\n // If not in cache, perform the live check.\n this.performPackageCheck(url, target)\n }\n}\n","import { type RuleDocumentation, type TagReadyEvent, type SchemaObject, Rule } from 'html-validate'\n\ninterface RuleOptions {\n requiredParameters: string[]\n}\n\nconst defaults: RuleOptions = {\n requiredParameters: ['subject', 'body'],\n}\n\nexport default class MailtoAwesomeRule extends Rule<void, RuleOptions> {\n public constructor(options: Partial<RuleOptions>) {\n /* assign default values if not provided by user */\n super({ ...defaults, ...options })\n }\n\n public static override schema(): SchemaObject {\n return {\n requiredParameters: {\n type: 'array',\n items: {\n type: 'string',\n },\n description:\n 'An array of parameters that must be present in a mailto: link (e.g., [\"subject\", \"body\"]).',\n },\n }\n }\n\n public override documentation(): RuleDocumentation {\n return {\n description: 'Enforce that mailto: links contain specific parameters.',\n url: 'https://github.com/fulldecent/html-validate-nice-checkers/blob/main/README.m#rules',\n }\n }\n\n public override setup(): void {\n this.on('tag:ready', (event: TagReadyEvent) => this.tagReady(event))\n }\n\n private tagReady(event: TagReadyEvent): void {\n const { target } = event\n\n // This rule only applies to <a> tags.\n if (target.tagName !== 'a') {\n return\n }\n\n const href = target.getAttribute('href')?.value\n if (typeof href !== 'string' || !href) {\n return\n }\n if (!href.startsWith('mailto:')) {\n return\n }\n\n try {\n const url = new URL(href)\n const missingParams = this.options.requiredParameters.filter(param => {\n return !url.searchParams.has(param)\n })\n\n if (missingParams.length > 0) {\n this.report({\n node: target,\n message: `mailto: link is missing required parameters: ${missingParams.join(', ')}`,\n })\n }\n } catch (e) {\n // Handle cases where the mailto: URL is malformed and cannot be parsed.\n this.report({\n node: target,\n message: 'mailto: link is malformed and could not be parsed.',\n })\n }\n }\n}\n","import { type TagReadyEvent, Rule, type RuleDocumentation } from 'html-validate'\n\nexport default class NoJqueryRule extends Rule {\n public override documentation(): RuleDocumentation {\n return {\n description: 'Disallow script tag with src including jQuery',\n url: 'https://github.com/fulldecent/html-validate-nice-checkers/blob/main/README.m#rules',\n }\n }\n\n public override setup(): void {\n this.on('tag:ready', (event: TagReadyEvent) => {\n const { target } = event\n if (target.tagName !== 'script') {\n return\n }\n const src = target.getAttribute('src')?.value\n if (typeof src !== 'string' || !src) {\n return\n }\n if (!src.toLowerCase().includes('jquery')) {\n return\n }\n this.report({\n node: target,\n message: 'Script tag with src including jQuery is not allowed',\n })\n })\n }\n}\n","/**\n * Nice Checkers plugin\n * A collection of essential HTML validation rules\n */\n\nimport type { Plugin } from 'html-validate'\nimport CanonicalLinkRule from './rules/CanonicalLinkRule'\nimport ExternalLinksRule from './rules/ExternalLinksRule'\nimport HttpsLinksRule from './rules/HttpsLinksRule'\nimport InternalLinksRule from './rules/InternalLinksRule'\nimport LatestPackagesRule from './rules/LatestPackagesRule'\nimport MailtoAwesomeRule from './rules/MailtoAwesomeRule'\nimport NoJqueryRule from './rules/NoJqueryRule'\n\nconst plugin: Plugin = {\n name: 'nice-checkers-plugin',\n\n rules: {\n 'nice-checkers/canonical-link': CanonicalLinkRule,\n 'nice-checkers/external-links': ExternalLinksRule,\n 'nice-checkers/https-links': HttpsLinksRule,\n 'nice-checkers/internal-links': InternalLinksRule,\n 'nice-checkers/latest-packages': LatestPackagesRule,\n 'nice-checkers/mailto-awesome': MailtoAwesomeRule,\n 'nice-checkers/no-jquery': NoJqueryRule,\n },\n\n configs: {\n recommended: {\n rules: {\n 'nice-checkers/canonical-link': ['error'],\n 'nice-checkers/external-links': ['error'],\n 'nice-checkers/https-links': ['error'],\n 'nice-checkers/internal-links': ['error'],\n 'nice-checkers/latest-packages': ['warn'],\n 'nice-checkers/mailto-awesome': ['error'],\n 'nice-checkers/no-jquery': ['error'],\n },\n },\n },\n}\n\nexport default plugin\n"]}
package/package.json ADDED
@@ -0,0 +1,86 @@
1
+ {
2
+ "name": "@fulldecent/nice-checkers-plugin",
3
+ "version": "0.1.0",
4
+ "description": "HTML-validate plugin and rules you should always use",
5
+ "author": "William Entriken",
6
+ "license": "MIT",
7
+ "homepage": "https://github.com/yourusername/html-validate-nice-checkers#readme",
8
+ "bugs": {
9
+ "url": "https://github.com/yourusername/html-validate-nice-checkers/issues"
10
+ },
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "git+https://github.com/yourusername/html-validate-nice-checkers.git"
14
+ },
15
+ "keywords": [
16
+ "html-validate",
17
+ "html",
18
+ "validation",
19
+ "rules",
20
+ "plugin",
21
+ "linting"
22
+ ],
23
+ "type": "module",
24
+ "main": "./dist/index.cjs",
25
+ "module": "./dist/index.js",
26
+ "types": "./dist/index.d.ts",
27
+ "exports": {
28
+ ".": {
29
+ "import": {
30
+ "types": "./dist/index.d.ts",
31
+ "default": "./dist/index.js"
32
+ },
33
+ "require": {
34
+ "types": "./dist/index.d.cts",
35
+ "default": "./dist/index.cjs"
36
+ }
37
+ }
38
+ },
39
+ "files": [
40
+ "dist",
41
+ "README.md",
42
+ "LICENSE"
43
+ ],
44
+ "scripts": {
45
+ "build": "tsup",
46
+ "build:watch": "tsup --watch",
47
+ "test": "vitest --run",
48
+ "test:watch": "vitest --watch",
49
+ "test:coverage": "vitest --coverage",
50
+ "lint": "tsc --noEmit",
51
+ "format": "prettier --write .",
52
+ "format:check": "prettier --check .",
53
+ "format:staged": "prettier --write",
54
+ "clean": "rimraf dist",
55
+ "prepublishOnly": "yarn clean && yarn lint && yarn format:check && yarn test && yarn build",
56
+ "dev": "yarn build:watch"
57
+ },
58
+ "devDependencies": {
59
+ "@types/better-sqlite3": "^7.6.13",
60
+ "@types/node": "^22.0.0",
61
+ "@types/shell-quote": "^1.7.5",
62
+ "@vitest/coverage-v8": "^3.2.4",
63
+ "html-validate": "^10.0.0",
64
+ "prettier": "^3.6.2",
65
+ "rimraf": "^6.0.1",
66
+ "structured-data-testing-tool": "^4.5.0",
67
+ "tsup": "^8.5.0",
68
+ "typescript": "^5.9.2",
69
+ "vitest": "^3.2.4"
70
+ },
71
+ "peerDependencies": {
72
+ "html-validate": "^8.10.0 || ^9.0.0 || ^10.0.0"
73
+ },
74
+ "engines": {
75
+ "node": ">=20.0.0"
76
+ },
77
+ "packageManager": "yarn@4.10.3",
78
+ "dependencies": {
79
+ "better-sqlite3": "^12.4.1",
80
+ "shell-quote": "^1.8.3"
81
+ },
82
+ "publishConfig": {
83
+ "registry": "https://registry.npmjs.org/",
84
+ "access": "public"
85
+ }
86
+ }