@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/src/index.js ADDED
@@ -0,0 +1,5 @@
1
+ // Main entry point for the WriteChoice Mint CLI library
2
+ // This file can be used if importing the library as a module
3
+
4
+ export { validateLinks } from './commands/validate/links.js';
5
+ export * from './utils/helpers.js';
@@ -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
+ }