starmark 1.0.1 → 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/README.md +30 -0
- package/package.json +1 -1
- package/public/app.js +515 -14
- package/public/frontmatter-editor.js +223 -22
- package/public/frontmatter.js +35 -0
- package/public/media-browser.js +464 -0
- package/public/styles.css +142 -5
- package/public/tools/31-image.js +25 -446
- package/src/server.js +15 -6
package/public/app.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { icons } from "./icons.js";
|
|
2
2
|
import { createFrontmatterEditor } from "./frontmatter-editor.js";
|
|
3
|
-
import { normalizeFrontmatter } from "./frontmatter.js";
|
|
3
|
+
import { normalizeFrontmatter, prepareImportedFrontmatter } from "./frontmatter.js";
|
|
4
4
|
|
|
5
5
|
const folderInput = document.getElementById("folder-path");
|
|
6
6
|
const browseBtn = document.getElementById("browse-btn");
|
|
@@ -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");
|
|
@@ -366,6 +367,46 @@ function saveEditorCaret() {
|
|
|
366
367
|
return { lineIndex: lineElements.length, offset: 0 };
|
|
367
368
|
}
|
|
368
369
|
|
|
370
|
+
function normalizeEditorDom() {
|
|
371
|
+
for (const child of [...markdownEditor.childNodes]) {
|
|
372
|
+
if (child.nodeType === Node.TEXT_NODE) {
|
|
373
|
+
if (!child.textContent?.trim()) {
|
|
374
|
+
child.remove();
|
|
375
|
+
continue;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const paragraph = document.createElement("p");
|
|
379
|
+
paragraph.textContent = child.textContent;
|
|
380
|
+
child.replaceWith(paragraph);
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
if (child.nodeType !== Node.ELEMENT_NODE) {
|
|
385
|
+
child.remove();
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const tagName = child.tagName.toLowerCase();
|
|
390
|
+
if (tagName === "br") {
|
|
391
|
+
child.remove();
|
|
392
|
+
continue;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
if (tagName === "div" && !child.matches(EDITOR_LINE_SELECTOR)) {
|
|
396
|
+
const paragraph = document.createElement("p");
|
|
397
|
+
while (child.firstChild) {
|
|
398
|
+
paragraph.appendChild(child.firstChild);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
if (paragraph.childNodes.length === 0) {
|
|
402
|
+
paragraph.append(document.createElement("br"));
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
child.replaceWith(paragraph);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
369
410
|
function getEditorLineElements() {
|
|
370
411
|
return [...markdownEditor.children].filter((child) => child.matches(EDITOR_LINE_SELECTOR));
|
|
371
412
|
}
|
|
@@ -375,6 +416,7 @@ function getEditorLines() {
|
|
|
375
416
|
}
|
|
376
417
|
|
|
377
418
|
function getEditorContent() {
|
|
419
|
+
normalizeEditorDom();
|
|
378
420
|
return getEditorLines().join("\n");
|
|
379
421
|
}
|
|
380
422
|
|
|
@@ -533,6 +575,40 @@ function runEditorHistoryAction(action) {
|
|
|
533
575
|
flushEditorHistory();
|
|
534
576
|
}
|
|
535
577
|
|
|
578
|
+
function insertParagraphAtEditorCaret() {
|
|
579
|
+
if (markdownEditor.dataset.loading || !currentEditFile) {
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
normalizeEditorDom();
|
|
584
|
+
const caret = getEditorCaretSnapshot();
|
|
585
|
+
if (!caret) {
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
const lines = getEditorLines();
|
|
590
|
+
const { lineIndex, offset } = caret;
|
|
591
|
+
const currentLine = lines[lineIndex] ?? "";
|
|
592
|
+
const before = currentLine.slice(0, offset);
|
|
593
|
+
const after = currentLine.slice(offset);
|
|
594
|
+
const nextLines = [
|
|
595
|
+
...lines.slice(0, lineIndex),
|
|
596
|
+
before,
|
|
597
|
+
after,
|
|
598
|
+
...lines.slice(lineIndex + 1),
|
|
599
|
+
];
|
|
600
|
+
|
|
601
|
+
renderMarkdownEditor(nextLines.join("\n"));
|
|
602
|
+
|
|
603
|
+
const lineElements = getEditorLineElements();
|
|
604
|
+
const nextLineElement = lineElements[lineIndex + 1];
|
|
605
|
+
if (nextLineElement) {
|
|
606
|
+
setCaretOffsetInElement(nextLineElement, 0);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
scheduleEditorHistoryCommit();
|
|
610
|
+
}
|
|
611
|
+
|
|
536
612
|
function insertMarkdownAtCaret(markdown, caret = pendingEditorCaret) {
|
|
537
613
|
if (!caret) {
|
|
538
614
|
return false;
|
|
@@ -810,9 +886,8 @@ function reevaluateMarkdownEditorLines() {
|
|
|
810
886
|
return;
|
|
811
887
|
}
|
|
812
888
|
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
);
|
|
889
|
+
normalizeEditorDom();
|
|
890
|
+
const lineElements = getEditorLineElements();
|
|
816
891
|
const lineStates = getEditorLineStates(lineElements.map(getEditorLineText));
|
|
817
892
|
|
|
818
893
|
lineElements.forEach((child, index) => {
|
|
@@ -1558,9 +1633,14 @@ function setFileInUrl(filePath) {
|
|
|
1558
1633
|
window.history.replaceState({}, "", url);
|
|
1559
1634
|
}
|
|
1560
1635
|
|
|
1636
|
+
function scrollMainToTop() {
|
|
1637
|
+
window.scrollTo(0, 0);
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1561
1640
|
function showListView() {
|
|
1562
1641
|
listView.hidden = false;
|
|
1563
1642
|
editView.hidden = true;
|
|
1643
|
+
scrollMainToTop();
|
|
1564
1644
|
currentEditFile = null;
|
|
1565
1645
|
currentEditFrontmatter = null;
|
|
1566
1646
|
frontmatterPanel.hidden = true;
|
|
@@ -1667,12 +1747,13 @@ async function saveCurrentFile() {
|
|
|
1667
1747
|
|
|
1668
1748
|
flushEditorHistory();
|
|
1669
1749
|
currentEditFrontmatter = normalizeFrontmatter(frontmatterEditor.getValue());
|
|
1670
|
-
const content = buildFileContent(currentEditFrontmatter, getEditorContent());
|
|
1671
1750
|
|
|
1672
1751
|
isSavingFile = true;
|
|
1673
1752
|
setSaveButtonState({ label: "Saving…", disabled: true });
|
|
1674
1753
|
|
|
1675
|
-
|
|
1754
|
+
const writeEditorContent = async () => {
|
|
1755
|
+
const body = getEditorContent();
|
|
1756
|
+
const content = buildFileContent(currentEditFrontmatter, body);
|
|
1676
1757
|
const response = await fetch("/api/file", {
|
|
1677
1758
|
method: "POST",
|
|
1678
1759
|
headers: { "Content-Type": "application/json" },
|
|
@@ -1682,6 +1763,11 @@ async function saveCurrentFile() {
|
|
|
1682
1763
|
}),
|
|
1683
1764
|
});
|
|
1684
1765
|
const data = await response.json();
|
|
1766
|
+
return { body, content, response, data };
|
|
1767
|
+
};
|
|
1768
|
+
|
|
1769
|
+
try {
|
|
1770
|
+
let { body, content, response, data } = await writeEditorContent();
|
|
1685
1771
|
|
|
1686
1772
|
if (!response.ok) {
|
|
1687
1773
|
setSaveButtonState({
|
|
@@ -1693,7 +1779,21 @@ async function saveCurrentFile() {
|
|
|
1693
1779
|
return;
|
|
1694
1780
|
}
|
|
1695
1781
|
|
|
1696
|
-
const
|
|
1782
|
+
const latestBody = getEditorContent();
|
|
1783
|
+
if (latestBody !== body) {
|
|
1784
|
+
({ body, content, response, data } = await writeEditorContent());
|
|
1785
|
+
if (!response.ok) {
|
|
1786
|
+
setSaveButtonState({
|
|
1787
|
+
label: data.error ?? "Save failed",
|
|
1788
|
+
disabled: false,
|
|
1789
|
+
error: true,
|
|
1790
|
+
});
|
|
1791
|
+
resetSaveButtonSoon(2500);
|
|
1792
|
+
return;
|
|
1793
|
+
}
|
|
1794
|
+
}
|
|
1795
|
+
|
|
1796
|
+
const { frontmatter, body: savedBody } = splitFrontmatter(content);
|
|
1697
1797
|
currentEditFrontmatter = normalizeFrontmatter(frontmatter ?? "");
|
|
1698
1798
|
frontmatterEditor.setValue(currentEditFrontmatter ?? "");
|
|
1699
1799
|
updateEditHeader(currentEditFile, currentEditFrontmatter);
|
|
@@ -1707,8 +1807,8 @@ async function saveCurrentFile() {
|
|
|
1707
1807
|
updateFileResults();
|
|
1708
1808
|
}
|
|
1709
1809
|
|
|
1710
|
-
renderMarkdownEditor(
|
|
1711
|
-
resetEditorHistory(
|
|
1810
|
+
renderMarkdownEditor(savedBody);
|
|
1811
|
+
resetEditorHistory(savedBody);
|
|
1712
1812
|
setSaveButtonState({ label: "Saved", disabled: false });
|
|
1713
1813
|
resetSaveButtonSoon();
|
|
1714
1814
|
} catch {
|
|
@@ -1737,6 +1837,7 @@ function handleFrontmatterInput() {
|
|
|
1737
1837
|
function showEditView() {
|
|
1738
1838
|
listView.hidden = true;
|
|
1739
1839
|
editView.hidden = false;
|
|
1840
|
+
scrollMainToTop();
|
|
1740
1841
|
}
|
|
1741
1842
|
|
|
1742
1843
|
async function openEditView(file) {
|
|
@@ -2332,7 +2433,40 @@ async function scanFolder(pathValue = folderInput.value.trim()) {
|
|
|
2332
2433
|
}
|
|
2333
2434
|
}
|
|
2334
2435
|
|
|
2335
|
-
|
|
2436
|
+
function isMarkdownEntryName(name) {
|
|
2437
|
+
const lower = name.trim().toLowerCase();
|
|
2438
|
+
return lower.endsWith(".md") || lower.endsWith(".mdx");
|
|
2439
|
+
}
|
|
2440
|
+
|
|
2441
|
+
function getPathsToExpand(relativePath) {
|
|
2442
|
+
const normalized = normalizeRelativePath(relativePath);
|
|
2443
|
+
const paths = new Set();
|
|
2444
|
+
|
|
2445
|
+
if (!normalized) {
|
|
2446
|
+
return paths;
|
|
2447
|
+
}
|
|
2448
|
+
|
|
2449
|
+
let current = "";
|
|
2450
|
+
for (const segment of normalized.split("/")) {
|
|
2451
|
+
current = current ? `${current}/${segment}` : segment;
|
|
2452
|
+
paths.add(current);
|
|
2453
|
+
}
|
|
2454
|
+
|
|
2455
|
+
return paths;
|
|
2456
|
+
}
|
|
2457
|
+
|
|
2458
|
+
async function fetchFileFrontmatter(absolutePath) {
|
|
2459
|
+
const response = await fetch(`/api/file?path=${encodeURIComponent(absolutePath)}`);
|
|
2460
|
+
const data = await response.json();
|
|
2461
|
+
|
|
2462
|
+
if (!response.ok) {
|
|
2463
|
+
throw new Error(data.error ?? "Could not read file");
|
|
2464
|
+
}
|
|
2465
|
+
|
|
2466
|
+
return data.frontmatter ?? "";
|
|
2467
|
+
}
|
|
2468
|
+
|
|
2469
|
+
async function createEntry(parentPath, name, { frontmatter = null } = {}) {
|
|
2336
2470
|
if (!currentProjectPath) {
|
|
2337
2471
|
return { ok: false, error: "Choose a project folder first" };
|
|
2338
2472
|
}
|
|
@@ -2360,6 +2494,7 @@ async function createEntry(parentPath, name) {
|
|
|
2360
2494
|
projectPath: currentProjectPath,
|
|
2361
2495
|
parentPath,
|
|
2362
2496
|
name: trimmedName,
|
|
2497
|
+
...(frontmatter ? { frontmatter } : {}),
|
|
2363
2498
|
}),
|
|
2364
2499
|
});
|
|
2365
2500
|
const data = await response.json();
|
|
@@ -2634,7 +2769,298 @@ function createConfirmDeleteDialog() {
|
|
|
2634
2769
|
|
|
2635
2770
|
const { openConfirmDeleteDialog } = createConfirmDeleteDialog();
|
|
2636
2771
|
|
|
2637
|
-
function
|
|
2772
|
+
function createFrontmatterSourceDialog() {
|
|
2773
|
+
const dialog = document.createElement("dialog");
|
|
2774
|
+
dialog.id = "frontmatter-source-dialog";
|
|
2775
|
+
dialog.className = "frontmatter-source-dialog";
|
|
2776
|
+
dialog.innerHTML = `
|
|
2777
|
+
<div class="dialog-header">
|
|
2778
|
+
<h2>Import frontmatter from</h2>
|
|
2779
|
+
<button type="button" class="dialog-close frontmatter-source-dialog-close" aria-label="Close">×</button>
|
|
2780
|
+
</div>
|
|
2781
|
+
<div class="frontmatter-source-body">
|
|
2782
|
+
<p class="frontmatter-source-hint">Choose a markdown file to copy its frontmatter.</p>
|
|
2783
|
+
<p class="frontmatter-source-error" hidden></p>
|
|
2784
|
+
<ul class="frontmatter-source-tree file-tree"></ul>
|
|
2785
|
+
</div>
|
|
2786
|
+
`;
|
|
2787
|
+
|
|
2788
|
+
const closeBtn = dialog.querySelector(".frontmatter-source-dialog-close");
|
|
2789
|
+
const treeRoot = dialog.querySelector(".frontmatter-source-tree");
|
|
2790
|
+
const errorEl = dialog.querySelector(".frontmatter-source-error");
|
|
2791
|
+
let onSelect = null;
|
|
2792
|
+
|
|
2793
|
+
function clearDialogError() {
|
|
2794
|
+
errorEl.hidden = true;
|
|
2795
|
+
errorEl.textContent = "";
|
|
2796
|
+
}
|
|
2797
|
+
|
|
2798
|
+
function showDialogError(message) {
|
|
2799
|
+
errorEl.textContent = message;
|
|
2800
|
+
errorEl.hidden = false;
|
|
2801
|
+
}
|
|
2802
|
+
|
|
2803
|
+
function createPickerNewPageRow(name, depth) {
|
|
2804
|
+
const item = document.createElement("li");
|
|
2805
|
+
item.className = "tree-file frontmatter-source-file frontmatter-source-new-page";
|
|
2806
|
+
item.style.setProperty("--depth", depth);
|
|
2807
|
+
item.dataset.newPageTarget = "true";
|
|
2808
|
+
|
|
2809
|
+
const badge = document.createElement("span");
|
|
2810
|
+
badge.className = "badge new-page";
|
|
2811
|
+
badge.innerHTML = `${icons.fileText}<span>new</span>`;
|
|
2812
|
+
|
|
2813
|
+
const label = document.createElement("span");
|
|
2814
|
+
label.className = "file-name";
|
|
2815
|
+
label.textContent = name;
|
|
2816
|
+
|
|
2817
|
+
const main = document.createElement("div");
|
|
2818
|
+
main.className = "tree-file-main";
|
|
2819
|
+
main.append(badge, label);
|
|
2820
|
+
item.append(main);
|
|
2821
|
+
|
|
2822
|
+
return item;
|
|
2823
|
+
}
|
|
2824
|
+
|
|
2825
|
+
function appendPickerChildren(container, node, depth, expandPaths, pickerContext) {
|
|
2826
|
+
for (const child of node.children) {
|
|
2827
|
+
container.append(createPickerRow(child, depth, expandPaths, pickerContext));
|
|
2828
|
+
}
|
|
2829
|
+
|
|
2830
|
+
if (pickerContext.newPageName && node.path === pickerContext.parentPath) {
|
|
2831
|
+
container.append(createPickerNewPageRow(pickerContext.newPageName, depth));
|
|
2832
|
+
}
|
|
2833
|
+
}
|
|
2834
|
+
|
|
2835
|
+
function createPickerFolderRow(node, depth, expandPaths, pickerContext) {
|
|
2836
|
+
const item = document.createElement("li");
|
|
2837
|
+
item.className = "tree-folder frontmatter-source-folder";
|
|
2838
|
+
item.style.setProperty("--depth", depth);
|
|
2839
|
+
|
|
2840
|
+
const details = document.createElement("details");
|
|
2841
|
+
details.open = expandPaths.has(node.path);
|
|
2842
|
+
|
|
2843
|
+
if (node.path === pickerContext.parentPath && !pickerContext.newPageName) {
|
|
2844
|
+
item.dataset.parentTarget = "true";
|
|
2845
|
+
}
|
|
2846
|
+
|
|
2847
|
+
const summary = document.createElement("summary");
|
|
2848
|
+
summary.className = "tree-folder-header frontmatter-source-folder-header";
|
|
2849
|
+
|
|
2850
|
+
const label = document.createElement("span");
|
|
2851
|
+
label.className = "tree-folder-name";
|
|
2852
|
+
label.textContent = depth === 0 ? (node.path || node.name) : node.name;
|
|
2853
|
+
|
|
2854
|
+
if (depth > 0) {
|
|
2855
|
+
summary.append(
|
|
2856
|
+
createFolderMain(createFolderPrefix(createFolderBadge(), createFolderChevron()), label),
|
|
2857
|
+
);
|
|
2858
|
+
} else if (depth === 0 && node.source) {
|
|
2859
|
+
summary.append(
|
|
2860
|
+
createFolderMain(
|
|
2861
|
+
createFolderPrefix(createSourceBadge(node.source), createFolderChevron()),
|
|
2862
|
+
label,
|
|
2863
|
+
),
|
|
2864
|
+
);
|
|
2865
|
+
} else {
|
|
2866
|
+
summary.append(createFolderMain(createFolderPrefix(createFolderChevron()), label));
|
|
2867
|
+
}
|
|
2868
|
+
|
|
2869
|
+
details.append(summary);
|
|
2870
|
+
|
|
2871
|
+
if (node.children.length > 0 || (pickerContext.newPageName && node.path === pickerContext.parentPath)) {
|
|
2872
|
+
const children = document.createElement("ul");
|
|
2873
|
+
children.className = "tree-children";
|
|
2874
|
+
appendPickerChildren(children, node, depth + 1, expandPaths, pickerContext);
|
|
2875
|
+
details.append(children);
|
|
2876
|
+
}
|
|
2877
|
+
|
|
2878
|
+
item.append(details);
|
|
2879
|
+
return item;
|
|
2880
|
+
}
|
|
2881
|
+
|
|
2882
|
+
function createPickerPageFolderRow(node, depth, expandPaths, pickerContext) {
|
|
2883
|
+
const { pageFile } = node;
|
|
2884
|
+
const item = document.createElement("li");
|
|
2885
|
+
item.className = "tree-folder tree-page-folder frontmatter-source-folder";
|
|
2886
|
+
item.style.setProperty("--depth", depth);
|
|
2887
|
+
|
|
2888
|
+
const details = document.createElement("details");
|
|
2889
|
+
details.open = expandPaths.has(node.path);
|
|
2890
|
+
|
|
2891
|
+
if (node.path === pickerContext.parentPath && !pickerContext.newPageName) {
|
|
2892
|
+
item.dataset.parentTarget = "true";
|
|
2893
|
+
}
|
|
2894
|
+
|
|
2895
|
+
const summary = document.createElement("summary");
|
|
2896
|
+
summary.className = "tree-folder-header frontmatter-source-folder-header";
|
|
2897
|
+
|
|
2898
|
+
const pageBadge = document.createElement("span");
|
|
2899
|
+
pageBadge.className = "badge page";
|
|
2900
|
+
pageBadge.innerHTML = `${icons.folder}<span>page folder</span>`;
|
|
2901
|
+
|
|
2902
|
+
const label = document.createElement("span");
|
|
2903
|
+
label.className = "tree-folder-name";
|
|
2904
|
+
label.textContent = node.name;
|
|
2905
|
+
|
|
2906
|
+
summary.append(
|
|
2907
|
+
createFolderMain(createFolderPrefix(pageBadge, createFolderChevron()), label),
|
|
2908
|
+
);
|
|
2909
|
+
|
|
2910
|
+
details.append(summary);
|
|
2911
|
+
|
|
2912
|
+
const children = document.createElement("ul");
|
|
2913
|
+
children.className = "tree-children";
|
|
2914
|
+
children.append(createPickerFileRow(
|
|
2915
|
+
{
|
|
2916
|
+
type: "file",
|
|
2917
|
+
name: pageFile.name,
|
|
2918
|
+
path: pageFile.relativePath,
|
|
2919
|
+
file: pageFile,
|
|
2920
|
+
},
|
|
2921
|
+
depth + 1,
|
|
2922
|
+
));
|
|
2923
|
+
appendPickerChildren(children, node, depth + 1, expandPaths, pickerContext);
|
|
2924
|
+
details.append(children);
|
|
2925
|
+
|
|
2926
|
+
item.append(details);
|
|
2927
|
+
return item;
|
|
2928
|
+
}
|
|
2929
|
+
|
|
2930
|
+
function createPickerFileRow(node, depth) {
|
|
2931
|
+
const file = node.file;
|
|
2932
|
+
const item = document.createElement("li");
|
|
2933
|
+
item.className = "tree-file frontmatter-source-file";
|
|
2934
|
+
item.style.setProperty("--depth", depth);
|
|
2935
|
+
|
|
2936
|
+
const extBadge = document.createElement("span");
|
|
2937
|
+
extBadge.className = `badge ${file.extension}`;
|
|
2938
|
+
extBadge.innerHTML = `${icons.fileText}<span>${file.extension}</span>`;
|
|
2939
|
+
|
|
2940
|
+
const name = document.createElement("span");
|
|
2941
|
+
name.className = "file-name";
|
|
2942
|
+
name.textContent = file.name;
|
|
2943
|
+
name.title = file.relativePath;
|
|
2944
|
+
|
|
2945
|
+
const main = document.createElement("div");
|
|
2946
|
+
main.className = "tree-file-main";
|
|
2947
|
+
main.append(extBadge, name);
|
|
2948
|
+
item.append(main);
|
|
2949
|
+
|
|
2950
|
+
item.addEventListener("click", async () => {
|
|
2951
|
+
if (!onSelect) {
|
|
2952
|
+
return;
|
|
2953
|
+
}
|
|
2954
|
+
|
|
2955
|
+
clearDialogError();
|
|
2956
|
+
|
|
2957
|
+
try {
|
|
2958
|
+
const sourceFrontmatter = await fetchFileFrontmatter(file.absolutePath);
|
|
2959
|
+
const importedFrontmatter = prepareImportedFrontmatter(sourceFrontmatter);
|
|
2960
|
+
|
|
2961
|
+
if (!importedFrontmatter) {
|
|
2962
|
+
showDialogError(`"${file.name}" has no frontmatter to import.`);
|
|
2963
|
+
return;
|
|
2964
|
+
}
|
|
2965
|
+
|
|
2966
|
+
onSelect({
|
|
2967
|
+
file,
|
|
2968
|
+
frontmatter: importedFrontmatter,
|
|
2969
|
+
});
|
|
2970
|
+
dialog.close();
|
|
2971
|
+
} catch (error) {
|
|
2972
|
+
showDialogError(error.message ?? "Could not import frontmatter");
|
|
2973
|
+
}
|
|
2974
|
+
});
|
|
2975
|
+
|
|
2976
|
+
return item;
|
|
2977
|
+
}
|
|
2978
|
+
|
|
2979
|
+
function createPickerRow(node, depth, expandPaths, pickerContext) {
|
|
2980
|
+
if (node.type === "folder") {
|
|
2981
|
+
return createPickerFolderRow(node, depth, expandPaths, pickerContext);
|
|
2982
|
+
}
|
|
2983
|
+
|
|
2984
|
+
if (node.type === "page-folder") {
|
|
2985
|
+
return createPickerPageFolderRow(node, depth, expandPaths, pickerContext);
|
|
2986
|
+
}
|
|
2987
|
+
|
|
2988
|
+
return createPickerFileRow(node, depth);
|
|
2989
|
+
}
|
|
2990
|
+
|
|
2991
|
+
function scrollPickerToTarget() {
|
|
2992
|
+
const target =
|
|
2993
|
+
treeRoot.querySelector("[data-new-page-target='true']") ??
|
|
2994
|
+
treeRoot.querySelector("[data-parent-target='true']");
|
|
2995
|
+
|
|
2996
|
+
if (!target) {
|
|
2997
|
+
return;
|
|
2998
|
+
}
|
|
2999
|
+
|
|
3000
|
+
target.scrollIntoView({ block: "center" });
|
|
3001
|
+
}
|
|
3002
|
+
|
|
3003
|
+
function renderPickerTree(parentPath, newPageName = "") {
|
|
3004
|
+
treeRoot.replaceChildren();
|
|
3005
|
+
|
|
3006
|
+
const expandPaths = getPathsToExpand(parentPath);
|
|
3007
|
+
const pickerContext = {
|
|
3008
|
+
parentPath: normalizeRelativePath(parentPath),
|
|
3009
|
+
newPageName: newPageName.trim(),
|
|
3010
|
+
};
|
|
3011
|
+
const markdownFiles = scannedFiles.filter((file) =>
|
|
3012
|
+
["md", "mdx"].includes(file.extension),
|
|
3013
|
+
);
|
|
3014
|
+
const tree = buildFileTree(markdownFiles, {
|
|
3015
|
+
directories: scannedDirectories,
|
|
3016
|
+
scanTargets: lastScanTargets,
|
|
3017
|
+
});
|
|
3018
|
+
|
|
3019
|
+
if (tree.length === 0) {
|
|
3020
|
+
const empty = document.createElement("li");
|
|
3021
|
+
empty.className = "frontmatter-source-empty";
|
|
3022
|
+
empty.textContent = "No markdown files found in this project.";
|
|
3023
|
+
treeRoot.append(empty);
|
|
3024
|
+
return;
|
|
3025
|
+
}
|
|
3026
|
+
|
|
3027
|
+
for (const node of tree) {
|
|
3028
|
+
treeRoot.append(createPickerRow(node, 0, expandPaths, pickerContext));
|
|
3029
|
+
}
|
|
3030
|
+
}
|
|
3031
|
+
|
|
3032
|
+
closeBtn.addEventListener("click", () => {
|
|
3033
|
+
dialog.close();
|
|
3034
|
+
});
|
|
3035
|
+
|
|
3036
|
+
dialog.addEventListener("click", (event) => {
|
|
3037
|
+
if (event.target === dialog) {
|
|
3038
|
+
dialog.close();
|
|
3039
|
+
}
|
|
3040
|
+
});
|
|
3041
|
+
|
|
3042
|
+
dialog.addEventListener("close", () => {
|
|
3043
|
+
onSelect = null;
|
|
3044
|
+
clearDialogError();
|
|
3045
|
+
treeRoot.replaceChildren();
|
|
3046
|
+
});
|
|
3047
|
+
|
|
3048
|
+
function openFrontmatterSourceDialog({ parentPath, newPageName = "" }, selectHandler) {
|
|
3049
|
+
onSelect = selectHandler;
|
|
3050
|
+
clearDialogError();
|
|
3051
|
+
renderPickerTree(parentPath, newPageName);
|
|
3052
|
+
dialog.showModal();
|
|
3053
|
+
requestAnimationFrame(() => {
|
|
3054
|
+
requestAnimationFrame(scrollPickerToTarget);
|
|
3055
|
+
});
|
|
3056
|
+
}
|
|
3057
|
+
|
|
3058
|
+
document.body.append(dialog);
|
|
3059
|
+
|
|
3060
|
+
return { openFrontmatterSourceDialog };
|
|
3061
|
+
}
|
|
3062
|
+
|
|
3063
|
+
function createNewItemDialog({ openFrontmatterSourceDialog }) {
|
|
2638
3064
|
const dialog = document.createElement("dialog");
|
|
2639
3065
|
dialog.id = "new-item-dialog";
|
|
2640
3066
|
dialog.className = "new-item-dialog";
|
|
@@ -2654,6 +3080,10 @@ function createNewItemDialog() {
|
|
|
2654
3080
|
placeholder="post.md or my-folder"
|
|
2655
3081
|
/>
|
|
2656
3082
|
<p class="new-item-hint">.md / .mdx creates a file; anything else creates a folder.</p>
|
|
3083
|
+
<div class="new-item-frontmatter" hidden>
|
|
3084
|
+
<button type="button" class="new-item-import-frontmatter">Import frontmatter from file…</button>
|
|
3085
|
+
<p class="new-item-frontmatter-source" hidden></p>
|
|
3086
|
+
</div>
|
|
2657
3087
|
<p class="new-item-error" hidden></p>
|
|
2658
3088
|
</div>
|
|
2659
3089
|
<div class="new-item-form-actions">
|
|
@@ -2665,8 +3095,12 @@ function createNewItemDialog() {
|
|
|
2665
3095
|
const closeBtn = dialog.querySelector(".new-item-dialog-close");
|
|
2666
3096
|
const form = dialog.querySelector(".new-item-form");
|
|
2667
3097
|
const nameInput = dialog.querySelector("#new-item-name");
|
|
3098
|
+
const frontmatterSection = dialog.querySelector(".new-item-frontmatter");
|
|
3099
|
+
const importFrontmatterBtn = dialog.querySelector(".new-item-import-frontmatter");
|
|
3100
|
+
const frontmatterSourceEl = dialog.querySelector(".new-item-frontmatter-source");
|
|
2668
3101
|
const errorEl = dialog.querySelector(".new-item-error");
|
|
2669
3102
|
let pendingParentPath = "";
|
|
3103
|
+
let pendingImportedFrontmatter = null;
|
|
2670
3104
|
|
|
2671
3105
|
function clearDialogError() {
|
|
2672
3106
|
errorEl.hidden = true;
|
|
@@ -2678,6 +3112,28 @@ function createNewItemDialog() {
|
|
|
2678
3112
|
errorEl.hidden = false;
|
|
2679
3113
|
}
|
|
2680
3114
|
|
|
3115
|
+
function clearImportedFrontmatter() {
|
|
3116
|
+
pendingImportedFrontmatter = null;
|
|
3117
|
+
frontmatterSourceEl.hidden = true;
|
|
3118
|
+
frontmatterSourceEl.textContent = "";
|
|
3119
|
+
}
|
|
3120
|
+
|
|
3121
|
+
function updateFrontmatterImportVisibility() {
|
|
3122
|
+
const showImport = isMarkdownEntryName(nameInput.value);
|
|
3123
|
+
frontmatterSection.hidden = !showImport;
|
|
3124
|
+
|
|
3125
|
+
if (!showImport) {
|
|
3126
|
+
clearImportedFrontmatter();
|
|
3127
|
+
}
|
|
3128
|
+
}
|
|
3129
|
+
|
|
3130
|
+
function setImportedFrontmatter(sourceName, frontmatter) {
|
|
3131
|
+
pendingImportedFrontmatter = frontmatter;
|
|
3132
|
+
frontmatterSourceEl.textContent = `Using frontmatter from ${sourceName}`;
|
|
3133
|
+
frontmatterSourceEl.hidden = false;
|
|
3134
|
+
clearDialogError();
|
|
3135
|
+
}
|
|
3136
|
+
|
|
2681
3137
|
closeBtn.addEventListener("click", () => {
|
|
2682
3138
|
dialog.close();
|
|
2683
3139
|
});
|
|
@@ -2690,11 +3146,29 @@ function createNewItemDialog() {
|
|
|
2690
3146
|
|
|
2691
3147
|
dialog.addEventListener("close", () => {
|
|
2692
3148
|
pendingParentPath = "";
|
|
3149
|
+
pendingImportedFrontmatter = null;
|
|
2693
3150
|
form.reset();
|
|
2694
3151
|
clearDialogError();
|
|
3152
|
+
clearImportedFrontmatter();
|
|
3153
|
+
frontmatterSection.hidden = true;
|
|
2695
3154
|
});
|
|
2696
3155
|
|
|
2697
|
-
nameInput.addEventListener("input",
|
|
3156
|
+
nameInput.addEventListener("input", () => {
|
|
3157
|
+
clearDialogError();
|
|
3158
|
+
updateFrontmatterImportVisibility();
|
|
3159
|
+
});
|
|
3160
|
+
|
|
3161
|
+
importFrontmatterBtn.addEventListener("click", () => {
|
|
3162
|
+
openFrontmatterSourceDialog(
|
|
3163
|
+
{
|
|
3164
|
+
parentPath: pendingParentPath,
|
|
3165
|
+
newPageName: nameInput.value.trim(),
|
|
3166
|
+
},
|
|
3167
|
+
({ file, frontmatter }) => {
|
|
3168
|
+
setImportedFrontmatter(file.name, frontmatter);
|
|
3169
|
+
},
|
|
3170
|
+
);
|
|
3171
|
+
});
|
|
2698
3172
|
|
|
2699
3173
|
form.addEventListener("submit", async (event) => {
|
|
2700
3174
|
event.preventDefault();
|
|
@@ -2705,7 +3179,9 @@ function createNewItemDialog() {
|
|
|
2705
3179
|
|
|
2706
3180
|
clearDialogError();
|
|
2707
3181
|
|
|
2708
|
-
const result = await createEntry(pendingParentPath, name
|
|
3182
|
+
const result = await createEntry(pendingParentPath, name, {
|
|
3183
|
+
frontmatter: pendingImportedFrontmatter,
|
|
3184
|
+
});
|
|
2709
3185
|
if (!result.ok) {
|
|
2710
3186
|
showDialogError(result.error);
|
|
2711
3187
|
nameInput.focus();
|
|
@@ -2718,7 +3194,9 @@ function createNewItemDialog() {
|
|
|
2718
3194
|
|
|
2719
3195
|
function openNewItemDialog(parentPath) {
|
|
2720
3196
|
pendingParentPath = parentPath;
|
|
3197
|
+
clearImportedFrontmatter();
|
|
2721
3198
|
clearDialogError();
|
|
3199
|
+
updateFrontmatterImportVisibility();
|
|
2722
3200
|
dialog.showModal();
|
|
2723
3201
|
nameInput.focus();
|
|
2724
3202
|
}
|
|
@@ -2728,7 +3206,8 @@ function createNewItemDialog() {
|
|
|
2728
3206
|
return { openNewItemDialog };
|
|
2729
3207
|
}
|
|
2730
3208
|
|
|
2731
|
-
const {
|
|
3209
|
+
const { openFrontmatterSourceDialog } = createFrontmatterSourceDialog();
|
|
3210
|
+
const { openNewItemDialog } = createNewItemDialog({ openFrontmatterSourceDialog });
|
|
2732
3211
|
|
|
2733
3212
|
browseBtn.addEventListener("click", browseFolder);
|
|
2734
3213
|
scanBtn.addEventListener("click", () => scanFolder());
|
|
@@ -2798,6 +3277,14 @@ function createImageLightbox() {
|
|
|
2798
3277
|
|
|
2799
3278
|
editBackBtn.insertAdjacentHTML("afterbegin", icons.chevronLeft);
|
|
2800
3279
|
|
|
3280
|
+
try {
|
|
3281
|
+
markdownEditor.focus({ preventScroll: true });
|
|
3282
|
+
document.execCommand("defaultParagraphSeparator", false, "p");
|
|
3283
|
+
markdownEditor.blur();
|
|
3284
|
+
} catch {
|
|
3285
|
+
// Browser may not support defaultParagraphSeparator.
|
|
3286
|
+
}
|
|
3287
|
+
|
|
2801
3288
|
editBackBtn.addEventListener("click", showListView);
|
|
2802
3289
|
editSaveBtn.addEventListener("click", saveCurrentFile);
|
|
2803
3290
|
markdownEditor.addEventListener("click", (event) => {
|
|
@@ -2829,7 +3316,21 @@ markdownEditor.addEventListener("beforeinput", (event) => {
|
|
|
2829
3316
|
}
|
|
2830
3317
|
});
|
|
2831
3318
|
|
|
3319
|
+
markdownEditor.addEventListener("focus", () => {
|
|
3320
|
+
try {
|
|
3321
|
+
document.execCommand("defaultParagraphSeparator", false, "p");
|
|
3322
|
+
} catch {
|
|
3323
|
+
// Browser may not support defaultParagraphSeparator.
|
|
3324
|
+
}
|
|
3325
|
+
});
|
|
3326
|
+
|
|
2832
3327
|
markdownEditor.addEventListener("keydown", (event) => {
|
|
3328
|
+
if (event.key === "Enter" && !event.shiftKey && !event.isComposing) {
|
|
3329
|
+
event.preventDefault();
|
|
3330
|
+
insertParagraphAtEditorCaret();
|
|
3331
|
+
return;
|
|
3332
|
+
}
|
|
3333
|
+
|
|
2833
3334
|
const isMod = event.metaKey || event.ctrlKey;
|
|
2834
3335
|
if (!isMod) {
|
|
2835
3336
|
return;
|