starmark 1.0.2 → 1.0.3
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/package.json +1 -1
- package/public/app.js +1 -0
- package/public/frontmatter-editor.js +223 -22
- package/public/media-browser.js +464 -0
- package/public/styles.css +54 -5
- package/public/tools/31-image.js +25 -446
package/package.json
CHANGED
package/public/app.js
CHANGED
|
@@ -29,6 +29,7 @@ const frontmatterEditor = createFrontmatterEditor(frontmatterEditorRoot, {
|
|
|
29
29
|
saveCurrentFile();
|
|
30
30
|
}
|
|
31
31
|
},
|
|
32
|
+
getProjectPath: () => currentProjectPath,
|
|
32
33
|
});
|
|
33
34
|
const projectsSection = document.getElementById("projects-section");
|
|
34
35
|
const projectList = document.getElementById("project-list");
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { icons } from "./icons.js";
|
|
2
|
+
import { createMediaDialog } from "./media-browser.js";
|
|
2
3
|
import {
|
|
3
4
|
defaultValueForType,
|
|
4
5
|
inferValueType,
|
|
@@ -31,7 +32,24 @@ function coerceScalar(type, rawValue) {
|
|
|
31
32
|
}
|
|
32
33
|
}
|
|
33
34
|
|
|
34
|
-
|
|
35
|
+
function isBannerImageSrcField(fieldPath) {
|
|
36
|
+
return (
|
|
37
|
+
fieldPath.length >= 2 &&
|
|
38
|
+
fieldPath[fieldPath.length - 2] === "bannerImage" &&
|
|
39
|
+
fieldPath[fieldPath.length - 1] === "src"
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function isDateField(fieldPath) {
|
|
44
|
+
const key = fieldPath[fieldPath.length - 1];
|
|
45
|
+
return key === "pubDate" || key === "modDate";
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function isIsoDateString(value) {
|
|
49
|
+
return /^\d{4}-\d{2}-\d{2}$/.test(String(value));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function createFrontmatterEditor(root, { onChange, getProjectPath } = {}) {
|
|
35
53
|
let data = {};
|
|
36
54
|
let parseError = null;
|
|
37
55
|
let rawFallback = "";
|
|
@@ -41,6 +59,32 @@ export function createFrontmatterEditor(root, { onChange } = {}) {
|
|
|
41
59
|
container.className = "frontmatter-form";
|
|
42
60
|
root.replaceChildren(container);
|
|
43
61
|
|
|
62
|
+
let mediaPickerTarget = null;
|
|
63
|
+
|
|
64
|
+
const mediaPickerContext = {
|
|
65
|
+
openMediaPicker: null,
|
|
66
|
+
setTarget(callback) {
|
|
67
|
+
mediaPickerTarget = callback;
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
if (getProjectPath) {
|
|
72
|
+
const { dialog, openMediaDialog } = createMediaDialog({
|
|
73
|
+
getProjectPath,
|
|
74
|
+
title: "Choose image",
|
|
75
|
+
selectMode: "path",
|
|
76
|
+
dialogId: "media-dialog-frontmatter",
|
|
77
|
+
onSelect(image) {
|
|
78
|
+
mediaPickerTarget?.(image.webPath);
|
|
79
|
+
},
|
|
80
|
+
onClose() {
|
|
81
|
+
mediaPickerTarget = null;
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
document.body.append(dialog);
|
|
85
|
+
mediaPickerContext.openMediaPicker = openMediaDialog;
|
|
86
|
+
}
|
|
87
|
+
|
|
44
88
|
function emitChange() {
|
|
45
89
|
if (silent || !onChange) {
|
|
46
90
|
return;
|
|
@@ -106,7 +150,7 @@ export function createFrontmatterEditor(root, { onChange } = {}) {
|
|
|
106
150
|
return;
|
|
107
151
|
}
|
|
108
152
|
|
|
109
|
-
container.appendChild(renderObjectEditor(data, refresh, touchData, 0));
|
|
153
|
+
container.appendChild(renderObjectEditor(data, refresh, touchData, 0, [], mediaPickerContext));
|
|
110
154
|
container.appendChild(renderAddFieldButton(data, refresh));
|
|
111
155
|
}
|
|
112
156
|
|
|
@@ -196,7 +240,7 @@ function renderAddFieldButton(objectValue, onRefresh, label = "Add field") {
|
|
|
196
240
|
return footer;
|
|
197
241
|
}
|
|
198
242
|
|
|
199
|
-
function renderObjectEditor(objectValue, onRefresh, onValueChange, depth) {
|
|
243
|
+
function renderObjectEditor(objectValue, onRefresh, onValueChange, depth, fieldPath, mediaPickerContext) {
|
|
200
244
|
const section = document.createElement("div");
|
|
201
245
|
section.className = "frontmatter-node frontmatter-object";
|
|
202
246
|
if (depth > 0) {
|
|
@@ -213,13 +257,33 @@ function renderObjectEditor(objectValue, onRefresh, onValueChange, depth) {
|
|
|
213
257
|
}
|
|
214
258
|
|
|
215
259
|
for (const [key, value] of entries) {
|
|
216
|
-
section.appendChild(
|
|
260
|
+
section.appendChild(
|
|
261
|
+
renderFieldRow(
|
|
262
|
+
key,
|
|
263
|
+
value,
|
|
264
|
+
objectValue,
|
|
265
|
+
onRefresh,
|
|
266
|
+
onValueChange,
|
|
267
|
+
depth,
|
|
268
|
+
[...fieldPath, key],
|
|
269
|
+
mediaPickerContext,
|
|
270
|
+
),
|
|
271
|
+
);
|
|
217
272
|
}
|
|
218
273
|
|
|
219
274
|
return section;
|
|
220
275
|
}
|
|
221
276
|
|
|
222
|
-
function renderFieldRow(
|
|
277
|
+
function renderFieldRow(
|
|
278
|
+
key,
|
|
279
|
+
value,
|
|
280
|
+
parentObject,
|
|
281
|
+
onRefresh,
|
|
282
|
+
onValueChange,
|
|
283
|
+
depth,
|
|
284
|
+
fieldPath,
|
|
285
|
+
mediaPickerContext,
|
|
286
|
+
) {
|
|
223
287
|
const field = document.createElement("div");
|
|
224
288
|
field.className = "frontmatter-field";
|
|
225
289
|
|
|
@@ -287,7 +351,15 @@ function renderFieldRow(key, value, parentObject, onRefresh, onValueChange, dept
|
|
|
287
351
|
const valueHost = document.createElement("div");
|
|
288
352
|
valueHost.className = "frontmatter-value-host";
|
|
289
353
|
valueHost.appendChild(
|
|
290
|
-
renderScalarEditor(
|
|
354
|
+
renderScalarEditor(
|
|
355
|
+
value,
|
|
356
|
+
currentType,
|
|
357
|
+
parentObject,
|
|
358
|
+
key,
|
|
359
|
+
onValueChange,
|
|
360
|
+
fieldPath,
|
|
361
|
+
mediaPickerContext,
|
|
362
|
+
),
|
|
291
363
|
);
|
|
292
364
|
row.append(keyInput, typeSelect, valueHost, removeButton);
|
|
293
365
|
}
|
|
@@ -298,7 +370,17 @@ function renderFieldRow(key, value, parentObject, onRefresh, onValueChange, dept
|
|
|
298
370
|
const children = document.createElement("div");
|
|
299
371
|
children.className = "frontmatter-children";
|
|
300
372
|
children.appendChild(
|
|
301
|
-
renderValueEditor(
|
|
373
|
+
renderValueEditor(
|
|
374
|
+
value,
|
|
375
|
+
currentType,
|
|
376
|
+
parentObject,
|
|
377
|
+
key,
|
|
378
|
+
onRefresh,
|
|
379
|
+
onValueChange,
|
|
380
|
+
depth + 1,
|
|
381
|
+
fieldPath,
|
|
382
|
+
mediaPickerContext,
|
|
383
|
+
),
|
|
302
384
|
);
|
|
303
385
|
field.appendChild(children);
|
|
304
386
|
}
|
|
@@ -314,6 +396,8 @@ function renderValueEditor(
|
|
|
314
396
|
onRefresh,
|
|
315
397
|
onValueChange,
|
|
316
398
|
depth,
|
|
399
|
+
fieldPath,
|
|
400
|
+
mediaPickerContext,
|
|
317
401
|
) {
|
|
318
402
|
if (type === "array") {
|
|
319
403
|
const arrayValue = Array.isArray(value) ? value : [];
|
|
@@ -321,7 +405,14 @@ function renderValueEditor(
|
|
|
321
405
|
parentContainer[parentKey] = arrayValue;
|
|
322
406
|
}
|
|
323
407
|
|
|
324
|
-
return renderArrayEditor(
|
|
408
|
+
return renderArrayEditor(
|
|
409
|
+
parentContainer[parentKey],
|
|
410
|
+
onRefresh,
|
|
411
|
+
onValueChange,
|
|
412
|
+
depth,
|
|
413
|
+
fieldPath,
|
|
414
|
+
mediaPickerContext,
|
|
415
|
+
);
|
|
325
416
|
}
|
|
326
417
|
|
|
327
418
|
if (type === "object") {
|
|
@@ -335,13 +426,36 @@ function renderValueEditor(
|
|
|
335
426
|
parentContainer[parentKey] = objectValue;
|
|
336
427
|
}
|
|
337
428
|
|
|
338
|
-
return renderNestedObjectEditor(
|
|
429
|
+
return renderNestedObjectEditor(
|
|
430
|
+
parentContainer[parentKey],
|
|
431
|
+
onRefresh,
|
|
432
|
+
onValueChange,
|
|
433
|
+
depth,
|
|
434
|
+
fieldPath,
|
|
435
|
+
mediaPickerContext,
|
|
436
|
+
);
|
|
339
437
|
}
|
|
340
438
|
|
|
341
|
-
return renderScalarEditor(
|
|
439
|
+
return renderScalarEditor(
|
|
440
|
+
value,
|
|
441
|
+
type,
|
|
442
|
+
parentContainer,
|
|
443
|
+
parentKey,
|
|
444
|
+
onValueChange,
|
|
445
|
+
fieldPath,
|
|
446
|
+
mediaPickerContext,
|
|
447
|
+
);
|
|
342
448
|
}
|
|
343
449
|
|
|
344
|
-
function renderScalarEditor(
|
|
450
|
+
function renderScalarEditor(
|
|
451
|
+
value,
|
|
452
|
+
type,
|
|
453
|
+
parentContainer,
|
|
454
|
+
parentKey,
|
|
455
|
+
onValueChange,
|
|
456
|
+
fieldPath = [],
|
|
457
|
+
mediaPickerContext = null,
|
|
458
|
+
) {
|
|
345
459
|
const wrapper = document.createElement("div");
|
|
346
460
|
wrapper.className = "frontmatter-scalar";
|
|
347
461
|
|
|
@@ -363,33 +477,82 @@ function renderScalarEditor(value, type, parentContainer, parentKey, onValueChan
|
|
|
363
477
|
return wrapper;
|
|
364
478
|
}
|
|
365
479
|
|
|
480
|
+
const stringValue =
|
|
481
|
+
type === "null" ? "" : value === null || value === undefined ? "" : String(value);
|
|
482
|
+
const useDateInput =
|
|
483
|
+
type === "string" &&
|
|
484
|
+
isDateField(fieldPath) &&
|
|
485
|
+
(stringValue === "" || isIsoDateString(stringValue));
|
|
486
|
+
|
|
366
487
|
const input = document.createElement("input");
|
|
367
|
-
input.type = type === "number" ? "number" : "text";
|
|
488
|
+
input.type = useDateInput ? "date" : type === "number" ? "number" : "text";
|
|
368
489
|
input.className = "frontmatter-value-input";
|
|
369
|
-
input.value =
|
|
370
|
-
|
|
371
|
-
input.placeholder = type === "null" ? "null" : "value";
|
|
490
|
+
input.value = stringValue;
|
|
491
|
+
input.placeholder = type === "null" ? "null" : useDateInput ? "YYYY-MM-DD" : "value";
|
|
372
492
|
input.spellcheck = false;
|
|
373
493
|
|
|
494
|
+
if (useDateInput) {
|
|
495
|
+
input.setAttribute("aria-label", `${fieldPath[fieldPath.length - 1]} date`);
|
|
496
|
+
}
|
|
497
|
+
|
|
374
498
|
input.addEventListener("input", () => {
|
|
375
499
|
parentContainer[parentKey] = coerceScalar(type, input.value);
|
|
376
500
|
onValueChange();
|
|
377
501
|
});
|
|
378
502
|
|
|
379
503
|
wrapper.appendChild(input);
|
|
504
|
+
|
|
505
|
+
if (
|
|
506
|
+
type === "string" &&
|
|
507
|
+
isBannerImageSrcField(fieldPath) &&
|
|
508
|
+
mediaPickerContext?.openMediaPicker
|
|
509
|
+
) {
|
|
510
|
+
const pickButton = document.createElement("button");
|
|
511
|
+
pickButton.type = "button";
|
|
512
|
+
pickButton.className = "frontmatter-media-btn";
|
|
513
|
+
pickButton.innerHTML = icons.image;
|
|
514
|
+
pickButton.setAttribute("aria-label", "Choose image from media library");
|
|
515
|
+
pickButton.title = "Choose image";
|
|
516
|
+
pickButton.addEventListener("click", () => {
|
|
517
|
+
mediaPickerContext.setTarget((webPath) => {
|
|
518
|
+
input.value = webPath;
|
|
519
|
+
parentContainer[parentKey] = webPath;
|
|
520
|
+
onValueChange();
|
|
521
|
+
});
|
|
522
|
+
mediaPickerContext.openMediaPicker();
|
|
523
|
+
});
|
|
524
|
+
wrapper.appendChild(pickButton);
|
|
525
|
+
}
|
|
526
|
+
|
|
380
527
|
return wrapper;
|
|
381
528
|
}
|
|
382
529
|
|
|
383
|
-
function renderNestedObjectEditor(
|
|
530
|
+
function renderNestedObjectEditor(
|
|
531
|
+
objectValue,
|
|
532
|
+
onRefresh,
|
|
533
|
+
onValueChange,
|
|
534
|
+
depth,
|
|
535
|
+
fieldPath,
|
|
536
|
+
mediaPickerContext,
|
|
537
|
+
) {
|
|
384
538
|
const wrapper = document.createElement("div");
|
|
385
539
|
wrapper.className = "frontmatter-nested";
|
|
386
540
|
|
|
387
|
-
wrapper.appendChild(
|
|
541
|
+
wrapper.appendChild(
|
|
542
|
+
renderObjectEditor(objectValue, onRefresh, onValueChange, depth, fieldPath, mediaPickerContext),
|
|
543
|
+
);
|
|
388
544
|
wrapper.appendChild(renderAddFieldButton(objectValue, onRefresh));
|
|
389
545
|
return wrapper;
|
|
390
546
|
}
|
|
391
547
|
|
|
392
|
-
function renderArrayEditor(
|
|
548
|
+
function renderArrayEditor(
|
|
549
|
+
arrayValue,
|
|
550
|
+
onRefresh,
|
|
551
|
+
onValueChange,
|
|
552
|
+
depth,
|
|
553
|
+
fieldPath,
|
|
554
|
+
mediaPickerContext,
|
|
555
|
+
) {
|
|
393
556
|
const wrapper = document.createElement("div");
|
|
394
557
|
wrapper.className = "frontmatter-array";
|
|
395
558
|
|
|
@@ -401,7 +564,18 @@ function renderArrayEditor(arrayValue, onRefresh, onValueChange, depth) {
|
|
|
401
564
|
}
|
|
402
565
|
|
|
403
566
|
arrayValue.forEach((item, index) => {
|
|
404
|
-
wrapper.appendChild(
|
|
567
|
+
wrapper.appendChild(
|
|
568
|
+
renderArrayItemRow(
|
|
569
|
+
item,
|
|
570
|
+
index,
|
|
571
|
+
arrayValue,
|
|
572
|
+
onRefresh,
|
|
573
|
+
onValueChange,
|
|
574
|
+
depth,
|
|
575
|
+
fieldPath,
|
|
576
|
+
mediaPickerContext,
|
|
577
|
+
),
|
|
578
|
+
);
|
|
405
579
|
});
|
|
406
580
|
|
|
407
581
|
const footer = document.createElement("div");
|
|
@@ -421,7 +595,16 @@ function renderArrayEditor(arrayValue, onRefresh, onValueChange, depth) {
|
|
|
421
595
|
return wrapper;
|
|
422
596
|
}
|
|
423
597
|
|
|
424
|
-
function renderArrayItemRow(
|
|
598
|
+
function renderArrayItemRow(
|
|
599
|
+
item,
|
|
600
|
+
index,
|
|
601
|
+
parentArray,
|
|
602
|
+
onRefresh,
|
|
603
|
+
onValueChange,
|
|
604
|
+
depth,
|
|
605
|
+
fieldPath,
|
|
606
|
+
mediaPickerContext,
|
|
607
|
+
) {
|
|
425
608
|
const entry = document.createElement("div");
|
|
426
609
|
entry.className = "frontmatter-array-entry";
|
|
427
610
|
|
|
@@ -470,7 +653,15 @@ function renderArrayItemRow(item, index, parentArray, onRefresh, onValueChange,
|
|
|
470
653
|
const valueHost = document.createElement("div");
|
|
471
654
|
valueHost.className = "frontmatter-value-host";
|
|
472
655
|
valueHost.appendChild(
|
|
473
|
-
renderScalarEditor(
|
|
656
|
+
renderScalarEditor(
|
|
657
|
+
item,
|
|
658
|
+
currentType,
|
|
659
|
+
parentArray,
|
|
660
|
+
index,
|
|
661
|
+
onValueChange,
|
|
662
|
+
[...fieldPath, String(index)],
|
|
663
|
+
mediaPickerContext,
|
|
664
|
+
),
|
|
474
665
|
);
|
|
475
666
|
row.append(marker, typeSelect, valueHost, removeButton);
|
|
476
667
|
}
|
|
@@ -481,7 +672,17 @@ function renderArrayItemRow(item, index, parentArray, onRefresh, onValueChange,
|
|
|
481
672
|
const children = document.createElement("div");
|
|
482
673
|
children.className = "frontmatter-children";
|
|
483
674
|
children.appendChild(
|
|
484
|
-
renderValueEditor(
|
|
675
|
+
renderValueEditor(
|
|
676
|
+
item,
|
|
677
|
+
currentType,
|
|
678
|
+
parentArray,
|
|
679
|
+
index,
|
|
680
|
+
onRefresh,
|
|
681
|
+
onValueChange,
|
|
682
|
+
depth + 1,
|
|
683
|
+
[...fieldPath, String(index)],
|
|
684
|
+
mediaPickerContext,
|
|
685
|
+
),
|
|
485
686
|
);
|
|
486
687
|
entry.appendChild(children);
|
|
487
688
|
}
|