@readme/cli 0.0.31 → 0.2.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/README.md +10 -0
- package/bin/readme.js +1 -1
- package/package.json +34 -24
- package/src/bootstrap.js +9 -13
- package/src/cli.js +17 -11
- package/src/commands/components.js +35 -18
- package/src/commands/dev.js +12 -8
- package/src/commands/eyes.js +12 -8
- package/src/commands/import.js +2263 -2066
- package/src/commands/lint.js +33 -29
- package/src/commands/oas-bundle.js +89 -0
- package/src/commands/oas-sync.js +96 -93
- package/src/commands/oas-validate.js +10 -7
- package/src/commands/pretty.js +177 -164
- package/src/commands/setup.js +24 -26
- package/src/commands/versions.js +6 -2
- package/src/dev/app/[...slug]/page.js +1 -0
- package/src/dev/app/layout.js +18 -11
- package/src/dev/app/page.js +1 -0
- package/src/dev/lib/docs.js +27 -19
- package/src/dev/next.config.mjs +2 -2
- package/src/gha.js +7 -7
- package/src/prompts/index.js +16 -6
- package/src/utils/eyes.js +143 -51
- package/src/utils/git.js +3 -3
- package/src/utils/lint.js +39 -38
- package/src/utils/llms.js +94 -91
- package/src/utils/markdown.js +256 -0
- package/src/utils/marketplace.js +12 -5
- package/src/utils/multi-spinner.js +5 -7
- package/src/utils/promise-pool.js +7 -5
- package/src/utils/reporter.js +88 -91
- package/src/utils/setup-templates.js +12 -6
- package/src/utils/styles.js +2 -1
- package/src/utils/tamagotchi.js +257 -91
- package/src/utils/tips.js +18 -9
- package/src/utils/url-segs.js +46 -41
- package/src/validators/components.js +55 -63
- package/src/validators/content.js +5 -10
- package/src/validators/duplicates.js +6 -2
- package/src/validators/frontmatter.js +19 -21
- package/src/validators/links.js +4 -8
- package/src/validators/oas-reference.js +7 -14
- package/src/validators/oas-schema.js +2 -0
- package/src/validators/ordering.js +5 -5
- package/src/validators/recipes.js +4 -12
- package/src/commands/import-oas.test.js +0 -277
- package/src/commands/import.classify.test.js +0 -172
- package/src/commands/import.landing.test.js +0 -217
- package/src/commands/import.slug-planner.test.js +0 -86
- package/src/commands/import.staging.test.js +0 -158
- package/src/commands/import.test.js +0 -382
- package/src/utils/llms.test.js +0 -302
- package/src/utils/url-segs.test.js +0 -239
package/src/utils/lint.js
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
|
-
import fs from 'node:fs'
|
|
2
|
-
import path from 'node:path'
|
|
3
|
-
|
|
4
|
-
import * as
|
|
5
|
-
import * as
|
|
6
|
-
import * as
|
|
7
|
-
import * as
|
|
8
|
-
import * as
|
|
9
|
-
import * as
|
|
10
|
-
import * as
|
|
11
|
-
import * as
|
|
12
|
-
import * as
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
import * as componentsValidator from '../validators/components.js';
|
|
5
|
+
import * as contentValidator from '../validators/content.js';
|
|
6
|
+
import * as duplicatesValidator from '../validators/duplicates.js';
|
|
7
|
+
import * as frontmatterValidator from '../validators/frontmatter.js';
|
|
8
|
+
import * as linksValidator from '../validators/links.js';
|
|
9
|
+
import * as nestingValidator from '../validators/nesting.js';
|
|
10
|
+
import * as oasReferenceValidator from '../validators/oas-reference.js';
|
|
11
|
+
import * as oasSchemaValidator from '../validators/oas-schema.js';
|
|
12
|
+
import * as orderingValidator from '../validators/ordering.js';
|
|
13
|
+
import * as recipesValidator from '../validators/recipes.js';
|
|
13
14
|
|
|
14
15
|
const TARGET_PATTERNS = [
|
|
15
16
|
{ dir: 'custom_blocks', ext: ['.mdx', '.md'] },
|
|
@@ -17,28 +18,28 @@ const TARGET_PATTERNS = [
|
|
|
17
18
|
{ dir: 'reference', ext: '.md' },
|
|
18
19
|
{ dir: 'custom_pages', ext: '.md' },
|
|
19
20
|
{ dir: 'recipes', ext: '.md' },
|
|
20
|
-
]
|
|
21
|
+
];
|
|
21
22
|
|
|
22
23
|
/**
|
|
23
24
|
* Collect all target files from the repo root.
|
|
24
25
|
*/
|
|
25
26
|
export function collectFiles(gitRoot) {
|
|
26
|
-
const files = []
|
|
27
|
+
const files = [];
|
|
27
28
|
|
|
28
29
|
for (const { dir, ext } of TARGET_PATTERNS) {
|
|
29
|
-
const dirPath = path.join(gitRoot, dir)
|
|
30
|
-
if (!fs.existsSync(dirPath)) continue
|
|
30
|
+
const dirPath = path.join(gitRoot, dir);
|
|
31
|
+
if (!fs.existsSync(dirPath)) continue;
|
|
31
32
|
|
|
32
|
-
const exts = Array.isArray(ext) ? ext : [ext]
|
|
33
|
-
const entries = fs.readdirSync(dirPath, { recursive: true })
|
|
33
|
+
const exts = Array.isArray(ext) ? ext : [ext];
|
|
34
|
+
const entries = fs.readdirSync(dirPath, { recursive: true });
|
|
34
35
|
for (const entry of entries) {
|
|
35
|
-
if (exts.some(
|
|
36
|
-
files.push(path.join(dir, entry))
|
|
36
|
+
if (exts.some(e => entry.endsWith(e))) {
|
|
37
|
+
files.push(path.join(dir, entry));
|
|
37
38
|
}
|
|
38
39
|
}
|
|
39
40
|
}
|
|
40
41
|
|
|
41
|
-
return files.sort()
|
|
42
|
+
return files.sort();
|
|
42
43
|
}
|
|
43
44
|
|
|
44
45
|
// Every validator in src/validators/, imported statically (rather than
|
|
@@ -57,14 +58,14 @@ const ALL_VALIDATORS = [
|
|
|
57
58
|
oasSchemaValidator,
|
|
58
59
|
orderingValidator,
|
|
59
60
|
recipesValidator,
|
|
60
|
-
]
|
|
61
|
+
];
|
|
61
62
|
|
|
62
63
|
/**
|
|
63
64
|
* Validators can export `validate()` for per-file checks and/or
|
|
64
65
|
* `validateAll()` for cross-file checks (like ordering).
|
|
65
66
|
*/
|
|
66
67
|
function loadValidators() {
|
|
67
|
-
return ALL_VALIDATORS.filter(
|
|
68
|
+
return ALL_VALIDATORS.filter(mod => mod.name && (mod.validate || mod.validateAll));
|
|
68
69
|
}
|
|
69
70
|
|
|
70
71
|
/**
|
|
@@ -73,42 +74,42 @@ function loadValidators() {
|
|
|
73
74
|
* Calls `onFile(relativePath)` before processing each file (for progress reporting).
|
|
74
75
|
*/
|
|
75
76
|
export async function runValidators(files, gitRoot, { onFile, onBeforeCrossFile, fix, nonInteractive } = {}) {
|
|
76
|
-
const validators = await loadValidators()
|
|
77
|
-
const results = []
|
|
77
|
+
const validators = await loadValidators();
|
|
78
|
+
const results = [];
|
|
78
79
|
|
|
79
80
|
// Per-file validators.
|
|
80
81
|
for (const relativePath of files) {
|
|
81
|
-
if (onFile) onFile(relativePath)
|
|
82
|
+
if (onFile) onFile(relativePath);
|
|
82
83
|
|
|
83
|
-
const filePath = path.join(gitRoot, relativePath)
|
|
84
|
-
const content = fs.readFileSync(filePath, 'utf-8')
|
|
84
|
+
const filePath = path.join(gitRoot, relativePath);
|
|
85
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
85
86
|
|
|
86
87
|
for (const validator of validators) {
|
|
87
|
-
if (!validator.validate) continue
|
|
88
|
-
const result = validator.validate({ filePath, content, relativePath, fix })
|
|
88
|
+
if (!validator.validate) continue;
|
|
89
|
+
const result = validator.validate({ filePath, content, relativePath, fix });
|
|
89
90
|
if (result) {
|
|
90
91
|
if (Array.isArray(result)) {
|
|
91
|
-
results.push(...result)
|
|
92
|
+
results.push(...result);
|
|
92
93
|
} else {
|
|
93
|
-
results.push(result)
|
|
94
|
+
results.push(result);
|
|
94
95
|
}
|
|
95
96
|
}
|
|
96
97
|
}
|
|
97
98
|
}
|
|
98
99
|
|
|
99
100
|
// Cross-file validators — stop the spinner first so interactive prompts work.
|
|
100
|
-
if (onBeforeCrossFile) onBeforeCrossFile()
|
|
101
|
+
if (onBeforeCrossFile) onBeforeCrossFile();
|
|
101
102
|
for (const validator of validators) {
|
|
102
|
-
if (!validator.validateAll) continue
|
|
103
|
-
const result = await validator.validateAll(files, gitRoot, { fix, nonInteractive })
|
|
103
|
+
if (!validator.validateAll) continue;
|
|
104
|
+
const result = await validator.validateAll(files, gitRoot, { fix, nonInteractive });
|
|
104
105
|
if (result) {
|
|
105
106
|
if (Array.isArray(result)) {
|
|
106
|
-
results.push(...result)
|
|
107
|
+
results.push(...result);
|
|
107
108
|
} else {
|
|
108
|
-
results.push(result)
|
|
109
|
+
results.push(result);
|
|
109
110
|
}
|
|
110
111
|
}
|
|
111
112
|
}
|
|
112
113
|
|
|
113
|
-
return results
|
|
114
|
+
return results;
|
|
114
115
|
}
|
package/src/utils/llms.js
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
const H1_RE = /^#\s+(.+)
|
|
2
|
-
const H2_RE = /^##\s+(.+)
|
|
3
|
-
const H3_RE = /^###\s+(.+)
|
|
1
|
+
const H1_RE = /^#\s+(.+)$/;
|
|
2
|
+
const H2_RE = /^##\s+(.+)$/;
|
|
3
|
+
const H3_RE = /^###\s+(.+)$/;
|
|
4
4
|
|
|
5
5
|
// llms.txt might have a literal " (section)" marker to every
|
|
6
6
|
// grouping heading (e.g. `## getting-started (section)`). It's internal
|
|
7
7
|
// metadata, not part of the category name — strip it before it reaches the sidebar.
|
|
8
|
-
const SECTION_MARKER_RE = /\s*\(section\)\s*$/i
|
|
8
|
+
const SECTION_MARKER_RE = /\s*\(section\)\s*$/i;
|
|
9
9
|
|
|
10
10
|
// Max items per section before we consider it "oversized" for usability purposes.
|
|
11
11
|
// Mirrors the cap in usableSections() in import.js.
|
|
12
|
-
const MAX_SECTION_ITEMS = 200
|
|
12
|
+
const MAX_SECTION_ITEMS = 200;
|
|
13
13
|
// Any line whose meaningful content is a markdown link. Accepts:
|
|
14
14
|
// - Standard list rows: `- [text](url)`, `* [text](url) — desc`
|
|
15
15
|
// - Bare link lines: `[text](url)`
|
|
@@ -20,9 +20,10 @@ const MAX_SECTION_ITEMS = 200
|
|
|
20
20
|
// breadcrumb sits between the url and the description and is matched-and-discarded:
|
|
21
21
|
// `[text](url) (Breadcrumb > path): description`
|
|
22
22
|
// Captures: [prefix, text, url, description] (prefix and description may be empty).
|
|
23
|
-
const LINK_LINE_RE =
|
|
24
|
-
|
|
25
|
-
const
|
|
23
|
+
const LINK_LINE_RE =
|
|
24
|
+
/^\s*(?:[-*+]\s+)?([^[\n]*?)\s*\[([^\]]+)\] ?\(([^)\s]+)\)(?:\s+\([^)]*\))?(?:\s*[:—–-]\s*(.+?))?\s*$/;
|
|
25
|
+
const BLOCKQUOTE_RE = /^\s*>/;
|
|
26
|
+
const FENCE_RE = /^\s*(?:```|~~~)/;
|
|
26
27
|
|
|
27
28
|
// A llms.txt is considered usable as long as it has at least one link row.
|
|
28
29
|
// Beyond that we enforce a structural ratio (share of "spec-shaped" lines —
|
|
@@ -31,81 +32,81 @@ const FENCE_RE = /^\s*(?:```|~~~)/
|
|
|
31
32
|
// 3-link file with one prose sentence would compute a misleading ratio; we
|
|
32
33
|
// can't tell signal from noise at that size, so we trust it and let the
|
|
33
34
|
// BFS-merge dedupe handle any junk that slips through.
|
|
34
|
-
const RATIO_CHECK_MIN_LINKS = 10
|
|
35
|
-
const MIN_CONFORMING_RATIO = 0.7
|
|
35
|
+
const RATIO_CHECK_MIN_LINKS = 10;
|
|
36
|
+
const MIN_CONFORMING_RATIO = 0.7;
|
|
36
37
|
|
|
37
38
|
export function analyzeLlmsTxt(body, llmsUrl, options = {}) {
|
|
38
|
-
const parsed = parseLlmsTxt(body, llmsUrl, options)
|
|
39
|
-
const lineStats = classifyLines(body)
|
|
40
|
-
const linkItems = parsed.sections.reduce((sum, s) => sum + s.items.length, 0)
|
|
41
|
-
const reason = getSkipReason(body, linkItems, lineStats)
|
|
39
|
+
const parsed = parseLlmsTxt(body, llmsUrl, options);
|
|
40
|
+
const lineStats = classifyLines(body);
|
|
41
|
+
const linkItems = parsed.sections.reduce((sum, s) => sum + s.items.length, 0);
|
|
42
|
+
const reason = getSkipReason(body, linkItems, lineStats);
|
|
42
43
|
|
|
43
44
|
return {
|
|
44
45
|
parsed,
|
|
45
46
|
usable: !reason,
|
|
46
47
|
reason,
|
|
47
48
|
stats: { ...lineStats, linkItems },
|
|
48
|
-
}
|
|
49
|
+
};
|
|
49
50
|
}
|
|
50
51
|
|
|
51
52
|
export function parseLlmsTxt(body, llmsUrl, options = {}) {
|
|
52
|
-
const h2Result = parseSections(body, llmsUrl, H2_RE, options)
|
|
53
|
+
const h2Result = parseSections(body, llmsUrl, H2_RE, options);
|
|
53
54
|
|
|
54
55
|
// If every H2 section is oversized (or there are none), and the file has H3
|
|
55
56
|
// headings, re-parse treating H3 (###) as section boundaries.
|
|
56
57
|
// For strange LLMS.txt formatting
|
|
57
|
-
const allOversized =
|
|
58
|
-
h2Result.sections.every(
|
|
58
|
+
const allOversized =
|
|
59
|
+
h2Result.sections.length === 0 || h2Result.sections.every(s => s.items.length > MAX_SECTION_ITEMS);
|
|
59
60
|
if (allOversized && /^###\s/m.test(body)) {
|
|
60
|
-
const h3Result = parseSections(body, llmsUrl, H3_RE, options)
|
|
61
|
+
const h3Result = parseSections(body, llmsUrl, H3_RE, options);
|
|
61
62
|
if (h3Result.sections.length > 1) {
|
|
62
|
-
return { ...h3Result, h3Fallback: true }
|
|
63
|
+
return { ...h3Result, h3Fallback: true };
|
|
63
64
|
}
|
|
64
65
|
}
|
|
65
66
|
|
|
66
|
-
return h2Result
|
|
67
|
+
return h2Result;
|
|
67
68
|
}
|
|
68
69
|
|
|
69
70
|
function parseSections(body, llmsUrl, sectionRe, options = {}) {
|
|
70
|
-
const lines = body.split(/\r?\n/)
|
|
71
|
-
let title = null
|
|
72
|
-
const sections = []
|
|
73
|
-
let current = null
|
|
71
|
+
const lines = body.split(/\r?\n/);
|
|
72
|
+
let title = null;
|
|
73
|
+
const sections = [];
|
|
74
|
+
let current = null;
|
|
74
75
|
|
|
75
76
|
for (const line of lines) {
|
|
76
|
-
const h1 = line.match(H1_RE)
|
|
77
|
+
const h1 = line.match(H1_RE);
|
|
77
78
|
if (h1 && !title) {
|
|
78
|
-
title = h1[1].trim()
|
|
79
|
-
continue
|
|
79
|
+
title = h1[1].trim();
|
|
80
|
+
continue;
|
|
80
81
|
}
|
|
81
82
|
|
|
82
|
-
const sectionMatch = line.match(sectionRe)
|
|
83
|
+
const sectionMatch = line.match(sectionRe);
|
|
83
84
|
if (sectionMatch) {
|
|
84
|
-
current = { title: sectionMatch[1].trim().replace(SECTION_MARKER_RE, '').trim(), items: [] }
|
|
85
|
-
sections.push(current)
|
|
86
|
-
continue
|
|
85
|
+
current = { title: sectionMatch[1].trim().replace(SECTION_MARKER_RE, '').trim(), items: [] };
|
|
86
|
+
sections.push(current);
|
|
87
|
+
continue;
|
|
87
88
|
}
|
|
88
89
|
|
|
89
|
-
const item = parseListLink(line, llmsUrl, options)
|
|
90
|
-
if (!item) continue
|
|
90
|
+
const item = parseListLink(line, llmsUrl, options);
|
|
91
|
+
if (!item) continue;
|
|
91
92
|
|
|
92
93
|
if (!current) {
|
|
93
|
-
current = { title: 'Resources', items: [] }
|
|
94
|
-
sections.push(current)
|
|
94
|
+
current = { title: 'Resources', items: [] };
|
|
95
|
+
sections.push(current);
|
|
95
96
|
}
|
|
96
|
-
current.items.push(item)
|
|
97
|
+
current.items.push(item);
|
|
97
98
|
}
|
|
98
99
|
|
|
99
|
-
return { title, sections }
|
|
100
|
+
return { title, sections };
|
|
100
101
|
}
|
|
101
102
|
|
|
102
103
|
function getSkipReason(body, linkItems, lineStats) {
|
|
103
|
-
if (/^---\r?\n/.test(body)) return 'starts with YAML frontmatter'
|
|
104
|
-
if (linkItems === 0) return 'no link items'
|
|
104
|
+
if (/^---\r?\n/.test(body)) return 'starts with YAML frontmatter';
|
|
105
|
+
if (linkItems === 0) return 'no link items';
|
|
105
106
|
if (linkItems >= RATIO_CHECK_MIN_LINKS && lineStats.ratio < MIN_CONFORMING_RATIO) {
|
|
106
|
-
return `conforming-line ratio ${lineStats.ratio.toFixed(2)} below ${MIN_CONFORMING_RATIO} (${lineStats.conforming}/${lineStats.total} lines)
|
|
107
|
+
return `conforming-line ratio ${lineStats.ratio.toFixed(2)} below ${MIN_CONFORMING_RATIO} (${lineStats.conforming}/${lineStats.total} lines)`;
|
|
107
108
|
}
|
|
108
|
-
return null
|
|
109
|
+
return null;
|
|
109
110
|
}
|
|
110
111
|
|
|
111
112
|
/**
|
|
@@ -116,109 +117,111 @@ function getSkipReason(body, linkItems, lineStats) {
|
|
|
116
117
|
* of) a fenced code block
|
|
117
118
|
*/
|
|
118
119
|
function classifyLines(body) {
|
|
119
|
-
const lines = body.split(/\r?\n/)
|
|
120
|
-
let inFence = false
|
|
121
|
-
let conforming = 0
|
|
122
|
-
let nonConforming = 0
|
|
120
|
+
const lines = body.split(/\r?\n/);
|
|
121
|
+
let inFence = false;
|
|
122
|
+
let conforming = 0;
|
|
123
|
+
let nonConforming = 0;
|
|
123
124
|
|
|
124
125
|
for (const line of lines) {
|
|
125
126
|
if (FENCE_RE.test(line)) {
|
|
126
|
-
nonConforming
|
|
127
|
-
inFence = !inFence
|
|
128
|
-
continue
|
|
127
|
+
nonConforming++;
|
|
128
|
+
inFence = !inFence;
|
|
129
|
+
continue;
|
|
129
130
|
}
|
|
130
131
|
if (inFence) {
|
|
131
|
-
nonConforming
|
|
132
|
-
continue
|
|
132
|
+
nonConforming++;
|
|
133
|
+
continue;
|
|
133
134
|
}
|
|
134
135
|
if (line.trim() === '') {
|
|
135
|
-
conforming
|
|
136
|
-
continue
|
|
136
|
+
conforming++;
|
|
137
|
+
continue;
|
|
137
138
|
}
|
|
138
139
|
if (H1_RE.test(line) || H2_RE.test(line) || LINK_LINE_RE.test(line) || BLOCKQUOTE_RE.test(line)) {
|
|
139
|
-
conforming
|
|
140
|
-
continue
|
|
140
|
+
conforming++;
|
|
141
|
+
continue;
|
|
141
142
|
}
|
|
142
|
-
nonConforming
|
|
143
|
+
nonConforming++;
|
|
143
144
|
}
|
|
144
145
|
|
|
145
|
-
const total = conforming + nonConforming
|
|
146
|
-
const ratio = total === 0 ? 0 : conforming / total
|
|
147
|
-
return { conforming, nonConforming, total, ratio }
|
|
146
|
+
const total = conforming + nonConforming;
|
|
147
|
+
const ratio = total === 0 ? 0 : conforming / total;
|
|
148
|
+
return { conforming, nonConforming, total, ratio };
|
|
148
149
|
}
|
|
149
150
|
|
|
150
151
|
function parseListLink(line, llmsUrl, options = {}) {
|
|
151
|
-
const match = line.match(LINK_LINE_RE)
|
|
152
|
-
if (!match) return null
|
|
152
|
+
const match = line.match(LINK_LINE_RE);
|
|
153
|
+
if (!match) return null;
|
|
153
154
|
|
|
154
|
-
const [, prefix, text, rawUrl, trailingDesc] = match
|
|
155
|
-
const url = normalizeUrl(rawUrl, llmsUrl, options)
|
|
156
|
-
if (!url) return null
|
|
155
|
+
const [, prefix, text, rawUrl, trailingDesc] = match;
|
|
156
|
+
const url = normalizeUrl(rawUrl, llmsUrl, options);
|
|
157
|
+
if (!url) return null;
|
|
157
158
|
|
|
158
159
|
// Prefer an explicit trailing description (`[text](url) — desc`); fall back
|
|
159
160
|
// to the breadcrumb prefix (`API Reference > Agents [text](url)`) when no
|
|
160
161
|
// explicit description is present. Either gives downstream extra context.
|
|
161
|
-
const description = trailingDesc?.trim() || prefix?.trim() || null
|
|
162
|
+
const description = trailingDesc?.trim() || prefix?.trim() || null;
|
|
162
163
|
|
|
163
164
|
return {
|
|
164
165
|
text: text.trim(),
|
|
165
166
|
url,
|
|
166
167
|
description: description || null,
|
|
167
|
-
}
|
|
168
|
+
};
|
|
168
169
|
}
|
|
169
170
|
|
|
170
171
|
function normalizeUrl(rawUrl, llmsUrl, options = {}) {
|
|
171
|
-
const trimmed = String(rawUrl || '')
|
|
172
|
-
|
|
172
|
+
const trimmed = String(rawUrl || '')
|
|
173
|
+
.trim()
|
|
174
|
+
.replace(/[.,;]+$/, '');
|
|
175
|
+
if (!trimmed || trimmed.startsWith('#') || /^(mailto|javascript):/i.test(trimmed)) return null;
|
|
173
176
|
|
|
174
177
|
try {
|
|
175
|
-
const url = llmsUrl ? new URL(resolveLlmsRelativeUrl(trimmed, llmsUrl, options), llmsUrl) : new URL(trimmed)
|
|
176
|
-
if (url.protocol !== 'http:' && url.protocol !== 'https:') return null
|
|
177
|
-
return url.toString()
|
|
178
|
+
const url = llmsUrl ? new URL(resolveLlmsRelativeUrl(trimmed, llmsUrl, options), llmsUrl) : new URL(trimmed);
|
|
179
|
+
if (url.protocol !== 'http:' && url.protocol !== 'https:') return null;
|
|
180
|
+
return url.toString();
|
|
178
181
|
} catch {
|
|
179
|
-
return null
|
|
182
|
+
return null;
|
|
180
183
|
}
|
|
181
184
|
}
|
|
182
185
|
|
|
183
186
|
function resolveLlmsRelativeUrl(trimmed, llmsUrl, options) {
|
|
184
|
-
if (options.rootRelativeResolution !== 'llms-dir' || !isRootRelativeUrl(trimmed)) return trimmed
|
|
187
|
+
if (options.rootRelativeResolution !== 'llms-dir' || !isRootRelativeUrl(trimmed)) return trimmed;
|
|
185
188
|
|
|
186
|
-
const nestedPrefix = getLlmsNestedPrefix(llmsUrl)
|
|
187
|
-
if (!nestedPrefix) return trimmed
|
|
189
|
+
const nestedPrefix = getLlmsNestedPrefix(llmsUrl);
|
|
190
|
+
if (!nestedPrefix) return trimmed;
|
|
188
191
|
|
|
189
|
-
const normallyResolved = new URL(trimmed, llmsUrl)
|
|
190
|
-
if (!shouldResolveRootRelativeFromLlmsDir(trimmed, normallyResolved.pathname, nestedPrefix)) return trimmed
|
|
192
|
+
const normallyResolved = new URL(trimmed, llmsUrl);
|
|
193
|
+
if (!shouldResolveRootRelativeFromLlmsDir(trimmed, normallyResolved.pathname, nestedPrefix)) return trimmed;
|
|
191
194
|
|
|
192
|
-
return `.${trimmed}
|
|
195
|
+
return `.${trimmed}`;
|
|
193
196
|
}
|
|
194
197
|
|
|
195
198
|
function shouldResolveRootRelativeFromLlmsDir(rawUrl, pathname, nestedPrefix) {
|
|
196
|
-
if (isPathWithinPrefix(pathname, nestedPrefix)) return false
|
|
197
|
-
if (/\/llms\.txt$/i.test(pathname)) return false
|
|
199
|
+
if (isPathWithinPrefix(pathname, nestedPrefix)) return false;
|
|
200
|
+
if (/\/llms\.txt$/i.test(pathname)) return false;
|
|
198
201
|
|
|
199
|
-
const lastSegment = pathname.split('/').pop() || ''
|
|
200
|
-
const ext = lastSegment.match(/\.([a-z0-9]+)$/i)?.[1]?.toLowerCase()
|
|
201
|
-
if (ext && !['htm', 'html', 'md', 'mdx'].includes(ext)) return false
|
|
202
|
+
const lastSegment = pathname.split('/').pop() || '';
|
|
203
|
+
const ext = lastSegment.match(/\.([a-z0-9]+)$/i)?.[1]?.toLowerCase();
|
|
204
|
+
if (ext && !['htm', 'html', 'md', 'mdx'].includes(ext)) return false;
|
|
202
205
|
|
|
203
|
-
return isRootRelativeUrl(rawUrl)
|
|
206
|
+
return isRootRelativeUrl(rawUrl);
|
|
204
207
|
}
|
|
205
208
|
|
|
206
209
|
function getLlmsNestedPrefix(llmsUrl) {
|
|
207
|
-
if (!llmsUrl) return null
|
|
210
|
+
if (!llmsUrl) return null;
|
|
208
211
|
|
|
209
212
|
try {
|
|
210
|
-
const pathname = new URL(llmsUrl).pathname
|
|
211
|
-
const dir = pathname.replace(/llms\.txt$/i, '').replace(/\/+$/, '/')
|
|
212
|
-
return dir === '/' ? null : dir
|
|
213
|
+
const pathname = new URL(llmsUrl).pathname;
|
|
214
|
+
const dir = pathname.replace(/llms\.txt$/i, '').replace(/\/+$/, '/');
|
|
215
|
+
return dir === '/' ? null : dir;
|
|
213
216
|
} catch {
|
|
214
|
-
return null
|
|
217
|
+
return null;
|
|
215
218
|
}
|
|
216
219
|
}
|
|
217
220
|
|
|
218
221
|
function isRootRelativeUrl(rawUrl) {
|
|
219
|
-
return /^\/(?!\/)/.test(rawUrl)
|
|
222
|
+
return /^\/(?!\/)/.test(rawUrl);
|
|
220
223
|
}
|
|
221
224
|
|
|
222
225
|
function isPathWithinPrefix(pathname, prefix) {
|
|
223
|
-
return pathname === prefix.replace(/\/$/, '') || pathname.startsWith(prefix)
|
|
226
|
+
return pathname === prefix.replace(/\/$/, '') || pathname.startsWith(prefix);
|
|
224
227
|
}
|