@echozedlabs/codemirror 0.1.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/dist/index.js ADDED
@@ -0,0 +1,1330 @@
1
+ import { defaultKeymap, history, historyKeymap } from "@codemirror/commands";
2
+ import { markdown } from "@codemirror/lang-markdown";
3
+ import { searchKeymap } from "@codemirror/search";
4
+ import { Annotation, Compartment, EditorSelection, EditorState, Prec, RangeSetBuilder, StateField } from "@codemirror/state";
5
+ import { Decoration, EditorView, keymap, placeholder as placeholderExtension, WidgetType } from "@codemirror/view";
6
+ const readOnlyCompartment = new Compartment();
7
+ const editableCompartment = new Compartment();
8
+ const modeCompartment = new Compartment();
9
+ const allowFrontmatterEdit = Annotation.define();
10
+ export function createMarkdownEditorView(options) {
11
+ let mode = options.mode ?? "markdown";
12
+ let destroyed = false;
13
+ let suppressChange = false;
14
+ const view = new EditorView({
15
+ parent: options.parent,
16
+ state: EditorState.create({
17
+ doc: options.markdown ?? "",
18
+ extensions: createExtensions(options, () => mode, (transaction) => {
19
+ if (suppressChange || !transaction.docChanged) {
20
+ return;
21
+ }
22
+ const meta = {
23
+ source: transaction.isUserEvent("input") ? "user" : "programmatic",
24
+ mode,
25
+ timestamp: Date.now()
26
+ };
27
+ options.onChange?.(transaction.newDoc.toString(), meta);
28
+ })
29
+ })
30
+ });
31
+ if (options.autofocus) {
32
+ view.focus();
33
+ }
34
+ return {
35
+ focus() {
36
+ assertLive();
37
+ view.focus();
38
+ },
39
+ destroy() {
40
+ if (!destroyed) {
41
+ view.destroy();
42
+ destroyed = true;
43
+ }
44
+ },
45
+ getMarkdown() {
46
+ assertLive();
47
+ return view.state.doc.toString();
48
+ },
49
+ setMarkdown(markdownText, setOptions) {
50
+ assertLive();
51
+ const currentMarkdown = view.state.doc.toString();
52
+ if (currentMarkdown === markdownText) {
53
+ return;
54
+ }
55
+ suppressChange = setOptions?.emitChange !== true;
56
+ try {
57
+ view.dispatch({
58
+ changes: {
59
+ from: 0,
60
+ to: view.state.doc.length,
61
+ insert: markdownText
62
+ },
63
+ annotations: allowFrontmatterEdit.of(true)
64
+ });
65
+ }
66
+ finally {
67
+ suppressChange = false;
68
+ }
69
+ },
70
+ getSelection() {
71
+ assertLive();
72
+ return selectionFromState(view.state);
73
+ },
74
+ setSelection(selection) {
75
+ assertLive();
76
+ const bounded = clampSelection(selection, view.state.doc.length);
77
+ view.dispatch({
78
+ selection: EditorSelection.range(bounded.anchor, bounded.head),
79
+ scrollIntoView: true
80
+ });
81
+ },
82
+ insertMarkdown(markdownText) {
83
+ assertLive();
84
+ // Merge the replaceSelection spec with the scroll/userEvent options into a
85
+ // single transaction. Passing them as a second dispatch arg (the previous
86
+ // bug) silently dropped both — no scroll, and the change was mis-attributed
87
+ // as programmatic instead of a user input.
88
+ view.dispatch(view.state.update(view.state.replaceSelection(markdownText), {
89
+ scrollIntoView: true,
90
+ userEvent: "input"
91
+ }));
92
+ },
93
+ setReadOnly(readOnly) {
94
+ assertLive();
95
+ view.dispatch({
96
+ effects: [
97
+ readOnlyCompartment.reconfigure(EditorState.readOnly.of(readOnly)),
98
+ editableCompartment.reconfigure(EditorView.editable.of(!readOnly))
99
+ ]
100
+ });
101
+ },
102
+ setMode(nextMode, hybridFrontmatterMode) {
103
+ assertLive();
104
+ mode = nextMode;
105
+ if (hybridFrontmatterMode !== undefined) {
106
+ options.hybridFrontmatterMode = hybridFrontmatterMode;
107
+ }
108
+ // Reconfigure in place: selection, scroll, and undo history are kept.
109
+ view.dispatch({
110
+ effects: modeCompartment.reconfigure(modePlaceholder(mode, options))
111
+ });
112
+ }
113
+ };
114
+ function assertLive() {
115
+ if (destroyed) {
116
+ throw new Error("Markdown editor view has been destroyed.");
117
+ }
118
+ }
119
+ }
120
+ function createExtensions(options, getMode, onTransaction) {
121
+ const extensions = [
122
+ history(),
123
+ markdown(),
124
+ EditorView.lineWrapping,
125
+ keymap.of([...defaultKeymap, ...historyKeymap, ...searchKeymap]),
126
+ EditorView.updateListener.of((update) => {
127
+ for (const transaction of update.transactions) {
128
+ onTransaction(transaction);
129
+ }
130
+ }),
131
+ EditorState.transactionFilter.of((transaction) => {
132
+ if (getMode() === "hybrid"
133
+ && options.hybridFrontmatterMode !== "source"
134
+ && transaction.docChanged
135
+ && transaction.annotation(allowFrontmatterEdit) !== true
136
+ && transactionTouchesHiddenFrontmatter(transaction)) {
137
+ return [];
138
+ }
139
+ return transaction;
140
+ }),
141
+ readOnlyCompartment.of(EditorState.readOnly.of(options.readOnly === true)),
142
+ editableCompartment.of(EditorView.editable.of(options.readOnly !== true)),
143
+ modeCompartment.of(modePlaceholder(getMode(), options)),
144
+ EditorView.domEventHandlers({
145
+ beforeinput(event, view) {
146
+ if (view.state.readOnly) {
147
+ event.preventDefault();
148
+ return true;
149
+ }
150
+ if (getMode() === "hybrid" && isDestructiveBeforeInput(event)) {
151
+ const direction = event.inputType.includes("Forward") ? "forward" : "backward";
152
+ if (preventHiddenFrontmatterDelete(view, direction, options)) {
153
+ event.preventDefault();
154
+ return true;
155
+ }
156
+ }
157
+ return false;
158
+ }
159
+ })
160
+ ];
161
+ if (options.placeholder !== undefined) {
162
+ extensions.push(placeholderExtension(options.placeholder));
163
+ }
164
+ if (options.attributes !== undefined) {
165
+ extensions.push(EditorView.editorAttributes.of(options.attributes));
166
+ }
167
+ if (options.extensions !== undefined) {
168
+ extensions.push(options.extensions);
169
+ }
170
+ return extensions;
171
+ }
172
+ function modePlaceholder(mode, options) {
173
+ return mode === "hybrid" ? hybridMarkdownExtension(options) : [];
174
+ }
175
+ function hybridMarkdownExtension(options) {
176
+ const field = StateField.define({
177
+ create(state) {
178
+ return buildHybridDecorations(state, options);
179
+ },
180
+ update(decorations, transaction) {
181
+ return transaction.docChanged || transaction.selection
182
+ ? buildHybridDecorations(transaction.state, options)
183
+ : decorations;
184
+ }
185
+ });
186
+ return [
187
+ Prec.high(keymap.of([
188
+ {
189
+ key: "ArrowDown",
190
+ run: (view) => moveSelectionByLogicalLine(view, "down", options)
191
+ },
192
+ {
193
+ key: "ArrowUp",
194
+ run: (view) => moveSelectionByLogicalLine(view, "up", options)
195
+ },
196
+ {
197
+ key: "Backspace",
198
+ run: (view) => preventHiddenFrontmatterDelete(view, "backward", options)
199
+ },
200
+ {
201
+ key: "Delete",
202
+ run: (view) => preventHiddenFrontmatterDelete(view, "forward", options)
203
+ }
204
+ ])),
205
+ field,
206
+ EditorView.decorations.from(field)
207
+ ];
208
+ }
209
+ function buildHybridDecorations(state, options) {
210
+ const builder = new RangeSetBuilder();
211
+ const activeLine = state.doc.lineAt(state.selection.main.head).number;
212
+ let lineNumber = 1;
213
+ const frontmatter = findFrontmatterRange(state);
214
+ if (frontmatter && options.hybridFrontmatterMode !== "source") {
215
+ if (options.hybridFrontmatterMode !== "hidden") {
216
+ builder.add(frontmatter.from, frontmatter.to, Decoration.replace({
217
+ block: true,
218
+ widget: new FrontmatterPropertiesWidget(frontmatter.raw, frontmatter.from, frontmatter.to, options.frontmatterSchema, options.hybridFrontmatterMode === "collapsed")
219
+ }));
220
+ }
221
+ else {
222
+ builder.add(frontmatter.from, frontmatter.to, Decoration.replace({ block: true }));
223
+ }
224
+ lineNumber = frontmatter.endLine + 1;
225
+ }
226
+ while (lineNumber <= state.doc.lines) {
227
+ const line = state.doc.line(lineNumber);
228
+ const text = line.text;
229
+ const renderedBlock = findHybridRenderedBlock(state, lineNumber);
230
+ if (renderedBlock) {
231
+ if (activeLine < renderedBlock.startLine || activeLine > renderedBlock.endLine) {
232
+ builder.add(renderedBlock.from, renderedBlock.to, Decoration.replace({
233
+ block: true,
234
+ widget: new RenderedMarkdownBlockWidget(renderedBlock.raw, `hybrid-block-${renderedBlock.startLine}`, renderedBlock.from, options.hybridRenderMarkdown)
235
+ }));
236
+ }
237
+ lineNumber = renderedBlock.endLine + 1;
238
+ continue;
239
+ }
240
+ if (lineNumber === activeLine) {
241
+ lineNumber += 1;
242
+ continue;
243
+ }
244
+ const heading = text.match(/^(#{1,6})\s+(.+)$/);
245
+ if (heading) {
246
+ const markerEnd = line.from + heading[1].length + 1;
247
+ builder.add(line.from, line.from, Decoration.line({
248
+ class: `cm-me-hybrid-line cm-me-hybrid-heading cm-me-hybrid-heading-${heading[1].length}`
249
+ }));
250
+ builder.add(line.from, markerEnd, Decoration.replace({}));
251
+ lineNumber += 1;
252
+ continue;
253
+ }
254
+ const task = text.match(/^(\s*)[-*+]\s+\[([ xX])\]\s+/);
255
+ if (task) {
256
+ const markerStart = line.from + task[1].length;
257
+ const markerEnd = line.from + task[0].length;
258
+ builder.add(line.from, line.from, Decoration.line({ class: "cm-me-hybrid-line cm-me-hybrid-task-line" }));
259
+ builder.add(markerStart, markerEnd, Decoration.replace({ widget: new TaskCheckboxWidget(task[2].toLowerCase() === "x") }));
260
+ lineNumber += 1;
261
+ continue;
262
+ }
263
+ const unorderedList = text.match(/^(\s*)[-*+]\s+/);
264
+ if (unorderedList) {
265
+ const markerStart = line.from + unorderedList[1].length;
266
+ const markerEnd = line.from + unorderedList[0].length;
267
+ builder.add(line.from, line.from, Decoration.line({ class: "cm-me-hybrid-line cm-me-hybrid-list-line" }));
268
+ builder.add(markerStart, markerEnd, Decoration.replace({ widget: new BulletWidget() }));
269
+ lineNumber += 1;
270
+ continue;
271
+ }
272
+ const orderedList = text.match(/^(\s*)(\d+[.)])\s+/);
273
+ if (orderedList) {
274
+ const markerStart = line.from + orderedList[1].length;
275
+ const markerEnd = line.from + orderedList[0].length;
276
+ builder.add(line.from, line.from, Decoration.line({ class: "cm-me-hybrid-line cm-me-hybrid-list-line" }));
277
+ builder.add(markerStart, markerEnd, Decoration.replace({ widget: new OrderedMarkerWidget(orderedList[2]) }));
278
+ lineNumber += 1;
279
+ continue;
280
+ }
281
+ const blockquote = text.match(/^>\s+/);
282
+ if (blockquote) {
283
+ builder.add(line.from, line.from, Decoration.line({ class: "cm-me-hybrid-line cm-me-hybrid-blockquote" }));
284
+ builder.add(line.from, line.from + blockquote[0].length, Decoration.replace({}));
285
+ }
286
+ addInlineLinkDecorations(builder, line.from, text);
287
+ lineNumber += 1;
288
+ }
289
+ return builder.finish();
290
+ }
291
+ const TRASH_ICON_PATH = "M160 400c0 26.5 21.5 48 48 48h160c26.5 0 48-21.5 48-48V160H160v240zm64-192h32v176h-32V208zm96 0h32v176h-32V208zM352 96l-16-32h-96l-16 32h-80v32h288V96h-80z";
292
+ function setIconSvg(button, path) {
293
+ button.innerHTML = "";
294
+ const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
295
+ svg.setAttribute("viewBox", "0 0 576 512");
296
+ svg.setAttribute("focusable", "false");
297
+ svg.setAttribute("aria-hidden", "true");
298
+ svg.classList.add("cm-me-inline-icon");
299
+ const shape = document.createElementNS("http://www.w3.org/2000/svg", "path");
300
+ shape.setAttribute("fill", "currentColor");
301
+ shape.setAttribute("d", path);
302
+ svg.append(shape);
303
+ button.append(svg);
304
+ }
305
+ function isDestructiveBeforeInput(event) {
306
+ return event.inputType === "deleteContentBackward"
307
+ || event.inputType === "deleteContentForward"
308
+ || event.inputType === "deleteWordBackward"
309
+ || event.inputType === "deleteWordForward"
310
+ || event.inputType === "deleteHardLineBackward"
311
+ || event.inputType === "deleteHardLineForward"
312
+ || event.inputType === "deleteSoftLineBackward"
313
+ || event.inputType === "deleteSoftLineForward"
314
+ || event.inputType === "deleteByCut"
315
+ || event.inputType === "deleteByDrag";
316
+ }
317
+ function transactionTouchesHiddenFrontmatter(transaction) {
318
+ const frontmatter = findFrontmatterRange(transaction.startState);
319
+ if (!frontmatter) {
320
+ return false;
321
+ }
322
+ const protectedFrom = frontmatter.from;
323
+ const protectedTo = Math.min(transaction.startState.doc.length, frontmatter.to + 1);
324
+ let touchesProtectedRange = false;
325
+ transaction.changes.iterChanges((fromA, toA) => {
326
+ if (fromA < protectedTo && toA > protectedFrom) {
327
+ touchesProtectedRange = true;
328
+ }
329
+ });
330
+ return touchesProtectedRange;
331
+ }
332
+ function preventHiddenFrontmatterDelete(view, direction, options) {
333
+ if (options.hybridFrontmatterMode === "source") {
334
+ return false;
335
+ }
336
+ const frontmatter = findFrontmatterRange(view.state);
337
+ if (!frontmatter) {
338
+ return false;
339
+ }
340
+ const selection = view.state.selection.main;
341
+ if (!selection.empty) {
342
+ // In table/hidden hybrid mode the YAML block is represented by a widget, not
343
+ // editable text. If a selection reaches into that protected range, swallow
344
+ // destructive keys so Backspace/Delete cannot remove the YAML envelope.
345
+ return selection.from < frontmatter.to;
346
+ }
347
+ if (direction === "backward") {
348
+ // Cursor can sit at the first visible body position immediately after the
349
+ // replaced frontmatter block. Backspace there would delete the hidden YAML
350
+ // terminator/newline, making the item lose required properties.
351
+ return selection.head <= frontmatter.to + 1;
352
+ }
353
+ return selection.head < frontmatter.to;
354
+ }
355
+ function moveSelectionByLogicalLine(view, direction, options) {
356
+ const selection = view.state.selection.main;
357
+ if (!selection.empty) {
358
+ return false;
359
+ }
360
+ const currentLine = view.state.doc.lineAt(selection.head);
361
+ let targetLineNumber = direction === "down" ? currentLine.number + 1 : currentLine.number - 1;
362
+ if (targetLineNumber < 1 || targetLineNumber > view.state.doc.lines) {
363
+ return false;
364
+ }
365
+ const frontmatter = options.hybridFrontmatterMode === "source" ? null : findFrontmatterRange(view.state);
366
+ if (frontmatter &&
367
+ targetLineNumber >= frontmatter.startLine &&
368
+ targetLineNumber <= frontmatter.endLine) {
369
+ targetLineNumber = direction === "down" ? frontmatter.endLine + 1 : frontmatter.startLine - 1;
370
+ }
371
+ if (targetLineNumber < 1 || targetLineNumber > view.state.doc.lines) {
372
+ return false;
373
+ }
374
+ const column = selection.head - currentLine.from;
375
+ const targetLine = view.state.doc.line(targetLineNumber);
376
+ const targetPosition = Math.min(targetLine.from + column, targetLine.to);
377
+ view.dispatch({
378
+ selection: EditorSelection.cursor(targetPosition)
379
+ });
380
+ return true;
381
+ }
382
+ function findFrontmatterRange(state) {
383
+ if (state.doc.lines < 3 || state.doc.line(1).text.trim() !== "---") {
384
+ return null;
385
+ }
386
+ for (let lineNumber = 2; lineNumber <= state.doc.lines; lineNumber += 1) {
387
+ const line = state.doc.line(lineNumber);
388
+ if (line.text.trim() === "---") {
389
+ const from = state.doc.line(1).from;
390
+ const to = line.to;
391
+ const raw = state.sliceDoc(from, to);
392
+ return {
393
+ from,
394
+ to,
395
+ startLine: 1,
396
+ endLine: lineNumber,
397
+ raw,
398
+ entries: parseFrontmatter(raw)
399
+ };
400
+ }
401
+ }
402
+ return null;
403
+ }
404
+ function findFencedBlock(state, startLine) {
405
+ if (startLine < 1 || startLine > state.doc.lines) {
406
+ return null;
407
+ }
408
+ const firstLine = state.doc.line(startLine);
409
+ if (!/^```[\w-]*\s*$/.test(firstLine.text)) {
410
+ return null;
411
+ }
412
+ for (let lineNumber = startLine + 1; lineNumber <= state.doc.lines; lineNumber += 1) {
413
+ const line = state.doc.line(lineNumber);
414
+ if (line.text.startsWith("```")) {
415
+ return {
416
+ from: firstLine.from,
417
+ to: line.to,
418
+ startLine,
419
+ endLine: lineNumber,
420
+ raw: state.sliceDoc(firstLine.from, line.to)
421
+ };
422
+ }
423
+ }
424
+ return null;
425
+ }
426
+ function findHybridRenderedBlock(state, startLine) {
427
+ return findFencedBlock(state, startLine)
428
+ ?? findCalloutBlock(state, startLine)
429
+ ?? findTableBlock(state, startLine)
430
+ ?? findImageBlock(state, startLine);
431
+ }
432
+ function findCalloutBlock(state, startLine) {
433
+ if (startLine < 1 || startLine > state.doc.lines) {
434
+ return null;
435
+ }
436
+ const firstLine = state.doc.line(startLine);
437
+ if (!/^>\s?\[!\w+\]/.test(firstLine.text)) {
438
+ return null;
439
+ }
440
+ let endLineNumber = startLine;
441
+ while (endLineNumber + 1 <= state.doc.lines && state.doc.line(endLineNumber + 1).text.startsWith(">")) {
442
+ endLineNumber += 1;
443
+ }
444
+ const endLine = state.doc.line(endLineNumber);
445
+ return {
446
+ from: firstLine.from,
447
+ to: endLine.to,
448
+ startLine,
449
+ endLine: endLineNumber,
450
+ raw: state.sliceDoc(firstLine.from, endLine.to)
451
+ };
452
+ }
453
+ function findTableBlock(state, startLine) {
454
+ if (startLine < 1 || startLine + 1 > state.doc.lines) {
455
+ return null;
456
+ }
457
+ const firstLine = state.doc.line(startLine);
458
+ const separatorLine = state.doc.line(startLine + 1);
459
+ if (!isTableLine(firstLine.text) || !isTableSeparator(separatorLine.text)) {
460
+ return null;
461
+ }
462
+ let endLineNumber = startLine + 1;
463
+ while (endLineNumber + 1 <= state.doc.lines && isTableLine(state.doc.line(endLineNumber + 1).text)) {
464
+ endLineNumber += 1;
465
+ }
466
+ const endLine = state.doc.line(endLineNumber);
467
+ return {
468
+ from: firstLine.from,
469
+ to: endLine.to,
470
+ startLine,
471
+ endLine: endLineNumber,
472
+ raw: state.sliceDoc(firstLine.from, endLine.to)
473
+ };
474
+ }
475
+ function findImageBlock(state, startLine) {
476
+ if (startLine < 1 || startLine > state.doc.lines) {
477
+ return null;
478
+ }
479
+ const line = state.doc.line(startLine);
480
+ if (!/^!\[[^\]]*]\([^)]+\)$/.test(line.text.trim())) {
481
+ return null;
482
+ }
483
+ return {
484
+ from: line.from,
485
+ to: line.to,
486
+ startLine,
487
+ endLine: startLine,
488
+ raw: line.text
489
+ };
490
+ }
491
+ function isTableLine(text) {
492
+ return /^\|.*\|$/.test(text.trim());
493
+ }
494
+ function isTableSeparator(text) {
495
+ return /^\|\s*:?-{3,}:?\s*(\|\s*:?-{3,}:?\s*)+\|?$/.test(text.trim());
496
+ }
497
+ function addInlineLinkDecorations(builder, lineFrom, text) {
498
+ const inlineLinkPattern = /(!)?\[([^\]]+)]\(([^)]+)\)|\[\[([^\]|]+)(?:\|([^\]]+))?]]/g;
499
+ for (const match of text.matchAll(inlineLinkPattern)) {
500
+ if (match.index === undefined || match[1] === "!") {
501
+ continue;
502
+ }
503
+ const from = lineFrom + match.index;
504
+ const to = from + match[0].length;
505
+ if (match[2] !== undefined) {
506
+ builder.add(from, to, Decoration.replace({
507
+ widget: new InlineLinkWidget(match[2], match[3], from, false)
508
+ }));
509
+ continue;
510
+ }
511
+ const target = match[4];
512
+ const label = match[5] ?? target;
513
+ builder.add(from, to, Decoration.replace({
514
+ widget: new InlineLinkWidget(label, target, from, true)
515
+ }));
516
+ }
517
+ }
518
+ const frontmatterPropertyTypes = [
519
+ "text",
520
+ "date",
521
+ "time",
522
+ "datetime",
523
+ "tags",
524
+ "boolean",
525
+ "link"
526
+ ];
527
+ const frontmatterTypeLabels = {
528
+ text: "Text",
529
+ date: "Date",
530
+ time: "Time",
531
+ datetime: "Date and time",
532
+ tags: "Tags",
533
+ boolean: "Boolean",
534
+ link: "Link"
535
+ };
536
+ const frontmatterTypeIcons = {
537
+ text: "▭",
538
+ date: "◷",
539
+ time: "◴",
540
+ datetime: "◷",
541
+ tags: "#",
542
+ boolean: "✓",
543
+ link: "@"
544
+ };
545
+ function parseFrontmatter(raw) {
546
+ const entries = [];
547
+ const lines = raw.replace(/\r\n?/g, "\n").split("\n").slice(1, -1);
548
+ for (let index = 0; index < lines.length; index += 1) {
549
+ const line = lines[index];
550
+ const listStart = line.match(/^([A-Za-z0-9_-]+):\s*$/);
551
+ if (listStart) {
552
+ const items = [];
553
+ let listIndex = index + 1;
554
+ while (listIndex < lines.length) {
555
+ const item = lines[listIndex].match(/^\s{2,}-\s*(.*)$/);
556
+ if (!item) {
557
+ break;
558
+ }
559
+ items.push(normalizeFrontmatterScalar(item[1]));
560
+ listIndex += 1;
561
+ }
562
+ if (items.length > 0) {
563
+ const value = items.join(", ");
564
+ entries.push({ key: listStart[1], value, type: inferFrontmatterType(listStart[1], value, true) });
565
+ index = listIndex - 1;
566
+ continue;
567
+ }
568
+ }
569
+ const match = line.match(/^([A-Za-z0-9_-]+):\s*(.*)$/);
570
+ if (!match) {
571
+ continue;
572
+ }
573
+ const value = normalizeFrontmatterValue(match[2] || "");
574
+ entries.push({ key: match[1], value, type: inferFrontmatterType(match[1], value, false) });
575
+ }
576
+ return entries;
577
+ }
578
+ function updateFrontmatterEntry(raw, entryIndex, patch) {
579
+ const entries = parseFrontmatter(raw);
580
+ const current = entries[entryIndex];
581
+ if (!current) {
582
+ return raw;
583
+ }
584
+ entries[entryIndex] = normalizeFrontmatterEntry({ ...current, ...patch });
585
+ return serializeFrontmatter(raw, entries);
586
+ }
587
+ function addFrontmatterEntry(raw, schema) {
588
+ const entries = parseFrontmatter(raw);
589
+ const existingKeys = new Set(entries.map((entry) => entry.key.toLowerCase()));
590
+ const schemaEntry = schema
591
+ ?.slice()
592
+ .sort((left, right) => (left.order ?? 0) - (right.order ?? 0))
593
+ .find((entry) => !existingKeys.has(entry.key.toLowerCase()));
594
+ const type = supportedSchemaType(schemaEntry?.type);
595
+ entries.push({
596
+ key: schemaEntry?.key ?? nextFrontmatterKey(entries),
597
+ value: normalizeDefaultFrontmatterValue(schemaEntry?.defaultValue, type),
598
+ type
599
+ });
600
+ return serializeFrontmatter(raw, entries);
601
+ }
602
+ function normalizeDefaultFrontmatterValue(value, type) {
603
+ if (Array.isArray(value)) {
604
+ return value.join(", ");
605
+ }
606
+ if (typeof value === "boolean") {
607
+ return value ? "true" : "false";
608
+ }
609
+ return coerceFrontmatterValue(value ?? defaultValueForType(type), type);
610
+ }
611
+ function removeFrontmatterEntry(raw, entryIndex) {
612
+ const entries = parseFrontmatter(raw);
613
+ if (!entries[entryIndex]) {
614
+ return raw;
615
+ }
616
+ entries.splice(entryIndex, 1);
617
+ return serializeFrontmatter(raw, entries);
618
+ }
619
+ function moveFrontmatterEntry(raw, entryIndex, direction) {
620
+ const entries = parseFrontmatter(raw);
621
+ const targetIndex = direction === "up" ? entryIndex - 1 : entryIndex + 1;
622
+ return moveFrontmatterEntryTo(raw, entryIndex, targetIndex, entries);
623
+ }
624
+ function moveFrontmatterEntryTo(raw, entryIndex, targetIndex, parsedEntries = parseFrontmatter(raw)) {
625
+ const entries = parsedEntries.slice();
626
+ if (!entries[entryIndex] || targetIndex < 0 || targetIndex >= entries.length) {
627
+ return raw;
628
+ }
629
+ const [entry] = entries.splice(entryIndex, 1);
630
+ entries.splice(targetIndex, 0, entry);
631
+ return serializeFrontmatter(raw, entries);
632
+ }
633
+ function serializeFrontmatter(raw, entries) {
634
+ const newline = raw.includes("\r\n") ? "\r\n" : "\n";
635
+ const lines = ["---"];
636
+ for (const entry of entries.map(normalizeFrontmatterEntry)) {
637
+ if (entry.type === "tags") {
638
+ const items = splitFrontmatterTags(entry.value);
639
+ lines.push(`${entry.key}: ${items.join(", ")}`);
640
+ continue;
641
+ }
642
+ lines.push(`${entry.key}: ${serializeFrontmatterValue(entry)}`);
643
+ }
644
+ lines.push("---");
645
+ return lines.join(newline);
646
+ }
647
+ function normalizeFrontmatterEntry(entry) {
648
+ const type = frontmatterPropertyTypes.includes(entry.type) ? entry.type : "text";
649
+ return {
650
+ key: sanitizeFrontmatterKey(entry.key),
651
+ value: coerceFrontmatterValue(entry.value, type),
652
+ type
653
+ };
654
+ }
655
+ function normalizeFrontmatterValue(value) {
656
+ const trimmed = value.trim();
657
+ const inlineList = trimmed.match(/^\[(.*)]$/);
658
+ if (inlineList) {
659
+ return splitFrontmatterTags(inlineList[1]).join(", ");
660
+ }
661
+ return normalizeFrontmatterScalar(trimmed);
662
+ }
663
+ function normalizeFrontmatterScalar(value) {
664
+ const trimmed = value.trim();
665
+ if ((trimmed.startsWith('"') && trimmed.endsWith('"'))
666
+ || (trimmed.startsWith("'") && trimmed.endsWith("'"))) {
667
+ return trimmed.slice(1, -1);
668
+ }
669
+ return trimmed;
670
+ }
671
+ function inferFrontmatterType(key, value, fromList) {
672
+ const normalizedKey = key.toLowerCase();
673
+ const trimmed = value.trim();
674
+ if (/^(true|false)$/i.test(trimmed)) {
675
+ return "boolean";
676
+ }
677
+ if (/^\d{4}-\d{2}-\d{2}$/.test(trimmed)) {
678
+ return "date";
679
+ }
680
+ if (/^\d{2}:\d{2}(?::\d{2})?$/.test(trimmed)) {
681
+ return "time";
682
+ }
683
+ if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2})?$/.test(trimmed)) {
684
+ return "datetime";
685
+ }
686
+ if (/^https?:\/\//i.test(trimmed) || normalizedKey === "link" || normalizedKey === "url") {
687
+ return "link";
688
+ }
689
+ if (fromList
690
+ || normalizedKey === "tag"
691
+ || normalizedKey === "tags"
692
+ || normalizedKey === "category"
693
+ || normalizedKey === "categories"
694
+ || trimmed.includes(",")) {
695
+ return "tags";
696
+ }
697
+ return "text";
698
+ }
699
+ function serializeFrontmatterValue(entry) {
700
+ if (entry.type === "boolean") {
701
+ return /^true$/i.test(entry.value) ? "true" : "false";
702
+ }
703
+ return entry.value;
704
+ }
705
+ function coerceFrontmatterValue(value, type) {
706
+ const trimmed = value.trim();
707
+ if (type === "boolean") {
708
+ return /^true$/i.test(trimmed) ? "true" : "false";
709
+ }
710
+ if (type === "tags") {
711
+ return splitFrontmatterTags(trimmed).join(", ");
712
+ }
713
+ if (type === "date") {
714
+ return /^\d{4}-\d{2}-\d{2}$/.test(trimmed) ? trimmed : "";
715
+ }
716
+ if (type === "time") {
717
+ const time = trimmed.match(/^(\d{2}:\d{2})(?::\d{2})?$/);
718
+ return time ? time[1] : "";
719
+ }
720
+ if (type === "datetime") {
721
+ const datetime = trimmed.match(/^(\d{4}-\d{2}-\d{2})[T ](\d{2}:\d{2})(?::\d{2})?$/);
722
+ return datetime ? `${datetime[1]}T${datetime[2]}` : "";
723
+ }
724
+ return value.replace(/\r?\n/g, " ");
725
+ }
726
+ function splitFrontmatterTags(value) {
727
+ return value
728
+ .split(",")
729
+ .map((item) => normalizeFrontmatterScalar(item))
730
+ .filter((item) => item.length > 0);
731
+ }
732
+ function sanitizeFrontmatterKey(key) {
733
+ const sanitized = key.trim().replace(/\s+/g, "_").replace(/[^A-Za-z0-9_-]/g, "");
734
+ return sanitized || "property";
735
+ }
736
+ function nextFrontmatterKey(entries) {
737
+ const keys = new Set(entries.map((entry) => entry.key));
738
+ if (!keys.has("property")) {
739
+ return "property";
740
+ }
741
+ for (let index = 2;; index += 1) {
742
+ const key = `property_${index}`;
743
+ if (!keys.has(key)) {
744
+ return key;
745
+ }
746
+ }
747
+ }
748
+ function getFrontmatterSchemaEntry(schema, key) {
749
+ return schema?.find((entry) => entry.key.toLowerCase() === key.toLowerCase());
750
+ }
751
+ function supportedSchemaType(type) {
752
+ return type !== undefined && frontmatterPropertyTypes.includes(type)
753
+ ? type
754
+ : "text";
755
+ }
756
+ function entryTypeWithSchema(entry, schema) {
757
+ const schemaEntry = getFrontmatterSchemaEntry(schema, entry.key);
758
+ return schemaEntry ? supportedSchemaType(schemaEntry.type) : entry.type;
759
+ }
760
+ function typeIconForEntry(type, _schemaEntry) {
761
+ // The schema `icon` field is intentionally ignored here: hosts often use
762
+ // symbolic IDs such as "title" or "status", and rendering those strings as
763
+ // visible text makes rows read as "title Title". Keep the reusable widget
764
+ // graphical and type-oriented instead.
765
+ return frontmatterTypeIcons[type];
766
+ }
767
+ function labelForEntry(entry, schema) {
768
+ const schemaEntry = getFrontmatterSchemaEntry(schema, entry.key);
769
+ return schemaEntry?.label ?? entry.key;
770
+ }
771
+ function defaultValueForType(type) {
772
+ if (type === "boolean") {
773
+ return "false";
774
+ }
775
+ return "";
776
+ }
777
+ class TaskCheckboxWidget extends WidgetType {
778
+ checked;
779
+ constructor(checked) {
780
+ super();
781
+ this.checked = checked;
782
+ }
783
+ toDOM() {
784
+ const checkbox = document.createElement("span");
785
+ checkbox.className = this.checked ? "cm-me-task-checkbox cm-me-task-checkbox-checked" : "cm-me-task-checkbox";
786
+ checkbox.setAttribute("aria-hidden", "true");
787
+ return checkbox;
788
+ }
789
+ }
790
+ class BulletWidget extends WidgetType {
791
+ toDOM() {
792
+ const bullet = document.createElement("span");
793
+ bullet.className = "cm-me-list-bullet";
794
+ bullet.textContent = "•";
795
+ return bullet;
796
+ }
797
+ }
798
+ class OrderedMarkerWidget extends WidgetType {
799
+ marker;
800
+ constructor(marker) {
801
+ super();
802
+ this.marker = marker;
803
+ }
804
+ toDOM() {
805
+ const marker = document.createElement("span");
806
+ marker.className = "cm-me-ordered-marker";
807
+ marker.textContent = `${this.marker} `;
808
+ return marker;
809
+ }
810
+ }
811
+ class InlineLinkWidget extends WidgetType {
812
+ label;
813
+ href;
814
+ from;
815
+ wikiLink;
816
+ constructor(label, href, from, wikiLink) {
817
+ super();
818
+ this.label = label;
819
+ this.href = href;
820
+ this.from = from;
821
+ this.wikiLink = wikiLink;
822
+ }
823
+ toDOM(view) {
824
+ const link = document.createElement("a");
825
+ link.className = this.wikiLink ? "cm-me-hybrid-link cm-me-hybrid-wiki-link" : "cm-me-hybrid-link";
826
+ link.href = this.wikiLink ? `#${this.href}` : this.href;
827
+ link.textContent = this.label;
828
+ link.addEventListener("click", (event) => {
829
+ event.preventDefault();
830
+ });
831
+ link.addEventListener("mousedown", (event) => {
832
+ event.preventDefault();
833
+ view.dispatch({
834
+ selection: EditorSelection.cursor(this.from)
835
+ });
836
+ });
837
+ return link;
838
+ }
839
+ ignoreEvent() {
840
+ return false;
841
+ }
842
+ }
843
+ class FrontmatterPropertiesWidget extends WidgetType {
844
+ raw;
845
+ from;
846
+ to;
847
+ schema;
848
+ collapsed;
849
+ constructor(raw, from, to, schema, collapsed = false) {
850
+ super();
851
+ this.raw = raw;
852
+ this.from = from;
853
+ this.to = to;
854
+ this.schema = schema;
855
+ this.collapsed = collapsed;
856
+ }
857
+ toDOM(view) {
858
+ const wrapper = document.createElement("section");
859
+ wrapper.className = "cm-me-properties";
860
+ wrapper.setAttribute("aria-label", "Markdown properties");
861
+ const readOnly = view.state.readOnly;
862
+ const entries = parseFrontmatter(this.raw);
863
+ const details = document.createElement("details");
864
+ details.className = "cm-me-properties-details";
865
+ details.open = !this.collapsed;
866
+ const heading = document.createElement("summary");
867
+ heading.className = "cm-me-properties-heading";
868
+ heading.setAttribute("aria-label", this.collapsed ? "Show Markdown properties" : "Hide Markdown properties");
869
+ const headingText = document.createElement("span");
870
+ headingText.className = "cm-me-properties-heading-text";
871
+ headingText.textContent = "Properties";
872
+ // The summary chips are an at-a-glance view for the COLLAPSED state only.
873
+ // When the panel is expanded the property editor table below shows the same
874
+ // content, so the chips would just be redundant pills — omit them.
875
+ heading.append(headingText);
876
+ if (this.collapsed) {
877
+ heading.append(this.createSummaryChips(entries));
878
+ }
879
+ const list = document.createElement("div");
880
+ list.className = "cm-me-properties-table";
881
+ list.setAttribute("role", "list");
882
+ entries.forEach((entry, index) => {
883
+ const type = entryTypeWithSchema(entry, this.schema);
884
+ const schemaEntry = getFrontmatterSchemaEntry(this.schema, entry.key);
885
+ const normalizedEntry = type === entry.type ? entry : { ...entry, type };
886
+ const row = document.createElement("div");
887
+ const handleCell = document.createElement("div");
888
+ const nameCell = document.createElement("div");
889
+ const valueCell = document.createElement("div");
890
+ const actionCell = document.createElement("div");
891
+ row.className = "cm-me-property-row";
892
+ row.dataset.propertyType = type;
893
+ row.dataset.propertyKey = entry.key;
894
+ row.setAttribute("role", "listitem");
895
+ row.addEventListener("dragover", (event) => {
896
+ if (!readOnly) {
897
+ event.preventDefault();
898
+ }
899
+ });
900
+ row.addEventListener("drop", (event) => {
901
+ if (readOnly) {
902
+ return;
903
+ }
904
+ event.preventDefault();
905
+ const sourceIndex = Number(event.dataTransfer?.getData("text/plain"));
906
+ if (Number.isInteger(sourceIndex)) {
907
+ this.commitRaw(view, moveFrontmatterEntryTo(this.raw, sourceIndex, index, entries));
908
+ }
909
+ });
910
+ handleCell.className = "cm-me-property-order-cell";
911
+ nameCell.className = "cm-me-property-key-cell";
912
+ valueCell.className = "cm-me-property-value-cell";
913
+ actionCell.className = "cm-me-property-remove-cell";
914
+ const dragHandle = this.createButton("", `Drag ${entry.key} property`, readOnly, () => undefined);
915
+ dragHandle.className = "cm-me-property-drag-handle";
916
+ dragHandle.draggable = !readOnly;
917
+ dragHandle.title = `Drag ${entry.key} property`;
918
+ dragHandle.addEventListener("dragstart", (event) => {
919
+ if (readOnly || !event.dataTransfer) {
920
+ event.preventDefault();
921
+ return;
922
+ }
923
+ event.dataTransfer.effectAllowed = "move";
924
+ event.dataTransfer.setData("text/plain", String(index));
925
+ });
926
+ dragHandle.addEventListener("keydown", (event) => {
927
+ if (readOnly || !event.altKey || (event.key !== "ArrowUp" && event.key !== "ArrowDown")) {
928
+ return;
929
+ }
930
+ event.preventDefault();
931
+ this.commitRaw(view, moveFrontmatterEntry(this.raw, index, event.key === "ArrowUp" ? "up" : "down"));
932
+ });
933
+ handleCell.append(dragHandle);
934
+ nameCell.append(this.createNameMenu(view, normalizedEntry, index, readOnly, schemaEntry));
935
+ valueCell.append(this.createValueControl(view, normalizedEntry, index, readOnly, schemaEntry));
936
+ const removeButton = this.createButton("", `Remove ${entry.key} property`, readOnly, () => {
937
+ this.commitRaw(view, removeFrontmatterEntry(this.raw, index));
938
+ });
939
+ removeButton.classList.add("cm-me-property-remove");
940
+ setIconSvg(removeButton, TRASH_ICON_PATH);
941
+ actionCell.append(removeButton);
942
+ row.append(handleCell, nameCell, valueCell, actionCell);
943
+ list.append(row);
944
+ });
945
+ if (entries.length === 0) {
946
+ const empty = document.createElement("div");
947
+ empty.className = "cm-me-property-empty";
948
+ empty.textContent = "No properties";
949
+ list.append(empty);
950
+ }
951
+ const addButton = this.createButton("+ Add property", "Add property", readOnly, () => {
952
+ this.commitRaw(view, addFrontmatterEntry(this.raw, this.schema));
953
+ });
954
+ addButton.classList.add("cm-me-property-add");
955
+ details.append(heading, list, addButton);
956
+ wrapper.append(details);
957
+ return wrapper;
958
+ }
959
+ ignoreEvent() {
960
+ return true;
961
+ }
962
+ createSummaryChips(entries) {
963
+ const chips = document.createElement("span");
964
+ chips.className = "cm-me-properties-chips";
965
+ const visibleEntries = entries.filter((entry) => entry.key.toLowerCase() !== "title").slice(0, 4);
966
+ for (const entry of visibleEntries) {
967
+ const chip = document.createElement("span");
968
+ chip.className = "cm-me-property-chip";
969
+ chip.dataset.propertyKey = entry.key;
970
+ const label = labelForEntry(entry, this.schema);
971
+ const value = entry.value.trim();
972
+ chip.textContent = value ? `${label}: ${value}` : label;
973
+ chips.append(chip);
974
+ }
975
+ if (entries.length > visibleEntries.length) {
976
+ const more = document.createElement("span");
977
+ more.className = "cm-me-property-chip cm-me-property-chip--muted";
978
+ more.textContent = `+${entries.length - visibleEntries.length} more`;
979
+ chips.append(more);
980
+ }
981
+ return chips;
982
+ }
983
+ createNameMenu(view, entry, entryIndex, readOnly, schemaEntry) {
984
+ const details = document.createElement("details");
985
+ const summary = document.createElement("summary");
986
+ const icon = document.createElement("span");
987
+ const label = document.createElement("span");
988
+ const menu = document.createElement("div");
989
+ const keyLabel = document.createElement("label");
990
+ const keyInput = document.createElement("input");
991
+ const typeMenu = document.createElement("div");
992
+ const suggestions = this.schema?.filter((schema) => schema.key.toLowerCase() !== entry.key.toLowerCase()) ?? [];
993
+ details.className = "cm-me-property-menu-details";
994
+ details.addEventListener("toggle", () => {
995
+ if (details.open) {
996
+ setTimeout(() => keyInput.focus(), 0);
997
+ }
998
+ });
999
+ summary.className = "cm-me-property-summary";
1000
+ summary.setAttribute("aria-label", `${entry.key} property settings`);
1001
+ summary.title = `${frontmatterTypeLabels[entry.type]} property`;
1002
+ icon.className = "cm-me-property-type-icon";
1003
+ icon.textContent = typeIconForEntry(entry.type, schemaEntry);
1004
+ icon.setAttribute("aria-hidden", "true");
1005
+ label.className = "cm-me-property-name";
1006
+ label.textContent = labelForEntry(entry, this.schema);
1007
+ summary.append(icon, label);
1008
+ menu.className = "cm-me-property-menu";
1009
+ keyLabel.className = "cm-me-property-menu-label";
1010
+ keyLabel.textContent = "Name";
1011
+ keyInput.className = "cm-me-property-key-input";
1012
+ keyInput.value = entry.key;
1013
+ keyInput.disabled = readOnly;
1014
+ keyInput.setAttribute("aria-label", `Property name for ${entry.key}`);
1015
+ keyInput.addEventListener("change", () => {
1016
+ const schemaMatch = getFrontmatterSchemaEntry(this.schema, keyInput.value);
1017
+ const nextType = schemaMatch ? supportedSchemaType(schemaMatch.type) : entry.type;
1018
+ this.commitRaw(view, updateFrontmatterEntry(this.raw, entryIndex, {
1019
+ key: keyInput.value,
1020
+ type: nextType,
1021
+ value: schemaMatch?.defaultValue === undefined
1022
+ ? coerceFrontmatterValue(entry.value, nextType)
1023
+ : normalizeDefaultFrontmatterValue(schemaMatch.defaultValue, nextType)
1024
+ }));
1025
+ });
1026
+ this.blurOnEnter(keyInput);
1027
+ keyLabel.append(keyInput);
1028
+ menu.append(keyLabel);
1029
+ if (suggestions.length > 0) {
1030
+ const suggestionList = document.createElement("div");
1031
+ suggestionList.className = "cm-me-property-suggestions";
1032
+ for (const suggestion of suggestions) {
1033
+ const suggestionButton = this.createButton(suggestion.label ?? suggestion.key, `Use ${suggestion.label ?? suggestion.key} property`, readOnly, () => {
1034
+ const type = supportedSchemaType(suggestion.type);
1035
+ this.commitRaw(view, updateFrontmatterEntry(this.raw, entryIndex, {
1036
+ key: suggestion.key,
1037
+ type,
1038
+ value: normalizeDefaultFrontmatterValue(suggestion.defaultValue, type)
1039
+ }));
1040
+ });
1041
+ suggestionButton.classList.add("cm-me-property-suggestion");
1042
+ suggestionList.append(suggestionButton);
1043
+ }
1044
+ menu.append(suggestionList);
1045
+ }
1046
+ typeMenu.className = "cm-me-property-type-menu";
1047
+ typeMenu.setAttribute("role", "menu");
1048
+ for (const type of frontmatterPropertyTypes) {
1049
+ const typeButton = this.createButton(`${frontmatterTypeIcons[type]} ${frontmatterTypeLabels[type]}`, `Set ${entry.key} property type to ${frontmatterTypeLabels[type]}`, readOnly, () => {
1050
+ this.commitRaw(view, updateFrontmatterEntry(this.raw, entryIndex, {
1051
+ type,
1052
+ value: coerceFrontmatterValue(entry.value, type)
1053
+ }));
1054
+ });
1055
+ typeButton.classList.add("cm-me-property-type-option");
1056
+ typeButton.dataset.active = type === entry.type ? "true" : "false";
1057
+ typeButton.setAttribute("aria-pressed", type === entry.type ? "true" : "false");
1058
+ typeButton.setAttribute("role", "menuitemradio");
1059
+ typeButton.setAttribute("aria-checked", type === entry.type ? "true" : "false");
1060
+ typeMenu.append(typeButton);
1061
+ }
1062
+ menu.append(typeMenu);
1063
+ details.append(summary, menu);
1064
+ return details;
1065
+ }
1066
+ createValueControl(view, entry, entryIndex, readOnly, schemaEntry) {
1067
+ if (entry.type === "boolean") {
1068
+ const checkbox = document.createElement("input");
1069
+ checkbox.className = "cm-me-property-input cm-me-property-boolean-input";
1070
+ checkbox.type = "checkbox";
1071
+ checkbox.checked = /^true$/i.test(entry.value);
1072
+ checkbox.disabled = readOnly;
1073
+ checkbox.setAttribute("aria-label", `${entry.key} property value`);
1074
+ checkbox.addEventListener("change", () => {
1075
+ this.commitRaw(view, updateFrontmatterEntry(this.raw, entryIndex, {
1076
+ value: checkbox.checked ? "true" : "false"
1077
+ }));
1078
+ });
1079
+ return checkbox;
1080
+ }
1081
+ if (entry.type === "tags") {
1082
+ return this.createTagsControl(view, entry, entryIndex, readOnly, schemaEntry);
1083
+ }
1084
+ const wrapper = document.createElement("div");
1085
+ const input = document.createElement("input");
1086
+ wrapper.className = "cm-me-property-input-wrap";
1087
+ input.className = "cm-me-property-input";
1088
+ input.value = entry.value;
1089
+ input.disabled = readOnly;
1090
+ input.setAttribute("aria-label", `${entry.key} property value`);
1091
+ if (entry.type === "date") {
1092
+ input.type = "date";
1093
+ }
1094
+ else if (entry.type === "time") {
1095
+ input.type = "time";
1096
+ }
1097
+ else if (entry.type === "datetime") {
1098
+ input.type = "datetime-local";
1099
+ }
1100
+ else if (entry.type === "link") {
1101
+ input.type = "url";
1102
+ }
1103
+ else {
1104
+ input.type = "text";
1105
+ }
1106
+ input.addEventListener("change", () => {
1107
+ this.commitRaw(view, updateFrontmatterEntry(this.raw, entryIndex, { value: input.value }));
1108
+ });
1109
+ this.blurOnEnter(input);
1110
+ wrapper.append(input);
1111
+ if (schemaEntry?.allowedValues && schemaEntry.allowedValues.length > 0) {
1112
+ const listId = `cm-me-${entry.key}-values-${entryIndex}`;
1113
+ input.setAttribute("list", listId);
1114
+ wrapper.append(this.createAllowedValuesDatalist(listId, schemaEntry.allowedValues));
1115
+ }
1116
+ if (entry.type === "date" || entry.type === "time" || entry.type === "datetime") {
1117
+ const pickerButton = this.createButton("Pick", `Open ${entry.key} picker`, readOnly, () => {
1118
+ const pickerInput = input;
1119
+ if (typeof pickerInput.showPicker === "function") {
1120
+ pickerInput.showPicker();
1121
+ }
1122
+ else {
1123
+ pickerInput.focus();
1124
+ }
1125
+ });
1126
+ pickerButton.classList.add("cm-me-property-picker");
1127
+ wrapper.append(pickerButton);
1128
+ }
1129
+ return wrapper;
1130
+ }
1131
+ createTagsControl(view, entry, entryIndex, readOnly, schemaEntry) {
1132
+ const tags = splitFrontmatterTags(entry.value);
1133
+ const wrapper = document.createElement("div");
1134
+ const input = document.createElement("input");
1135
+ wrapper.className = "cm-me-property-tags";
1136
+ wrapper.setAttribute("aria-label", `${entry.key} property value`);
1137
+ const commitTags = (nextTags) => {
1138
+ this.commitRaw(view, updateFrontmatterEntry(this.raw, entryIndex, {
1139
+ value: nextTags.join(", ")
1140
+ }));
1141
+ };
1142
+ for (const tag of tags) {
1143
+ const token = document.createElement("span");
1144
+ const label = document.createElement("span");
1145
+ const remove = this.createButton("x", `Remove ${tag} tag`, readOnly, () => {
1146
+ commitTags(tags.filter((item) => item !== tag));
1147
+ });
1148
+ token.className = "cm-me-property-tag";
1149
+ label.textContent = tag;
1150
+ remove.className = "cm-me-property-tag-remove";
1151
+ token.append(label, remove);
1152
+ wrapper.append(token);
1153
+ }
1154
+ input.className = "cm-me-property-tag-input";
1155
+ input.disabled = readOnly;
1156
+ input.type = "text";
1157
+ input.placeholder = "Add tag";
1158
+ input.setAttribute("aria-label", `${entry.key} tag entry`);
1159
+ if (schemaEntry?.allowedValues && schemaEntry.allowedValues.length > 0) {
1160
+ const listId = `cm-me-${entry.key}-tag-values-${entryIndex}`;
1161
+ input.setAttribute("list", listId);
1162
+ input.setAttribute("aria-description", `Choose an existing ${entry.key} value or type a new one.`);
1163
+ wrapper.append(this.createAllowedValuesDatalist(listId, schemaEntry.allowedValues, tags));
1164
+ }
1165
+ input.addEventListener("keydown", (event) => {
1166
+ if (event.key === "Enter") {
1167
+ event.preventDefault();
1168
+ const nextTag = input.value.trim();
1169
+ if (nextTag.length > 0) {
1170
+ commitTags([...tags, nextTag]);
1171
+ }
1172
+ return;
1173
+ }
1174
+ if (event.key === "Backspace" && input.value.length === 0 && tags.length > 0) {
1175
+ event.preventDefault();
1176
+ commitTags(tags.slice(0, -1));
1177
+ }
1178
+ });
1179
+ input.addEventListener("blur", () => {
1180
+ const nextTag = input.value.trim();
1181
+ if (nextTag.length > 0) {
1182
+ commitTags([...tags, nextTag]);
1183
+ }
1184
+ });
1185
+ wrapper.append(input);
1186
+ return wrapper;
1187
+ }
1188
+ createAllowedValuesDatalist(id, allowedValues, excludedValues = []) {
1189
+ const datalist = document.createElement("datalist");
1190
+ const excluded = new Set(excludedValues.map((value) => value.toLowerCase()));
1191
+ datalist.id = id;
1192
+ for (const value of allowedValues) {
1193
+ const normalized = value.trim();
1194
+ if (normalized.length === 0 || excluded.has(normalized.toLowerCase())) {
1195
+ continue;
1196
+ }
1197
+ const option = document.createElement("option");
1198
+ option.value = normalized;
1199
+ datalist.append(option);
1200
+ }
1201
+ return datalist;
1202
+ }
1203
+ createButton(label, ariaLabel, disabled, onClick) {
1204
+ const button = document.createElement("button");
1205
+ button.type = "button";
1206
+ button.textContent = label;
1207
+ button.disabled = disabled;
1208
+ button.setAttribute("aria-label", ariaLabel);
1209
+ button.addEventListener("click", () => {
1210
+ onClick();
1211
+ });
1212
+ return button;
1213
+ }
1214
+ blurOnEnter(input) {
1215
+ input.addEventListener("keydown", (event) => {
1216
+ if (event.key === "Enter") {
1217
+ event.preventDefault();
1218
+ input.blur();
1219
+ }
1220
+ });
1221
+ }
1222
+ commitRaw(view, nextRaw) {
1223
+ if (nextRaw === this.raw || view.state.readOnly) {
1224
+ return;
1225
+ }
1226
+ view.dispatch({
1227
+ changes: { from: this.from, to: this.to, insert: nextRaw },
1228
+ userEvent: "input",
1229
+ annotations: allowFrontmatterEdit.of(true)
1230
+ });
1231
+ }
1232
+ }
1233
+ class RenderedMarkdownBlockWidget extends WidgetType {
1234
+ markdown;
1235
+ blockId;
1236
+ from;
1237
+ renderMarkdown;
1238
+ controller;
1239
+ constructor(markdown, blockId, from, renderMarkdown) {
1240
+ super();
1241
+ this.markdown = markdown;
1242
+ this.blockId = blockId;
1243
+ this.from = from;
1244
+ this.renderMarkdown = renderMarkdown;
1245
+ }
1246
+ eq(other) {
1247
+ return other instanceof RenderedMarkdownBlockWidget
1248
+ && this.markdown === other.markdown
1249
+ && this.from === other.from
1250
+ && this.renderMarkdown === other.renderMarkdown;
1251
+ }
1252
+ toDOM(view) {
1253
+ const wrapper = document.createElement("div");
1254
+ wrapper.className = "cm-me-rendered-block";
1255
+ wrapper.setAttribute("role", "button");
1256
+ wrapper.tabIndex = 0;
1257
+ wrapper.addEventListener("mousedown", (event) => {
1258
+ event.preventDefault();
1259
+ view.dispatch({
1260
+ selection: EditorSelection.cursor(this.from)
1261
+ });
1262
+ });
1263
+ wrapper.addEventListener("keydown", (event) => {
1264
+ if (event.key !== "Enter" && event.key !== " ") {
1265
+ return;
1266
+ }
1267
+ event.preventDefault();
1268
+ view.dispatch({
1269
+ selection: EditorSelection.cursor(this.from)
1270
+ });
1271
+ });
1272
+ if (!this.renderMarkdown) {
1273
+ const fallback = document.createElement("pre");
1274
+ fallback.textContent = this.markdown;
1275
+ wrapper.append(fallback);
1276
+ return wrapper;
1277
+ }
1278
+ const renderMarkdown = this.renderMarkdown;
1279
+ const controller = new AbortController();
1280
+ this.controller = controller;
1281
+ const context = {
1282
+ blockId: this.blockId,
1283
+ signal: controller.signal
1284
+ };
1285
+ Promise.resolve()
1286
+ .then(() => renderMarkdown(this.markdown, context))
1287
+ .then((result) => {
1288
+ if (!controller.signal.aborted) {
1289
+ wrapper.innerHTML = result.html;
1290
+ }
1291
+ })
1292
+ .catch((error) => {
1293
+ if (controller.signal.aborted) {
1294
+ return;
1295
+ }
1296
+ const fallback = document.createElement("pre");
1297
+ fallback.className = "cm-me-rendered-block-error";
1298
+ fallback.textContent = error instanceof Error ? error.message : String(error);
1299
+ wrapper.replaceChildren(fallback);
1300
+ });
1301
+ return wrapper;
1302
+ }
1303
+ ignoreEvent() {
1304
+ return false;
1305
+ }
1306
+ destroy() {
1307
+ this.controller?.abort();
1308
+ this.controller = undefined;
1309
+ }
1310
+ }
1311
+ function selectionFromState(state) {
1312
+ const selection = state.selection.main;
1313
+ return {
1314
+ anchor: selection.anchor,
1315
+ head: selection.head
1316
+ };
1317
+ }
1318
+ function clampSelection(selection, documentLength) {
1319
+ return {
1320
+ anchor: clampPosition(selection.anchor, documentLength),
1321
+ head: clampPosition(selection.head, documentLength)
1322
+ };
1323
+ }
1324
+ function clampPosition(position, documentLength) {
1325
+ if (!Number.isFinite(position)) {
1326
+ return 0;
1327
+ }
1328
+ return Math.min(Math.max(Math.trunc(position), 0), documentLength);
1329
+ }
1330
+ //# sourceMappingURL=index.js.map