astro-smart-links 1.0.1 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -0
- package/README.zh-CN.md +7 -0
- package/dist/{chunk-67PNI3F6.js → chunk-JNP2MFPB.js} +36 -24
- package/dist/cli.cjs +39 -26
- package/dist/cli.js +4 -3
- package/dist/index.cjs +193 -115
- package/dist/index.d.cts +12 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +156 -90
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -12,6 +12,11 @@ No routes file, no second build. The integration validates links against the rea
|
|
|
12
12
|
|
|
13
13
|
> Migrating from `rehype-smart-links@0.x`? See [Migration](#migration-from-rehype-smart-links).
|
|
14
14
|
|
|
15
|
+
## Requirements
|
|
16
|
+
|
|
17
|
+
- Node.js 18.17 or newer.
|
|
18
|
+
- Astro 4 or newer. Both Astro 7's default Sätteri Markdown processor and the unified processor from `@astrojs/markdown-remark` are supported.
|
|
19
|
+
|
|
15
20
|
## Installation
|
|
16
21
|
|
|
17
22
|
```bash
|
|
@@ -113,6 +118,7 @@ npx astro-smart-links check --all --extensions html pdf zip
|
|
|
113
118
|
| `--json` | Print the report to stdout |
|
|
114
119
|
| `-a, --all` | Treat every file type as a valid route |
|
|
115
120
|
| `-e, --extensions <ext...>` | File extensions to include (default `html`) |
|
|
121
|
+
| `--skip-classes <class...>` | Skip links inside elements with these classes |
|
|
116
122
|
| `--fail-on-broken` | Exit with code 1 when broken links are found |
|
|
117
123
|
| `-q, --quiet` | Only print the summary |
|
|
118
124
|
|
|
@@ -128,6 +134,7 @@ npx astro-smart-links check --all --extensions html pdf zip
|
|
|
128
134
|
| `target` | `string \| null` | `'_blank'` | `target` attribute for external links |
|
|
129
135
|
| `rel` | `string \| null` | `'noopener noreferrer'` | `rel` attribute for external links |
|
|
130
136
|
| `ignore` | `(string \| RegExp)[]` | `[]` | Hrefs that are never processed (prefix match or RegExp) |
|
|
137
|
+
| `skipClasses` | `string[]` | `[]` | Skip links inside elements with these classes (e.g. `['not-content']`) |
|
|
131
138
|
| `routes` | `string[]` | — | Explicit route list for broken-link detection |
|
|
132
139
|
| `routesFile` | `string` | — | JSON file with a route list |
|
|
133
140
|
| `publicDir` | `string` | — | Directory to scan for routes |
|
package/README.zh-CN.md
CHANGED
|
@@ -12,6 +12,11 @@
|
|
|
12
12
|
|
|
13
13
|
> 从 `rehype-smart-links@0.x` 迁移?见 [迁移指南](#从-rehype-smart-links-迁移)。
|
|
14
14
|
|
|
15
|
+
## 环境要求
|
|
16
|
+
|
|
17
|
+
- Node.js 18.17 或更高版本。
|
|
18
|
+
- Astro 4 或更高版本。同时支持 Astro 7 默认的 Sätteri Markdown 处理器和 `@astrojs/markdown-remark` 的 unified 处理器。
|
|
19
|
+
|
|
15
20
|
## 安装
|
|
16
21
|
|
|
17
22
|
```bash
|
|
@@ -113,6 +118,7 @@ npx astro-smart-links check --all --extensions html pdf zip
|
|
|
113
118
|
| `--json` | 以 JSON 输出到 stdout |
|
|
114
119
|
| `-a, --all` | 将所有文件类型视为有效路由 |
|
|
115
120
|
| `-e, --extensions <ext...>` | 要包含的文件扩展名(默认 `html`) |
|
|
121
|
+
| `--skip-classes <class...>` | 跳过位于这些类名元素内的链接 |
|
|
116
122
|
| `--fail-on-broken` | 发现断链时以退出码 1 结束 |
|
|
117
123
|
| `-q, --quiet` | 只输出摘要 |
|
|
118
124
|
|
|
@@ -128,6 +134,7 @@ npx astro-smart-links check --all --extensions html pdf zip
|
|
|
128
134
|
| `target` | `string \| null` | `'_blank'` | 外部链接的 `target` |
|
|
129
135
|
| `rel` | `string \| null` | `'noopener noreferrer'` | 外部链接的 `rel` |
|
|
130
136
|
| `ignore` | `(string \| RegExp)[]` | `[]` | 不处理的链接(前缀匹配或正则) |
|
|
137
|
+
| `skipClasses` | `string[]` | `[]` | 跳过位于这些类名元素内的链接(如 `['not-content']`) |
|
|
131
138
|
| `routes` | `string[]` | — | 显式路由列表 |
|
|
132
139
|
| `routesFile` | `string` | — | 包含路由列表的 JSON 文件 |
|
|
133
140
|
| `publicDir` | `string` | — | 扫描路由的目录 |
|
|
@@ -188,7 +188,6 @@ import { join as join2, relative } from "path";
|
|
|
188
188
|
import rehypeParse from "rehype-parse";
|
|
189
189
|
import rehypeStringify from "rehype-stringify";
|
|
190
190
|
import { unified } from "unified";
|
|
191
|
-
import { visit } from "unist-util-visit";
|
|
192
191
|
function collectHtmlFiles(directory) {
|
|
193
192
|
const files = [];
|
|
194
193
|
const walk = (currentDir) => {
|
|
@@ -209,8 +208,14 @@ function routeFromHtmlFile(distDir, htmlFile) {
|
|
|
209
208
|
relativePath = relativePath.replace(/(?:^|\/)index$/i, "");
|
|
210
209
|
return normalizeRoute(relativePath);
|
|
211
210
|
}
|
|
211
|
+
function hasAnyClass(node, classes) {
|
|
212
|
+
const current = node.properties?.className;
|
|
213
|
+
const list = Array.isArray(current) ? current.map(String) : typeof current === "string" ? current.split(/\s+/) : [];
|
|
214
|
+
return classes.some((className) => list.includes(className));
|
|
215
|
+
}
|
|
212
216
|
function checkDirectory(directory, options = {}) {
|
|
213
217
|
const routes = options.routes ?? scanRoutes(directory, options);
|
|
218
|
+
const skipClasses = options.skipClasses ?? [];
|
|
214
219
|
const broken = [];
|
|
215
220
|
let checkedLinks = 0;
|
|
216
221
|
for (const htmlFile of collectHtmlFiles(directory)) {
|
|
@@ -221,30 +226,37 @@ function checkDirectory(directory, options = {}) {
|
|
|
221
226
|
const tree = processor.parse(source);
|
|
222
227
|
const pagePath = routeFromHtmlFile(directory, htmlFile);
|
|
223
228
|
let changed = false;
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
229
|
+
const walk = (parent, insideSkipped) => {
|
|
230
|
+
for (const child of parent.children) {
|
|
231
|
+
if (child.type !== "element")
|
|
232
|
+
continue;
|
|
233
|
+
const skipped = insideSkipped || skipClasses.length > 0 && hasAnyClass(child, skipClasses);
|
|
234
|
+
if (child.tagName === "a" && !skipped && child.properties) {
|
|
235
|
+
const href = child.properties.href;
|
|
236
|
+
if (typeof href === "string" && href.length > 0) {
|
|
237
|
+
const classified = classifyHref(href, {
|
|
238
|
+
base: options.base,
|
|
239
|
+
routes,
|
|
240
|
+
ignore: options.ignore,
|
|
241
|
+
pagePath
|
|
242
|
+
});
|
|
243
|
+
if (classified.type !== "ignored") {
|
|
244
|
+
checkedLinks++;
|
|
245
|
+
if (classified.type === "broken") {
|
|
246
|
+
broken.push({ href, pathname: classified.pathname ?? href, sourceFile: htmlFile });
|
|
247
|
+
if (options.rewrite) {
|
|
248
|
+
removeClass(child, options.internalLinkClass);
|
|
249
|
+
addClass(child, options.brokenLinkClass);
|
|
250
|
+
changed = true;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
walk(child, skipped);
|
|
246
257
|
}
|
|
247
|
-
}
|
|
258
|
+
};
|
|
259
|
+
walk(tree, false);
|
|
248
260
|
if (changed)
|
|
249
261
|
writeFileSync2(htmlFile, processor.stringify(tree));
|
|
250
262
|
}
|
package/dist/cli.cjs
CHANGED
|
@@ -35,7 +35,6 @@ var import_node_path2 = require("path");
|
|
|
35
35
|
var import_rehype_parse = __toESM(require("rehype-parse"), 1);
|
|
36
36
|
var import_rehype_stringify = __toESM(require("rehype-stringify"), 1);
|
|
37
37
|
var import_unified = require("unified");
|
|
38
|
-
var import_unist_util_visit = require("unist-util-visit");
|
|
39
38
|
|
|
40
39
|
// src/core/normalize.ts
|
|
41
40
|
var SCHEME_RE = /^[a-z][a-z0-9+.-]*:/i;
|
|
@@ -212,8 +211,14 @@ function routeFromHtmlFile(distDir, htmlFile) {
|
|
|
212
211
|
relativePath = relativePath.replace(/(?:^|\/)index$/i, "");
|
|
213
212
|
return normalizeRoute(relativePath);
|
|
214
213
|
}
|
|
214
|
+
function hasAnyClass(node, classes) {
|
|
215
|
+
const current = node.properties?.className;
|
|
216
|
+
const list = Array.isArray(current) ? current.map(String) : typeof current === "string" ? current.split(/\s+/) : [];
|
|
217
|
+
return classes.some((className) => list.includes(className));
|
|
218
|
+
}
|
|
215
219
|
function checkDirectory(directory, options = {}) {
|
|
216
220
|
const routes = options.routes ?? scanRoutes(directory, options);
|
|
221
|
+
const skipClasses = options.skipClasses ?? [];
|
|
217
222
|
const broken = [];
|
|
218
223
|
let checkedLinks = 0;
|
|
219
224
|
for (const htmlFile of collectHtmlFiles(directory)) {
|
|
@@ -224,30 +229,37 @@ function checkDirectory(directory, options = {}) {
|
|
|
224
229
|
const tree = processor.parse(source);
|
|
225
230
|
const pagePath = routeFromHtmlFile(directory, htmlFile);
|
|
226
231
|
let changed = false;
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
232
|
+
const walk = (parent, insideSkipped) => {
|
|
233
|
+
for (const child of parent.children) {
|
|
234
|
+
if (child.type !== "element")
|
|
235
|
+
continue;
|
|
236
|
+
const skipped = insideSkipped || skipClasses.length > 0 && hasAnyClass(child, skipClasses);
|
|
237
|
+
if (child.tagName === "a" && !skipped && child.properties) {
|
|
238
|
+
const href = child.properties.href;
|
|
239
|
+
if (typeof href === "string" && href.length > 0) {
|
|
240
|
+
const classified = classifyHref(href, {
|
|
241
|
+
base: options.base,
|
|
242
|
+
routes,
|
|
243
|
+
ignore: options.ignore,
|
|
244
|
+
pagePath
|
|
245
|
+
});
|
|
246
|
+
if (classified.type !== "ignored") {
|
|
247
|
+
checkedLinks++;
|
|
248
|
+
if (classified.type === "broken") {
|
|
249
|
+
broken.push({ href, pathname: classified.pathname ?? href, sourceFile: htmlFile });
|
|
250
|
+
if (options.rewrite) {
|
|
251
|
+
removeClass(child, options.internalLinkClass);
|
|
252
|
+
addClass(child, options.brokenLinkClass);
|
|
253
|
+
changed = true;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
walk(child, skipped);
|
|
249
260
|
}
|
|
250
|
-
}
|
|
261
|
+
};
|
|
262
|
+
walk(tree, false);
|
|
251
263
|
if (changed)
|
|
252
264
|
(0, import_node_fs2.writeFileSync)(htmlFile, processor.stringify(tree));
|
|
253
265
|
}
|
|
@@ -350,8 +362,8 @@ function escapeHtml(value) {
|
|
|
350
362
|
|
|
351
363
|
// src/cli.ts
|
|
352
364
|
var program = new import_commander.Command();
|
|
353
|
-
program.name("astro-smart-links").description("CLI utility for astro-smart-links").version("1.
|
|
354
|
-
program.command("check", { isDefault: true }).description("Check a build directory for broken internal links").option("-d, --dir <path>", "Build directory path", "./dist").option("-o, --output <path>", "Write the report to a file").option("--format <format>", "Report format: json or html", "json").option("--json", "Print the report to stdout as JSON", false).option("-a, --all", "Treat every file type as a valid route", false).option("-e, --extensions <ext...>", "File extensions to treat as routes", ["html"]).option("--fail-on-broken", "Exit with code 1 when broken links are found", false).option("-q, --quiet", "Only print the summary line", false).action((options) => {
|
|
365
|
+
program.name("astro-smart-links").description("CLI utility for astro-smart-links").version("1.1.1");
|
|
366
|
+
program.command("check", { isDefault: true }).description("Check a build directory for broken internal links").option("-d, --dir <path>", "Build directory path", "./dist").option("-o, --output <path>", "Write the report to a file").option("--format <format>", "Report format: json or html", "json").option("--json", "Print the report to stdout as JSON", false).option("-a, --all", "Treat every file type as a valid route", false).option("-e, --extensions <ext...>", "File extensions to treat as routes", ["html"]).option("--skip-classes <class...>", "Skip links inside elements with these classes").option("--fail-on-broken", "Exit with code 1 when broken links are found", false).option("-q, --quiet", "Only print the summary line", false).action((options) => {
|
|
355
367
|
const buildDir = (0, import_node_path3.resolve)(import_node_process.default.cwd(), options.dir);
|
|
356
368
|
const routesFile = options.output ? (0, import_node_path3.resolve)(import_node_process.default.cwd(), options.output) : void 0;
|
|
357
369
|
if (!(0, import_node_fs3.existsSync)(buildDir)) {
|
|
@@ -361,6 +373,7 @@ program.command("check", { isDefault: true }).description("Check a build directo
|
|
|
361
373
|
const { routes, broken, checkedLinks } = checkDirectory(buildDir, {
|
|
362
374
|
includeAllFiles: options.all,
|
|
363
375
|
includeFileExtensions: options.extensions,
|
|
376
|
+
skipClasses: options.skipClasses,
|
|
364
377
|
rewrite: false,
|
|
365
378
|
routes: scanRoutes(buildDir, {
|
|
366
379
|
includeAllFiles: options.all,
|
package/dist/cli.js
CHANGED
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
reportToJSON,
|
|
8
8
|
scanRoutes,
|
|
9
9
|
writeRoutesFile
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-JNP2MFPB.js";
|
|
11
11
|
|
|
12
12
|
// src/cli.ts
|
|
13
13
|
import { existsSync, writeFileSync } from "fs";
|
|
@@ -15,8 +15,8 @@ import { resolve } from "path";
|
|
|
15
15
|
import process from "process";
|
|
16
16
|
import { Command } from "commander";
|
|
17
17
|
var program = new Command();
|
|
18
|
-
program.name("astro-smart-links").description("CLI utility for astro-smart-links").version("1.
|
|
19
|
-
program.command("check", { isDefault: true }).description("Check a build directory for broken internal links").option("-d, --dir <path>", "Build directory path", "./dist").option("-o, --output <path>", "Write the report to a file").option("--format <format>", "Report format: json or html", "json").option("--json", "Print the report to stdout as JSON", false).option("-a, --all", "Treat every file type as a valid route", false).option("-e, --extensions <ext...>", "File extensions to treat as routes", ["html"]).option("--fail-on-broken", "Exit with code 1 when broken links are found", false).option("-q, --quiet", "Only print the summary line", false).action((options) => {
|
|
18
|
+
program.name("astro-smart-links").description("CLI utility for astro-smart-links").version("1.1.1");
|
|
19
|
+
program.command("check", { isDefault: true }).description("Check a build directory for broken internal links").option("-d, --dir <path>", "Build directory path", "./dist").option("-o, --output <path>", "Write the report to a file").option("--format <format>", "Report format: json or html", "json").option("--json", "Print the report to stdout as JSON", false).option("-a, --all", "Treat every file type as a valid route", false).option("-e, --extensions <ext...>", "File extensions to treat as routes", ["html"]).option("--skip-classes <class...>", "Skip links inside elements with these classes").option("--fail-on-broken", "Exit with code 1 when broken links are found", false).option("-q, --quiet", "Only print the summary line", false).action((options) => {
|
|
20
20
|
const buildDir = resolve(process.cwd(), options.dir);
|
|
21
21
|
const routesFile = options.output ? resolve(process.cwd(), options.output) : void 0;
|
|
22
22
|
if (!existsSync(buildDir)) {
|
|
@@ -26,6 +26,7 @@ program.command("check", { isDefault: true }).description("Check a build directo
|
|
|
26
26
|
const { routes, broken, checkedLinks } = checkDirectory(buildDir, {
|
|
27
27
|
includeAllFiles: options.all,
|
|
28
28
|
includeFileExtensions: options.extensions,
|
|
29
|
+
skipClasses: options.skipClasses,
|
|
29
30
|
rewrite: false,
|
|
30
31
|
routes: scanRoutes(buildDir, {
|
|
31
32
|
includeAllFiles: options.all,
|
package/dist/index.cjs
CHANGED
|
@@ -200,7 +200,7 @@ function writeRoutesFile(routes, routesFilePath) {
|
|
|
200
200
|
var import_node_fs4 = require("fs");
|
|
201
201
|
var import_node_path3 = require("path");
|
|
202
202
|
var import_node_process = __toESM(require("process"), 1);
|
|
203
|
-
var
|
|
203
|
+
var import_node_url2 = require("url");
|
|
204
204
|
|
|
205
205
|
// src/core/check.ts
|
|
206
206
|
var import_node_fs2 = require("fs");
|
|
@@ -208,7 +208,6 @@ var import_node_path2 = require("path");
|
|
|
208
208
|
var import_rehype_parse = __toESM(require("rehype-parse"), 1);
|
|
209
209
|
var import_rehype_stringify = __toESM(require("rehype-stringify"), 1);
|
|
210
210
|
var import_unified = require("unified");
|
|
211
|
-
var import_unist_util_visit = require("unist-util-visit");
|
|
212
211
|
|
|
213
212
|
// src/core/classify.ts
|
|
214
213
|
function classifyHref(href, context = {}) {
|
|
@@ -282,8 +281,14 @@ function routeFromHtmlFile(distDir, htmlFile) {
|
|
|
282
281
|
relativePath = relativePath.replace(/(?:^|\/)index$/i, "");
|
|
283
282
|
return normalizeRoute(relativePath);
|
|
284
283
|
}
|
|
284
|
+
function hasAnyClass(node, classes) {
|
|
285
|
+
const current = node.properties?.className;
|
|
286
|
+
const list = Array.isArray(current) ? current.map(String) : typeof current === "string" ? current.split(/\s+/) : [];
|
|
287
|
+
return classes.some((className) => list.includes(className));
|
|
288
|
+
}
|
|
285
289
|
function checkDirectory(directory, options = {}) {
|
|
286
290
|
const routes = options.routes ?? scanRoutes(directory, options);
|
|
291
|
+
const skipClasses = options.skipClasses ?? [];
|
|
287
292
|
const broken = [];
|
|
288
293
|
let checkedLinks = 0;
|
|
289
294
|
for (const htmlFile of collectHtmlFiles(directory)) {
|
|
@@ -294,46 +299,50 @@ function checkDirectory(directory, options = {}) {
|
|
|
294
299
|
const tree = processor.parse(source);
|
|
295
300
|
const pagePath = routeFromHtmlFile(directory, htmlFile);
|
|
296
301
|
let changed = false;
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
302
|
+
const walk = (parent, insideSkipped) => {
|
|
303
|
+
for (const child of parent.children) {
|
|
304
|
+
if (child.type !== "element")
|
|
305
|
+
continue;
|
|
306
|
+
const skipped = insideSkipped || skipClasses.length > 0 && hasAnyClass(child, skipClasses);
|
|
307
|
+
if (child.tagName === "a" && !skipped && child.properties) {
|
|
308
|
+
const href = child.properties.href;
|
|
309
|
+
if (typeof href === "string" && href.length > 0) {
|
|
310
|
+
const classified = classifyHref(href, {
|
|
311
|
+
base: options.base,
|
|
312
|
+
routes,
|
|
313
|
+
ignore: options.ignore,
|
|
314
|
+
pagePath
|
|
315
|
+
});
|
|
316
|
+
if (classified.type !== "ignored") {
|
|
317
|
+
checkedLinks++;
|
|
318
|
+
if (classified.type === "broken") {
|
|
319
|
+
broken.push({ href, pathname: classified.pathname ?? href, sourceFile: htmlFile });
|
|
320
|
+
if (options.rewrite) {
|
|
321
|
+
removeClass(child, options.internalLinkClass);
|
|
322
|
+
addClass(child, options.brokenLinkClass);
|
|
323
|
+
changed = true;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
walk(child, skipped);
|
|
319
330
|
}
|
|
320
|
-
}
|
|
331
|
+
};
|
|
332
|
+
walk(tree, false);
|
|
321
333
|
if (changed)
|
|
322
334
|
(0, import_node_fs2.writeFileSync)(htmlFile, processor.stringify(tree));
|
|
323
335
|
}
|
|
324
336
|
return { routes, broken, checkedLinks };
|
|
325
337
|
}
|
|
326
338
|
|
|
327
|
-
// src/
|
|
328
|
-
var
|
|
339
|
+
// src/core/satteri.ts
|
|
340
|
+
var import_node_url = require("url");
|
|
329
341
|
|
|
330
342
|
// src/core/transform.ts
|
|
331
|
-
var
|
|
332
|
-
function getPagePath(
|
|
333
|
-
if (!pagesDir)
|
|
334
|
-
return void 0;
|
|
335
|
-
const filePath = file?.path ?? file?.history?.[0];
|
|
336
|
-
if (!filePath)
|
|
343
|
+
var import_unist_util_visit = require("unist-util-visit");
|
|
344
|
+
function getPagePath(filePath, pagesDir) {
|
|
345
|
+
if (!pagesDir || !filePath)
|
|
337
346
|
return void 0;
|
|
338
347
|
const normalizedPagesDir = pagesDir.replace(/[\\/]+$/, "");
|
|
339
348
|
if (!filePath.startsWith(normalizedPagesDir))
|
|
@@ -343,94 +352,155 @@ function getPagePath(file, pagesDir) {
|
|
|
343
352
|
relativePath = relativePath.replace(/(?:^|\/)index$/i, "");
|
|
344
353
|
return relativePath ? `/${relativePath}` : "/";
|
|
345
354
|
}
|
|
346
|
-
function
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
355
|
+
function transformAnchor(node, href, context, hooks) {
|
|
356
|
+
const { options, routes } = context;
|
|
357
|
+
const classified = classifyHref(href, {
|
|
358
|
+
base: options.base,
|
|
359
|
+
pagePath: context.pagePath,
|
|
360
|
+
routes,
|
|
361
|
+
ignore: options.ignore
|
|
362
|
+
});
|
|
363
|
+
if (classified.type === "ignored")
|
|
364
|
+
return void 0;
|
|
365
|
+
const type = classified.type;
|
|
366
|
+
const className = type === "internal" ? options.internalLinkClass : type === "external" ? options.externalLinkClass : options.brokenLinkClass;
|
|
367
|
+
const meta = {
|
|
368
|
+
href,
|
|
369
|
+
pathname: classified.pathname,
|
|
370
|
+
className,
|
|
371
|
+
sourceFile: context.sourceFile
|
|
372
|
+
};
|
|
373
|
+
const record = {
|
|
374
|
+
type,
|
|
375
|
+
href,
|
|
376
|
+
pathname: classified.pathname,
|
|
377
|
+
sourceFile: context.sourceFile
|
|
378
|
+
};
|
|
379
|
+
options.onLink?.(record);
|
|
380
|
+
const applyWrapper = () => {
|
|
381
|
+
if (!options.wrapperTemplate)
|
|
382
|
+
return false;
|
|
383
|
+
const result = options.wrapperTemplate(node, type, meta);
|
|
384
|
+
if (result && result !== node)
|
|
385
|
+
hooks.replace(node, result);
|
|
386
|
+
return true;
|
|
387
|
+
};
|
|
388
|
+
if (type === "external") {
|
|
389
|
+
if (options.customExternalLinkTransform) {
|
|
390
|
+
options.customExternalLinkTransform(node, meta);
|
|
391
|
+
return record;
|
|
392
|
+
}
|
|
393
|
+
if (options.target)
|
|
394
|
+
node.properties.target = options.target;
|
|
395
|
+
if (options.rel)
|
|
396
|
+
node.properties.rel = options.rel;
|
|
397
|
+
if (applyWrapper())
|
|
398
|
+
return record;
|
|
399
|
+
addClass(node, options.externalLinkClass);
|
|
400
|
+
if (options.content)
|
|
401
|
+
hooks.appendChild(node, createContentElement(options.contentClass, options.content.value));
|
|
402
|
+
return record;
|
|
403
|
+
}
|
|
404
|
+
if (type === "internal") {
|
|
405
|
+
if (options.customInternalLinkTransform) {
|
|
406
|
+
options.customInternalLinkTransform(node, meta);
|
|
407
|
+
return record;
|
|
408
|
+
}
|
|
409
|
+
if (applyWrapper())
|
|
410
|
+
return record;
|
|
411
|
+
addClass(node, options.internalLinkClass);
|
|
412
|
+
return record;
|
|
413
|
+
}
|
|
414
|
+
if (options.customBrokenLinkTransform) {
|
|
415
|
+
options.customBrokenLinkTransform(node, meta);
|
|
416
|
+
return record;
|
|
417
|
+
}
|
|
418
|
+
if (applyWrapper())
|
|
419
|
+
return record;
|
|
420
|
+
removeClass(node, options.internalLinkClass);
|
|
421
|
+
addClass(node, options.brokenLinkClass);
|
|
422
|
+
return record;
|
|
357
423
|
}
|
|
358
424
|
function transformTree(tree, file, context) {
|
|
359
|
-
const
|
|
360
|
-
const pagePath = context.pagePath ?? getPagePath(
|
|
361
|
-
const sourceFile = file?.path ?? file?.history?.[0];
|
|
425
|
+
const sourceFile = context.sourceFile ?? file?.path ?? file?.history?.[0];
|
|
426
|
+
const pagePath = context.pagePath ?? getPagePath(sourceFile, context.options.pagesDir);
|
|
362
427
|
const counts = { internal: 0, external: 0, broken: 0 };
|
|
363
428
|
const records = [];
|
|
364
|
-
(0,
|
|
429
|
+
(0, import_unist_util_visit.visit)(tree, "element", (node, index, parent) => {
|
|
365
430
|
if (node.tagName !== "a" || !node.properties)
|
|
366
431
|
return;
|
|
367
432
|
const href = node.properties.href;
|
|
368
433
|
if (typeof href !== "string" || href.length === 0)
|
|
369
434
|
return;
|
|
370
|
-
const
|
|
371
|
-
|
|
372
|
-
pagePath,
|
|
373
|
-
routes,
|
|
374
|
-
ignore: options.ignore
|
|
375
|
-
});
|
|
376
|
-
if (classified.type === "ignored")
|
|
377
|
-
return;
|
|
378
|
-
const type = classified.type;
|
|
379
|
-
const className = type === "internal" ? options.internalLinkClass : type === "external" ? options.externalLinkClass : options.brokenLinkClass;
|
|
380
|
-
const meta = {
|
|
435
|
+
const record = transformAnchor(
|
|
436
|
+
node,
|
|
381
437
|
href,
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
if (options.customExternalLinkTransform) {
|
|
392
|
-
options.customExternalLinkTransform(node, meta);
|
|
393
|
-
return;
|
|
394
|
-
}
|
|
395
|
-
if (options.target)
|
|
396
|
-
node.properties.target = options.target;
|
|
397
|
-
if (options.rel)
|
|
398
|
-
node.properties.rel = options.rel;
|
|
399
|
-
if (options.wrapperTemplate) {
|
|
400
|
-
applyWrapper(node, "external", options.externalLinkClass, meta, options, parent, index);
|
|
401
|
-
return;
|
|
402
|
-
}
|
|
403
|
-
addClass(node, options.externalLinkClass);
|
|
404
|
-
if (options.content)
|
|
405
|
-
node.children.push(createContentElement(options.contentClass, options.content.value));
|
|
406
|
-
return;
|
|
407
|
-
}
|
|
408
|
-
if (type === "internal") {
|
|
409
|
-
if (options.customInternalLinkTransform) {
|
|
410
|
-
options.customInternalLinkTransform(node, meta);
|
|
411
|
-
return;
|
|
412
|
-
}
|
|
413
|
-
if (options.wrapperTemplate) {
|
|
414
|
-
applyWrapper(node, "internal", options.internalLinkClass, meta, options, parent, index);
|
|
415
|
-
return;
|
|
438
|
+
{ ...context, pagePath, sourceFile },
|
|
439
|
+
{
|
|
440
|
+
replace: (_target, replacement) => {
|
|
441
|
+
if (parent && typeof index === "number")
|
|
442
|
+
parent.children[index] = replacement;
|
|
443
|
+
},
|
|
444
|
+
appendChild: (target, child) => {
|
|
445
|
+
target.children.push(child);
|
|
446
|
+
}
|
|
416
447
|
}
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
options.customBrokenLinkTransform(node, meta);
|
|
422
|
-
return;
|
|
423
|
-
}
|
|
424
|
-
if (options.wrapperTemplate) {
|
|
425
|
-
applyWrapper(node, "broken", options.brokenLinkClass, meta, options, parent, index);
|
|
426
|
-
return;
|
|
448
|
+
);
|
|
449
|
+
if (record) {
|
|
450
|
+
counts[record.type]++;
|
|
451
|
+
records.push(record);
|
|
427
452
|
}
|
|
428
|
-
removeClass(node, options.internalLinkClass);
|
|
429
|
-
addClass(node, options.brokenLinkClass);
|
|
430
453
|
});
|
|
431
454
|
return { counts, records };
|
|
432
455
|
}
|
|
433
456
|
|
|
457
|
+
// src/core/satteri.ts
|
|
458
|
+
function cloneHastNode(node) {
|
|
459
|
+
const clone = { ...node };
|
|
460
|
+
if (Array.isArray(clone.children))
|
|
461
|
+
clone.children = clone.children.map((child) => cloneHastNode(child));
|
|
462
|
+
if (clone.properties && typeof clone.properties === "object")
|
|
463
|
+
clone.properties = { ...clone.properties };
|
|
464
|
+
return clone;
|
|
465
|
+
}
|
|
466
|
+
function satteriSmartLinksPlugin(options, routes) {
|
|
467
|
+
return ({ fileURL }) => {
|
|
468
|
+
const sourceFile = fileURL ? (0, import_node_url.fileURLToPath)(fileURL) : void 0;
|
|
469
|
+
const pagePath = getPagePath(sourceFile, options.pagesDir);
|
|
470
|
+
return {
|
|
471
|
+
name: "astro-smart-links",
|
|
472
|
+
element: {
|
|
473
|
+
filter: ["a"],
|
|
474
|
+
visit: (node, ctx) => {
|
|
475
|
+
const href = node.properties?.href;
|
|
476
|
+
if (typeof href !== "string" || href.length === 0)
|
|
477
|
+
return;
|
|
478
|
+
const clone = cloneHastNode(node);
|
|
479
|
+
let result = clone;
|
|
480
|
+
transformAnchor(
|
|
481
|
+
clone,
|
|
482
|
+
href,
|
|
483
|
+
{ options, routes, pagePath, sourceFile },
|
|
484
|
+
{
|
|
485
|
+
replace: (_target, replacement) => {
|
|
486
|
+
result = replacement;
|
|
487
|
+
},
|
|
488
|
+
appendChild: (target, child) => {
|
|
489
|
+
target.children.push(child);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
);
|
|
493
|
+
if (result !== node)
|
|
494
|
+
ctx.replaceNode(node, result);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
};
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// src/rehype.ts
|
|
502
|
+
var import_node_fs3 = require("fs");
|
|
503
|
+
|
|
434
504
|
// src/types.ts
|
|
435
505
|
var defaultOptions = {
|
|
436
506
|
content: { type: "text", value: "\u2197" },
|
|
@@ -639,7 +709,7 @@ function smartLinks(options = {}) {
|
|
|
639
709
|
hooks: {
|
|
640
710
|
"astro:config:setup": ({ config, updateConfig, logger }) => {
|
|
641
711
|
records.length = 0;
|
|
642
|
-
projectRoot = (0,
|
|
712
|
+
projectRoot = (0, import_node_url2.fileURLToPath)(config.root);
|
|
643
713
|
const adapter = {
|
|
644
714
|
debug: (message) => logger.debug(message),
|
|
645
715
|
info: (message) => logger.info(message),
|
|
@@ -655,29 +725,37 @@ function smartLinks(options = {}) {
|
|
|
655
725
|
resolved = resolveOptions({
|
|
656
726
|
...linkOptions,
|
|
657
727
|
base: config.base,
|
|
658
|
-
pagesDir: (0,
|
|
728
|
+
pagesDir: (0, import_node_url2.fileURLToPath)(new URL("pages/", config.srcDir)),
|
|
659
729
|
logger: adapter,
|
|
660
730
|
onLink: (record) => {
|
|
661
731
|
records.push(record);
|
|
662
732
|
linkOptions.onLink?.(record);
|
|
663
733
|
}
|
|
664
734
|
});
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
735
|
+
const processor = config.markdown?.processor;
|
|
736
|
+
if (processor?.options && Array.isArray(processor.options.hastPlugins)) {
|
|
737
|
+
processor.options.hastPlugins.push(satteriSmartLinksPlugin(resolved));
|
|
738
|
+
} else if (processor?.options && Array.isArray(processor.options.rehypePlugins)) {
|
|
739
|
+
processor.options.rehypePlugins.push([rehypeSmartLinks, resolved]);
|
|
740
|
+
} else {
|
|
741
|
+
updateConfig({
|
|
742
|
+
markdown: {
|
|
743
|
+
rehypePlugins: [
|
|
744
|
+
[rehypeSmartLinks, resolved]
|
|
745
|
+
]
|
|
746
|
+
}
|
|
747
|
+
});
|
|
748
|
+
}
|
|
672
749
|
},
|
|
673
750
|
"astro:build:done": async ({ dir, logger }) => {
|
|
674
751
|
const current = resolved;
|
|
675
752
|
if (!current)
|
|
676
753
|
return;
|
|
677
|
-
const distDir = (0,
|
|
754
|
+
const distDir = (0, import_node_url2.fileURLToPath)(dir);
|
|
678
755
|
const { routes, broken } = checkDirectory(distDir, {
|
|
679
756
|
base: current.base,
|
|
680
757
|
ignore: current.ignore,
|
|
758
|
+
skipClasses: current.skipClasses,
|
|
681
759
|
includeAllFiles: current.includeAllFiles,
|
|
682
760
|
includeFileExtensions: current.includeFileExtensions,
|
|
683
761
|
rewrite: true,
|
package/dist/index.d.cts
CHANGED
|
@@ -56,6 +56,12 @@ interface SmartLinksOptions {
|
|
|
56
56
|
* normalized pathname (for strings: prefix match).
|
|
57
57
|
*/
|
|
58
58
|
ignore?: (string | RegExp)[];
|
|
59
|
+
/**
|
|
60
|
+
* Links inside elements carrying one of these classes are skipped by the
|
|
61
|
+
* build-time check and are never rewritten. Useful for component or demo
|
|
62
|
+
* markup that opts out of content styles (e.g. Starlight's `not-content`).
|
|
63
|
+
*/
|
|
64
|
+
skipClasses?: string[];
|
|
59
65
|
/** Explicit list of known routes. When provided, broken links are detected. */
|
|
60
66
|
routes?: string[];
|
|
61
67
|
/** Path to a JSON file containing a list of known routes. */
|
|
@@ -147,6 +153,12 @@ interface CheckDirectoryOptions extends ScanRoutesOptions {
|
|
|
147
153
|
/** Known routes. When omitted, routes are collected by scanning the directory. */
|
|
148
154
|
routes?: Set<string>;
|
|
149
155
|
ignore?: (string | RegExp)[];
|
|
156
|
+
/**
|
|
157
|
+
* Links inside elements carrying one of these classes are skipped entirely:
|
|
158
|
+
* they are not checked and never rewritten. Useful for component or demo
|
|
159
|
+
* markup that opts out of content styles (e.g. Starlight's `not-content`).
|
|
160
|
+
*/
|
|
161
|
+
skipClasses?: string[];
|
|
150
162
|
/** Rewrite generated HTML so broken links receive `brokenLinkClass`. */
|
|
151
163
|
rewrite?: boolean;
|
|
152
164
|
internalLinkClass?: string;
|
package/dist/index.d.ts
CHANGED
|
@@ -56,6 +56,12 @@ interface SmartLinksOptions {
|
|
|
56
56
|
* normalized pathname (for strings: prefix match).
|
|
57
57
|
*/
|
|
58
58
|
ignore?: (string | RegExp)[];
|
|
59
|
+
/**
|
|
60
|
+
* Links inside elements carrying one of these classes are skipped by the
|
|
61
|
+
* build-time check and are never rewritten. Useful for component or demo
|
|
62
|
+
* markup that opts out of content styles (e.g. Starlight's `not-content`).
|
|
63
|
+
*/
|
|
64
|
+
skipClasses?: string[];
|
|
59
65
|
/** Explicit list of known routes. When provided, broken links are detected. */
|
|
60
66
|
routes?: string[];
|
|
61
67
|
/** Path to a JSON file containing a list of known routes. */
|
|
@@ -147,6 +153,12 @@ interface CheckDirectoryOptions extends ScanRoutesOptions {
|
|
|
147
153
|
/** Known routes. When omitted, routes are collected by scanning the directory. */
|
|
148
154
|
routes?: Set<string>;
|
|
149
155
|
ignore?: (string | RegExp)[];
|
|
156
|
+
/**
|
|
157
|
+
* Links inside elements carrying one of these classes are skipped entirely:
|
|
158
|
+
* they are not checked and never rewritten. Useful for component or demo
|
|
159
|
+
* markup that opts out of content styles (e.g. Starlight's `not-content`).
|
|
160
|
+
*/
|
|
161
|
+
skipClasses?: string[];
|
|
150
162
|
/** Rewrite generated HTML so broken links receive `brokenLinkClass`. */
|
|
151
163
|
rewrite?: boolean;
|
|
152
164
|
internalLinkClass?: string;
|
package/dist/index.js
CHANGED
|
@@ -20,24 +20,21 @@ import {
|
|
|
20
20
|
routeFromHtmlFile,
|
|
21
21
|
scanRoutes,
|
|
22
22
|
writeRoutesFile
|
|
23
|
-
} from "./chunk-
|
|
23
|
+
} from "./chunk-JNP2MFPB.js";
|
|
24
24
|
|
|
25
25
|
// src/integration.ts
|
|
26
26
|
import { writeFileSync } from "fs";
|
|
27
27
|
import { resolve } from "path";
|
|
28
28
|
import process from "process";
|
|
29
|
-
import { fileURLToPath } from "url";
|
|
29
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
30
30
|
|
|
31
|
-
// src/
|
|
32
|
-
import {
|
|
31
|
+
// src/core/satteri.ts
|
|
32
|
+
import { fileURLToPath } from "url";
|
|
33
33
|
|
|
34
34
|
// src/core/transform.ts
|
|
35
35
|
import { visit } from "unist-util-visit";
|
|
36
|
-
function getPagePath(
|
|
37
|
-
if (!pagesDir)
|
|
38
|
-
return void 0;
|
|
39
|
-
const filePath = file?.path ?? file?.history?.[0];
|
|
40
|
-
if (!filePath)
|
|
36
|
+
function getPagePath(filePath, pagesDir) {
|
|
37
|
+
if (!pagesDir || !filePath)
|
|
41
38
|
return void 0;
|
|
42
39
|
const normalizedPagesDir = pagesDir.replace(/[\\/]+$/, "");
|
|
43
40
|
if (!filePath.startsWith(normalizedPagesDir))
|
|
@@ -47,22 +44,78 @@ function getPagePath(file, pagesDir) {
|
|
|
47
44
|
relativePath = relativePath.replace(/(?:^|\/)index$/i, "");
|
|
48
45
|
return relativePath ? `/${relativePath}` : "/";
|
|
49
46
|
}
|
|
50
|
-
function
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
47
|
+
function transformAnchor(node, href, context, hooks) {
|
|
48
|
+
const { options, routes } = context;
|
|
49
|
+
const classified = classifyHref(href, {
|
|
50
|
+
base: options.base,
|
|
51
|
+
pagePath: context.pagePath,
|
|
52
|
+
routes,
|
|
53
|
+
ignore: options.ignore
|
|
54
|
+
});
|
|
55
|
+
if (classified.type === "ignored")
|
|
56
|
+
return void 0;
|
|
57
|
+
const type = classified.type;
|
|
58
|
+
const className = type === "internal" ? options.internalLinkClass : type === "external" ? options.externalLinkClass : options.brokenLinkClass;
|
|
59
|
+
const meta = {
|
|
60
|
+
href,
|
|
61
|
+
pathname: classified.pathname,
|
|
62
|
+
className,
|
|
63
|
+
sourceFile: context.sourceFile
|
|
64
|
+
};
|
|
65
|
+
const record = {
|
|
66
|
+
type,
|
|
67
|
+
href,
|
|
68
|
+
pathname: classified.pathname,
|
|
69
|
+
sourceFile: context.sourceFile
|
|
70
|
+
};
|
|
71
|
+
options.onLink?.(record);
|
|
72
|
+
const applyWrapper = () => {
|
|
73
|
+
if (!options.wrapperTemplate)
|
|
74
|
+
return false;
|
|
75
|
+
const result = options.wrapperTemplate(node, type, meta);
|
|
76
|
+
if (result && result !== node)
|
|
77
|
+
hooks.replace(node, result);
|
|
78
|
+
return true;
|
|
79
|
+
};
|
|
80
|
+
if (type === "external") {
|
|
81
|
+
if (options.customExternalLinkTransform) {
|
|
82
|
+
options.customExternalLinkTransform(node, meta);
|
|
83
|
+
return record;
|
|
84
|
+
}
|
|
85
|
+
if (options.target)
|
|
86
|
+
node.properties.target = options.target;
|
|
87
|
+
if (options.rel)
|
|
88
|
+
node.properties.rel = options.rel;
|
|
89
|
+
if (applyWrapper())
|
|
90
|
+
return record;
|
|
91
|
+
addClass(node, options.externalLinkClass);
|
|
92
|
+
if (options.content)
|
|
93
|
+
hooks.appendChild(node, createContentElement(options.contentClass, options.content.value));
|
|
94
|
+
return record;
|
|
95
|
+
}
|
|
96
|
+
if (type === "internal") {
|
|
97
|
+
if (options.customInternalLinkTransform) {
|
|
98
|
+
options.customInternalLinkTransform(node, meta);
|
|
99
|
+
return record;
|
|
100
|
+
}
|
|
101
|
+
if (applyWrapper())
|
|
102
|
+
return record;
|
|
103
|
+
addClass(node, options.internalLinkClass);
|
|
104
|
+
return record;
|
|
105
|
+
}
|
|
106
|
+
if (options.customBrokenLinkTransform) {
|
|
107
|
+
options.customBrokenLinkTransform(node, meta);
|
|
108
|
+
return record;
|
|
109
|
+
}
|
|
110
|
+
if (applyWrapper())
|
|
111
|
+
return record;
|
|
112
|
+
removeClass(node, options.internalLinkClass);
|
|
113
|
+
addClass(node, options.brokenLinkClass);
|
|
114
|
+
return record;
|
|
61
115
|
}
|
|
62
116
|
function transformTree(tree, file, context) {
|
|
63
|
-
const
|
|
64
|
-
const pagePath = context.pagePath ?? getPagePath(
|
|
65
|
-
const sourceFile = file?.path ?? file?.history?.[0];
|
|
117
|
+
const sourceFile = context.sourceFile ?? file?.path ?? file?.history?.[0];
|
|
118
|
+
const pagePath = context.pagePath ?? getPagePath(sourceFile, context.options.pagesDir);
|
|
66
119
|
const counts = { internal: 0, external: 0, broken: 0 };
|
|
67
120
|
const records = [];
|
|
68
121
|
visit(tree, "element", (node, index, parent) => {
|
|
@@ -71,70 +124,75 @@ function transformTree(tree, file, context) {
|
|
|
71
124
|
const href = node.properties.href;
|
|
72
125
|
if (typeof href !== "string" || href.length === 0)
|
|
73
126
|
return;
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
pagePath,
|
|
77
|
-
routes,
|
|
78
|
-
ignore: options.ignore
|
|
79
|
-
});
|
|
80
|
-
if (classified.type === "ignored")
|
|
81
|
-
return;
|
|
82
|
-
const type = classified.type;
|
|
83
|
-
const className = type === "internal" ? options.internalLinkClass : type === "external" ? options.externalLinkClass : options.brokenLinkClass;
|
|
84
|
-
const meta = {
|
|
127
|
+
const record = transformAnchor(
|
|
128
|
+
node,
|
|
85
129
|
href,
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
if (options.customExternalLinkTransform) {
|
|
96
|
-
options.customExternalLinkTransform(node, meta);
|
|
97
|
-
return;
|
|
98
|
-
}
|
|
99
|
-
if (options.target)
|
|
100
|
-
node.properties.target = options.target;
|
|
101
|
-
if (options.rel)
|
|
102
|
-
node.properties.rel = options.rel;
|
|
103
|
-
if (options.wrapperTemplate) {
|
|
104
|
-
applyWrapper(node, "external", options.externalLinkClass, meta, options, parent, index);
|
|
105
|
-
return;
|
|
106
|
-
}
|
|
107
|
-
addClass(node, options.externalLinkClass);
|
|
108
|
-
if (options.content)
|
|
109
|
-
node.children.push(createContentElement(options.contentClass, options.content.value));
|
|
110
|
-
return;
|
|
111
|
-
}
|
|
112
|
-
if (type === "internal") {
|
|
113
|
-
if (options.customInternalLinkTransform) {
|
|
114
|
-
options.customInternalLinkTransform(node, meta);
|
|
115
|
-
return;
|
|
116
|
-
}
|
|
117
|
-
if (options.wrapperTemplate) {
|
|
118
|
-
applyWrapper(node, "internal", options.internalLinkClass, meta, options, parent, index);
|
|
119
|
-
return;
|
|
130
|
+
{ ...context, pagePath, sourceFile },
|
|
131
|
+
{
|
|
132
|
+
replace: (_target, replacement) => {
|
|
133
|
+
if (parent && typeof index === "number")
|
|
134
|
+
parent.children[index] = replacement;
|
|
135
|
+
},
|
|
136
|
+
appendChild: (target, child) => {
|
|
137
|
+
target.children.push(child);
|
|
138
|
+
}
|
|
120
139
|
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
options.customBrokenLinkTransform(node, meta);
|
|
126
|
-
return;
|
|
127
|
-
}
|
|
128
|
-
if (options.wrapperTemplate) {
|
|
129
|
-
applyWrapper(node, "broken", options.brokenLinkClass, meta, options, parent, index);
|
|
130
|
-
return;
|
|
140
|
+
);
|
|
141
|
+
if (record) {
|
|
142
|
+
counts[record.type]++;
|
|
143
|
+
records.push(record);
|
|
131
144
|
}
|
|
132
|
-
removeClass(node, options.internalLinkClass);
|
|
133
|
-
addClass(node, options.brokenLinkClass);
|
|
134
145
|
});
|
|
135
146
|
return { counts, records };
|
|
136
147
|
}
|
|
137
148
|
|
|
149
|
+
// src/core/satteri.ts
|
|
150
|
+
function cloneHastNode(node) {
|
|
151
|
+
const clone = { ...node };
|
|
152
|
+
if (Array.isArray(clone.children))
|
|
153
|
+
clone.children = clone.children.map((child) => cloneHastNode(child));
|
|
154
|
+
if (clone.properties && typeof clone.properties === "object")
|
|
155
|
+
clone.properties = { ...clone.properties };
|
|
156
|
+
return clone;
|
|
157
|
+
}
|
|
158
|
+
function satteriSmartLinksPlugin(options, routes) {
|
|
159
|
+
return ({ fileURL }) => {
|
|
160
|
+
const sourceFile = fileURL ? fileURLToPath(fileURL) : void 0;
|
|
161
|
+
const pagePath = getPagePath(sourceFile, options.pagesDir);
|
|
162
|
+
return {
|
|
163
|
+
name: "astro-smart-links",
|
|
164
|
+
element: {
|
|
165
|
+
filter: ["a"],
|
|
166
|
+
visit: (node, ctx) => {
|
|
167
|
+
const href = node.properties?.href;
|
|
168
|
+
if (typeof href !== "string" || href.length === 0)
|
|
169
|
+
return;
|
|
170
|
+
const clone = cloneHastNode(node);
|
|
171
|
+
let result = clone;
|
|
172
|
+
transformAnchor(
|
|
173
|
+
clone,
|
|
174
|
+
href,
|
|
175
|
+
{ options, routes, pagePath, sourceFile },
|
|
176
|
+
{
|
|
177
|
+
replace: (_target, replacement) => {
|
|
178
|
+
result = replacement;
|
|
179
|
+
},
|
|
180
|
+
appendChild: (target, child) => {
|
|
181
|
+
target.children.push(child);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
);
|
|
185
|
+
if (result !== node)
|
|
186
|
+
ctx.replaceNode(node, result);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// src/rehype.ts
|
|
194
|
+
import { existsSync } from "fs";
|
|
195
|
+
|
|
138
196
|
// src/types.ts
|
|
139
197
|
var defaultOptions = {
|
|
140
198
|
content: { type: "text", value: "\u2197" },
|
|
@@ -249,7 +307,7 @@ function smartLinks(options = {}) {
|
|
|
249
307
|
hooks: {
|
|
250
308
|
"astro:config:setup": ({ config, updateConfig, logger }) => {
|
|
251
309
|
records.length = 0;
|
|
252
|
-
projectRoot =
|
|
310
|
+
projectRoot = fileURLToPath2(config.root);
|
|
253
311
|
const adapter = {
|
|
254
312
|
debug: (message) => logger.debug(message),
|
|
255
313
|
info: (message) => logger.info(message),
|
|
@@ -265,29 +323,37 @@ function smartLinks(options = {}) {
|
|
|
265
323
|
resolved = resolveOptions({
|
|
266
324
|
...linkOptions,
|
|
267
325
|
base: config.base,
|
|
268
|
-
pagesDir:
|
|
326
|
+
pagesDir: fileURLToPath2(new URL("pages/", config.srcDir)),
|
|
269
327
|
logger: adapter,
|
|
270
328
|
onLink: (record) => {
|
|
271
329
|
records.push(record);
|
|
272
330
|
linkOptions.onLink?.(record);
|
|
273
331
|
}
|
|
274
332
|
});
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
333
|
+
const processor = config.markdown?.processor;
|
|
334
|
+
if (processor?.options && Array.isArray(processor.options.hastPlugins)) {
|
|
335
|
+
processor.options.hastPlugins.push(satteriSmartLinksPlugin(resolved));
|
|
336
|
+
} else if (processor?.options && Array.isArray(processor.options.rehypePlugins)) {
|
|
337
|
+
processor.options.rehypePlugins.push([rehypeSmartLinks, resolved]);
|
|
338
|
+
} else {
|
|
339
|
+
updateConfig({
|
|
340
|
+
markdown: {
|
|
341
|
+
rehypePlugins: [
|
|
342
|
+
[rehypeSmartLinks, resolved]
|
|
343
|
+
]
|
|
344
|
+
}
|
|
345
|
+
});
|
|
346
|
+
}
|
|
282
347
|
},
|
|
283
348
|
"astro:build:done": async ({ dir, logger }) => {
|
|
284
349
|
const current = resolved;
|
|
285
350
|
if (!current)
|
|
286
351
|
return;
|
|
287
|
-
const distDir =
|
|
352
|
+
const distDir = fileURLToPath2(dir);
|
|
288
353
|
const { routes, broken } = checkDirectory(distDir, {
|
|
289
354
|
base: current.base,
|
|
290
355
|
ignore: current.ignore,
|
|
356
|
+
skipClasses: current.skipClasses,
|
|
291
357
|
includeAllFiles: current.includeAllFiles,
|
|
292
358
|
includeFileExtensions: current.includeFileExtensions,
|
|
293
359
|
rewrite: true,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "astro-smart-links",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.1.1",
|
|
5
5
|
"packageManager": "pnpm@11.8.0",
|
|
6
6
|
"description": "An Astro integration that adds smart styling and broken-link detection to internal and external links",
|
|
7
7
|
"author": "EveSunMaple",
|
|
@@ -80,11 +80,11 @@
|
|
|
80
80
|
},
|
|
81
81
|
"devDependencies": {
|
|
82
82
|
"@antfu/eslint-config": "^3.16.0",
|
|
83
|
-
"@types/node": "^
|
|
83
|
+
"@types/node": "^24.0.0",
|
|
84
84
|
"@typescript-eslint/eslint-plugin": "^8.29.0",
|
|
85
85
|
"@typescript-eslint/parser": "^8.29.0",
|
|
86
86
|
"@vitest/ui": "3.1.1",
|
|
87
|
-
"astro": "^
|
|
87
|
+
"astro": "^7.3.6",
|
|
88
88
|
"cheerio": "^1.0.0",
|
|
89
89
|
"eslint": "^9.24.0",
|
|
90
90
|
"eslint-plugin-astro": "^1.3.1",
|