@writechoice/mint-cli 0.0.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/LICENSE +21 -0
- package/PUBLISH.md +386 -0
- package/README.md +324 -0
- package/bin/cli.js +44 -0
- package/package.json +51 -0
- package/src/commands/validate/links.js +1183 -0
- package/src/index.js +5 -0
- package/src/utils/helpers.js +218 -0
package/src/index.js
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { join, relative, resolve, dirname } from 'path';
|
|
2
|
+
import { URL } from 'url';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Clean heading text by removing duplicates and extra whitespace.
|
|
6
|
+
*
|
|
7
|
+
* Some headings have duplicate text separated by newlines like:
|
|
8
|
+
* "Create resources\nCreate resources" -> "Create resources"
|
|
9
|
+
*/
|
|
10
|
+
export function cleanHeadingText(text) {
|
|
11
|
+
// Split by newlines and get unique parts while preserving order
|
|
12
|
+
const lines = text
|
|
13
|
+
.split('\n')
|
|
14
|
+
.map(line => line.trim())
|
|
15
|
+
.filter(line => line);
|
|
16
|
+
|
|
17
|
+
// If all lines are the same, return just one
|
|
18
|
+
const uniqueLines = [...new Set(lines)];
|
|
19
|
+
if (uniqueLines.length === 1) {
|
|
20
|
+
return uniqueLines[0];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Otherwise, join with space (in case they're genuinely different)
|
|
24
|
+
return lines.join(' ');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Convert heading text to kebab-case anchor format.
|
|
29
|
+
*
|
|
30
|
+
* Examples:
|
|
31
|
+
* "Getting Started Guide" -> "getting-started-guide"
|
|
32
|
+
* "AI/ML Integration" -> "ai-ml-integration"
|
|
33
|
+
*/
|
|
34
|
+
export function toKebabCase(text) {
|
|
35
|
+
let result = text.toLowerCase();
|
|
36
|
+
// Remove non-alphanumeric except hyphens
|
|
37
|
+
result = result.replace(/[^\w\s-]/g, '');
|
|
38
|
+
// Replace spaces and multiple hyphens
|
|
39
|
+
result = result.replace(/[-\s]+/g, '-');
|
|
40
|
+
return result.replace(/^-+|-+$/g, '');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Check if a URL is external (http/https)
|
|
45
|
+
*/
|
|
46
|
+
export function isExternalUrl(href) {
|
|
47
|
+
return href.startsWith('http://') || href.startsWith('https://');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Check if href is just an anchor (starts with #)
|
|
52
|
+
*/
|
|
53
|
+
export function isAnchorOnly(href) {
|
|
54
|
+
return href.startsWith('#');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Normalize URL by removing /index and trailing slashes
|
|
59
|
+
*/
|
|
60
|
+
export function normalizeUrl(url) {
|
|
61
|
+
// Remove /index.mdx at the end
|
|
62
|
+
if (url.endsWith('/index.mdx')) {
|
|
63
|
+
url = url.slice(0, -10); // Remove "/index.mdx"
|
|
64
|
+
}
|
|
65
|
+
// Remove /index at the end (but not /index-something)
|
|
66
|
+
else if (url.endsWith('/index')) {
|
|
67
|
+
url = url.slice(0, -6); // Remove "/index"
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
url = url.replace(/\/+$/, '');
|
|
71
|
+
return url;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Find the line number for a match position in the content
|
|
76
|
+
*/
|
|
77
|
+
export function findLineNumber(content, matchStart) {
|
|
78
|
+
return content.slice(0, matchStart).split('\n').length;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Remove code blocks and frontmatter from content, return cleaned content
|
|
83
|
+
* and list of removed ranges.
|
|
84
|
+
*/
|
|
85
|
+
export function removeCodeBlocksAndFrontmatter(content) {
|
|
86
|
+
const removedRanges = [];
|
|
87
|
+
|
|
88
|
+
// Find frontmatter
|
|
89
|
+
const frontmatterPattern = /^---\n.*?\n---\n/ms;
|
|
90
|
+
const frontmatterMatch = frontmatterPattern.exec(content);
|
|
91
|
+
if (frontmatterMatch) {
|
|
92
|
+
removedRanges.push([frontmatterMatch.index, frontmatterMatch.index + frontmatterMatch[0].length]);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Find code blocks
|
|
96
|
+
const codeBlockPattern = /```.*?```/gs;
|
|
97
|
+
let match;
|
|
98
|
+
while ((match = codeBlockPattern.exec(content)) !== null) {
|
|
99
|
+
removedRanges.push([match.index, match.index + match[0].length]);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Remove in reverse order to preserve positions
|
|
103
|
+
removedRanges.sort((a, b) => b[0] - a[0]);
|
|
104
|
+
let cleanedContent = content;
|
|
105
|
+
for (const [start, end] of removedRanges) {
|
|
106
|
+
cleanedContent = cleanedContent.slice(0, start) + ' '.repeat(end - start) + cleanedContent.slice(end);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return { cleanedContent, removedRanges };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Convert a URL back to an MDX file path.
|
|
114
|
+
*
|
|
115
|
+
* Examples:
|
|
116
|
+
* "https://docs.nebius.com/kubernetes/gpu/set-up" -> repoRoot / "kubernetes/gpu/set-up.mdx"
|
|
117
|
+
* "https://docs.nebius.com/portal/" -> repoRoot / "portal/index.mdx"
|
|
118
|
+
*/
|
|
119
|
+
export function urlToFilePath(url, baseUrl, repoRoot) {
|
|
120
|
+
// Remove base URL to get the path
|
|
121
|
+
let path;
|
|
122
|
+
if (url.startsWith(baseUrl)) {
|
|
123
|
+
path = url.slice(baseUrl.length);
|
|
124
|
+
} else {
|
|
125
|
+
// Try to extract path from URL
|
|
126
|
+
const parsed = new URL(url);
|
|
127
|
+
path = parsed.pathname;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Remove leading slash
|
|
131
|
+
path = path.replace(/^\/+/, '');
|
|
132
|
+
|
|
133
|
+
// If empty or just /, it's the root index
|
|
134
|
+
if (!path || path === '/') {
|
|
135
|
+
return join(repoRoot, 'index.mdx');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Try with .mdx extension first
|
|
139
|
+
const mdxPath = join(repoRoot, `${path}.mdx`);
|
|
140
|
+
|
|
141
|
+
// Try with /index.mdx
|
|
142
|
+
const indexPath = join(repoRoot, path, 'index.mdx');
|
|
143
|
+
|
|
144
|
+
// Return the .mdx path (we'll check existence later)
|
|
145
|
+
return { mdxPath, indexPath };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Resolve relative/absolute MDX links to full URLs.
|
|
150
|
+
*
|
|
151
|
+
* Examples:
|
|
152
|
+
* "./catalog" from /portal/index.mdx -> https://base/portal/catalog
|
|
153
|
+
* "/plugins/soundcheck" -> https://base/plugins/soundcheck
|
|
154
|
+
* "./guides/auth" from /portal/getting-started.mdx -> https://base/portal/guides/auth
|
|
155
|
+
*
|
|
156
|
+
* Returns null for external URLs or invalid paths.
|
|
157
|
+
*/
|
|
158
|
+
export function resolvePath(mdxFilePath, href, baseUrl, repoRoot) {
|
|
159
|
+
// Handle external URLs (skip)
|
|
160
|
+
if (isExternalUrl(href)) {
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Split anchor from path
|
|
165
|
+
let path, anchor;
|
|
166
|
+
if (href.includes('#')) {
|
|
167
|
+
[path, anchor] = href.split('#', 2);
|
|
168
|
+
} else {
|
|
169
|
+
path = href;
|
|
170
|
+
anchor = '';
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Handle anchor-only links (same page)
|
|
174
|
+
if (!path && anchor) {
|
|
175
|
+
// Convert file path to URL path
|
|
176
|
+
const relPath = relative(repoRoot, mdxFilePath);
|
|
177
|
+
const urlPath = relPath.replace(/\.mdx$/, '');
|
|
178
|
+
const fullUrl = normalizeUrl(`${baseUrl}/${urlPath}`);
|
|
179
|
+
return `${fullUrl}#${anchor}`;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
let fullUrl;
|
|
183
|
+
|
|
184
|
+
// Absolute path (starts with /)
|
|
185
|
+
if (path.startsWith('/')) {
|
|
186
|
+
fullUrl = normalizeUrl(baseUrl + path);
|
|
187
|
+
}
|
|
188
|
+
// Relative path
|
|
189
|
+
else {
|
|
190
|
+
// Get MDX file's directory
|
|
191
|
+
const mdxDir = dirname(mdxFilePath);
|
|
192
|
+
|
|
193
|
+
// Handle ./ prefix
|
|
194
|
+
if (path.startsWith('./')) {
|
|
195
|
+
path = path.slice(2);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Resolve relative to MDX file's directory
|
|
199
|
+
const resolved = resolve(mdxDir, path);
|
|
200
|
+
|
|
201
|
+
// Check if resolved path is within repo
|
|
202
|
+
const relToRoot = relative(repoRoot, resolved);
|
|
203
|
+
if (relToRoot.startsWith('..')) {
|
|
204
|
+
// Path is outside repo
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const urlPath = relToRoot.replace(/\.mdx$/, '');
|
|
209
|
+
fullUrl = normalizeUrl(`${baseUrl}/${urlPath}`);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Add anchor back if present
|
|
213
|
+
if (anchor) {
|
|
214
|
+
fullUrl += '#' + anchor;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return fullUrl;
|
|
218
|
+
}
|