starmark 1.0.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/bin/starmark.js +5 -0
- package/package.json +43 -0
- package/public/app.js +2952 -0
- package/public/frontmatter-editor.js +490 -0
- package/public/frontmatter.js +554 -0
- package/public/icons.js +52 -0
- package/public/index.html +113 -0
- package/public/styles.css +1564 -0
- package/public/toolkit.js +19 -0
- package/public/tools/10-undo.js +26 -0
- package/public/tools/11-redo.js +26 -0
- package/public/tools/20-bold.js +21 -0
- package/public/tools/21-italic.js +21 -0
- package/public/tools/22-strikethrough.js +21 -0
- package/public/tools/30-link.js +136 -0
- package/public/tools/31-image.js +488 -0
- package/src/content-config.js +391 -0
- package/src/server.js +1056 -0
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
import fs from "fs/promises";
|
|
2
|
+
import path from "path";
|
|
3
|
+
|
|
4
|
+
const CONTENT_CONFIG_CANDIDATES = [
|
|
5
|
+
"src/content.config.ts",
|
|
6
|
+
"src/content.config.js",
|
|
7
|
+
"src/content.config.mjs",
|
|
8
|
+
"src/content/config.ts",
|
|
9
|
+
"src/content/config.js",
|
|
10
|
+
"src/content/config.mjs",
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
const PAGES_FRONTMATTER_FIELDS = [
|
|
14
|
+
{ name: "layout", type: "literal", value: "src/layouts/Default.astro" },
|
|
15
|
+
{ name: "title", type: "string" },
|
|
16
|
+
{ name: "pubDate", type: "string" },
|
|
17
|
+
{ name: "description", type: "string" },
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
function normalizeSlashes(value) {
|
|
21
|
+
return String(value).replace(/\\/g, "/").replace(/^\/+/, "");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function stripQuotes(value) {
|
|
25
|
+
const trimmed = value.trim();
|
|
26
|
+
if (
|
|
27
|
+
(trimmed.startsWith('"') && trimmed.endsWith('"')) ||
|
|
28
|
+
(trimmed.startsWith("'") && trimmed.endsWith("'")) ||
|
|
29
|
+
(trimmed.startsWith("`") && trimmed.endsWith("`"))
|
|
30
|
+
) {
|
|
31
|
+
return trimmed.slice(1, -1);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return trimmed;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function formatFrontmatter(fields) {
|
|
38
|
+
const lines = fields.map((field) => {
|
|
39
|
+
if (field.type === "literal") {
|
|
40
|
+
return `${field.name}: ${field.value}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (field.type === "boolean") {
|
|
44
|
+
return `${field.name}: false`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (field.type === "array") {
|
|
48
|
+
return `${field.name}: []`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (field.type === "object") {
|
|
52
|
+
return `${field.name}: {}`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return `${field.name}:`;
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
return `---\n${lines.join("\n")}\n---\n`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function inferZodType(typeSource) {
|
|
62
|
+
const normalized = typeSource.replace(/\s+/g, " ");
|
|
63
|
+
|
|
64
|
+
if (/z\.array/.test(normalized)) {
|
|
65
|
+
return "array";
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (/z\.object/.test(normalized)) {
|
|
69
|
+
return "object";
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (/z\.boolean/.test(normalized)) {
|
|
73
|
+
return "boolean";
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (/z\.number/.test(normalized)) {
|
|
77
|
+
return "number";
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (/z\.(?:coerce\.)?date/.test(normalized)) {
|
|
81
|
+
return "string";
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return "string";
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function parseZodObjectFields(objectBody) {
|
|
88
|
+
const fields = [];
|
|
89
|
+
let depth = 0;
|
|
90
|
+
let chunkStart = 0;
|
|
91
|
+
|
|
92
|
+
for (let index = 0; index < objectBody.length; index += 1) {
|
|
93
|
+
const char = objectBody[index];
|
|
94
|
+
|
|
95
|
+
if (char === "{" || char === "(") {
|
|
96
|
+
depth += 1;
|
|
97
|
+
} else if (char === "}" || char === ")") {
|
|
98
|
+
depth -= 1;
|
|
99
|
+
} else if (char === "," && depth === 0) {
|
|
100
|
+
const chunk = objectBody.slice(chunkStart, index).trim();
|
|
101
|
+
const field = parseFieldChunk(chunk);
|
|
102
|
+
if (field) {
|
|
103
|
+
fields.push(field);
|
|
104
|
+
}
|
|
105
|
+
chunkStart = index + 1;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const finalChunk = objectBody.slice(chunkStart).trim();
|
|
110
|
+
if (finalChunk) {
|
|
111
|
+
const field = parseFieldChunk(finalChunk);
|
|
112
|
+
if (field) {
|
|
113
|
+
fields.push(field);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return fields;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function parseFieldChunk(chunk) {
|
|
121
|
+
const match = chunk.match(/^(\w+)\s*:/);
|
|
122
|
+
if (!match) {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const name = match[1];
|
|
127
|
+
const typeSource = chunk.slice(match[0].length).trim();
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
name,
|
|
131
|
+
type: inferZodType(typeSource),
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function extractSchemaObjectBody(defineCollectionBody, configText) {
|
|
136
|
+
const inlineSchemaMatch = defineCollectionBody.match(
|
|
137
|
+
/schema\s*:\s*(?:\([^)]*\)\s*=>\s*)?z\.object\s*\(\s*\{([\s\S]*?)\}\s*\)/,
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
if (inlineSchemaMatch) {
|
|
141
|
+
return inlineSchemaMatch[1];
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const schemaRefMatch = defineCollectionBody.match(/schema\s*:\s*(\w+)\s*(?:\(|,|\})/);
|
|
145
|
+
if (!schemaRefMatch) {
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const schemaName = schemaRefMatch[1];
|
|
150
|
+
const exportedSchemaMatch = configText.match(
|
|
151
|
+
new RegExp(
|
|
152
|
+
`(?:export\\s+)?(?:const|function)\\s+${schemaName}\\s*=\\s*(?:\\([^)]*\\)\\s*=>\\s*)?z\\.object\\s*\\(\\s*\\{([\\s\\S]*?)\\}\\s*\\)`,
|
|
153
|
+
),
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
return exportedSchemaMatch?.[1] ?? null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function extractLoaderBase(defineCollectionBody) {
|
|
160
|
+
const baseMatch = defineCollectionBody.match(/base\s*:\s*['"`]([^'"`]+)['"`]/);
|
|
161
|
+
if (!baseMatch) {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return normalizeSlashes(stripQuotes(baseMatch[1])).replace(/^\.\//, "");
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function extractDefineCollectionBody(text, startIndex) {
|
|
169
|
+
const openBraceIndex = text.indexOf("{", startIndex);
|
|
170
|
+
if (openBraceIndex === -1) {
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
let depth = 0;
|
|
175
|
+
for (let index = openBraceIndex; index < text.length; index += 1) {
|
|
176
|
+
const char = text[index];
|
|
177
|
+
if (char === "{") {
|
|
178
|
+
depth += 1;
|
|
179
|
+
} else if (char === "}") {
|
|
180
|
+
depth -= 1;
|
|
181
|
+
if (depth === 0) {
|
|
182
|
+
return text.slice(openBraceIndex + 1, index);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function parseDefineCollectionBlocks(configText) {
|
|
191
|
+
const blocksByIdentifier = new Map();
|
|
192
|
+
const defineCollectionPattern = /(?:const|let)\s+(\w+)\s*=\s*defineCollection\s*\(/g;
|
|
193
|
+
|
|
194
|
+
for (const match of configText.matchAll(defineCollectionPattern)) {
|
|
195
|
+
const identifier = match[1];
|
|
196
|
+
const body = extractDefineCollectionBody(configText, match.index + match[0].length - 1);
|
|
197
|
+
if (body) {
|
|
198
|
+
blocksByIdentifier.set(identifier, body);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return blocksByIdentifier;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function splitTopLevelCommas(text) {
|
|
206
|
+
const parts = [];
|
|
207
|
+
let depth = 0;
|
|
208
|
+
let chunkStart = 0;
|
|
209
|
+
|
|
210
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
211
|
+
const char = text[index];
|
|
212
|
+
|
|
213
|
+
if (char === "{" || char === "(") {
|
|
214
|
+
depth += 1;
|
|
215
|
+
} else if (char === "}" || char === ")") {
|
|
216
|
+
depth -= 1;
|
|
217
|
+
} else if (char === "," && depth === 0) {
|
|
218
|
+
const chunk = text.slice(chunkStart, index).trim();
|
|
219
|
+
if (chunk) {
|
|
220
|
+
parts.push(chunk);
|
|
221
|
+
}
|
|
222
|
+
chunkStart = index + 1;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const finalChunk = text.slice(chunkStart).trim();
|
|
227
|
+
if (finalChunk) {
|
|
228
|
+
parts.push(finalChunk);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return parts;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function parseCollectionsExport(configText, blocksByIdentifier) {
|
|
235
|
+
const collections = new Map();
|
|
236
|
+
const exportMatch = configText.match(/export\s+const\s+collections\s*=\s*\{/);
|
|
237
|
+
|
|
238
|
+
if (!exportMatch) {
|
|
239
|
+
for (const [identifier, body] of blocksByIdentifier.entries()) {
|
|
240
|
+
collections.set(identifier, body);
|
|
241
|
+
}
|
|
242
|
+
return collections;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const openBraceIndex = configText.indexOf("{", exportMatch.index);
|
|
246
|
+
let depth = 0;
|
|
247
|
+
let closeBraceIndex = openBraceIndex;
|
|
248
|
+
|
|
249
|
+
for (let index = openBraceIndex; index < configText.length; index += 1) {
|
|
250
|
+
const char = configText[index];
|
|
251
|
+
if (char === "{") {
|
|
252
|
+
depth += 1;
|
|
253
|
+
} else if (char === "}") {
|
|
254
|
+
depth -= 1;
|
|
255
|
+
if (depth === 0) {
|
|
256
|
+
closeBraceIndex = index;
|
|
257
|
+
break;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const exportBody = configText.slice(openBraceIndex + 1, closeBraceIndex);
|
|
263
|
+
|
|
264
|
+
for (const entry of splitTopLevelCommas(exportBody)) {
|
|
265
|
+
const shorthandMatch = entry.match(/^(\w+)\s*$/);
|
|
266
|
+
if (shorthandMatch) {
|
|
267
|
+
const identifier = shorthandMatch[1];
|
|
268
|
+
if (blocksByIdentifier.has(identifier)) {
|
|
269
|
+
collections.set(identifier, blocksByIdentifier.get(identifier));
|
|
270
|
+
}
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const explicitMatch = entry.match(/^(['"`]?)([\w-]+)\1\s*:\s*defineCollection\s*\(/);
|
|
275
|
+
if (explicitMatch) {
|
|
276
|
+
const collectionName = explicitMatch[2];
|
|
277
|
+
const defineIndex = configText.indexOf(entry, openBraceIndex);
|
|
278
|
+
const bodyStart = configText.indexOf("{", defineIndex);
|
|
279
|
+
const body = extractDefineCollectionBody(configText, bodyStart);
|
|
280
|
+
if (body) {
|
|
281
|
+
collections.set(collectionName, body);
|
|
282
|
+
}
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const identifierMatch = entry.match(/^(\w+)\s*:\s*(\w+)\s*$/);
|
|
287
|
+
if (identifierMatch) {
|
|
288
|
+
const collectionName = identifierMatch[1];
|
|
289
|
+
const identifier = identifierMatch[2];
|
|
290
|
+
if (blocksByIdentifier.has(identifier)) {
|
|
291
|
+
collections.set(collectionName, blocksByIdentifier.get(identifier));
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (collections.size === 0) {
|
|
297
|
+
for (const [identifier, body] of blocksByIdentifier.entries()) {
|
|
298
|
+
collections.set(identifier, body);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return collections;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function parseContentConfig(configText) {
|
|
306
|
+
const blocksByIdentifier = parseDefineCollectionBlocks(configText);
|
|
307
|
+
const rawCollections = parseCollectionsExport(configText, blocksByIdentifier);
|
|
308
|
+
const collections = {};
|
|
309
|
+
|
|
310
|
+
for (const [name, body] of rawCollections.entries()) {
|
|
311
|
+
const schemaBody = extractSchemaObjectBody(body, configText);
|
|
312
|
+
collections[name] = {
|
|
313
|
+
name,
|
|
314
|
+
basePath: extractLoaderBase(body),
|
|
315
|
+
fields: schemaBody ? parseZodObjectFields(schemaBody) : [],
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return collections;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
async function readContentConfig(projectPath) {
|
|
323
|
+
for (const relativePath of CONTENT_CONFIG_CANDIDATES) {
|
|
324
|
+
const configPath = path.join(projectPath, relativePath);
|
|
325
|
+
|
|
326
|
+
try {
|
|
327
|
+
const configText = await fs.readFile(configPath, "utf8");
|
|
328
|
+
return parseContentConfig(configText);
|
|
329
|
+
} catch {
|
|
330
|
+
// Try the next known config location.
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
return {};
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function getContentCollectionName(relativePath, collections) {
|
|
338
|
+
const normalizedPath = normalizeSlashes(relativePath);
|
|
339
|
+
const folderMatch = normalizedPath.match(/^src\/content\/([^/]+)/);
|
|
340
|
+
|
|
341
|
+
if (!folderMatch) {
|
|
342
|
+
return null;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const folderName = folderMatch[1];
|
|
346
|
+
|
|
347
|
+
if (collections[folderName]) {
|
|
348
|
+
return folderName;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
for (const [name, collection] of Object.entries(collections)) {
|
|
352
|
+
if (collection.basePath && normalizedPath.startsWith(`${collection.basePath}/`)) {
|
|
353
|
+
return name;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (collection.basePath === normalizedPath) {
|
|
357
|
+
return name;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
return folderName;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function buildPagesFrontmatter() {
|
|
365
|
+
return formatFrontmatter(PAGES_FRONTMATTER_FIELDS);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function buildContentFrontmatter(collection) {
|
|
369
|
+
if (!collection?.fields?.length) {
|
|
370
|
+
return "---\n---\n";
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
return formatFrontmatter(collection.fields);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
export async function buildInitialFileContent(projectPath, relativePath, source) {
|
|
377
|
+
const normalizedPath = normalizeSlashes(relativePath);
|
|
378
|
+
|
|
379
|
+
if (source === "pages" || normalizedPath.startsWith("src/pages/")) {
|
|
380
|
+
return buildPagesFrontmatter();
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
if (source === "content" || normalizedPath.startsWith("src/content/")) {
|
|
384
|
+
const collections = await readContentConfig(projectPath);
|
|
385
|
+
const collectionName = getContentCollectionName(normalizedPath, collections);
|
|
386
|
+
const collection = collectionName ? collections[collectionName] : null;
|
|
387
|
+
return buildContentFrontmatter(collection);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
return "";
|
|
391
|
+
}
|