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/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
- const lineElements = [...markdownEditor.children].filter((child) =>
814
- child.matches(EDITOR_LINE_SELECTOR),
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
- try {
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 { frontmatter, body } = splitFrontmatter(content);
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(body);
1711
- resetEditorHistory(body);
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
- async function createEntry(parentPath, name) {
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 createNewItemDialog() {
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">&times;</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", clearDialogError);
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 { openNewItemDialog } = createNewItemDialog();
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;