@openmarkdown-dev/editor 0.1.1 → 0.1.2

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.
Files changed (3) hide show
  1. package/README.md +1 -1
  2. package/dist/editor.js +107 -34
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -38,7 +38,7 @@ Lossless round-trip: open a Markdown file and save it without editing, and the f
38
38
 
39
39
  That names one sentence of the claim below. For edits, the claim says exactly what changes. This is the whole claim. Every sentence is backed by a test:
40
40
 
41
- > OpenMarkdown edits your Markdown source text directly; it never rebuilds the file from a document tree. Open and save without editing, and the file is byte-for-byte what it was: line endings (LF / CRLF), BOM, final newline, whitespace, tabs, escapes and Unicode spelling untouched. When you edit, only what you touched changes, with a few exceptions, each pinned by a test: a table you edit (or click into a cell of) is re-aligned as a whole when you leave it — alignment, cell text and escapes kept; table commands rewrite that table the same way; indenting a list item or pressing Enter in a numbered list renumbers the affected items; Enter in a list item drops trailing spaces before the cursor; a file with mixed line endings gets uniform ones once you edit it.
41
+ > OpenMarkdown edits your Markdown source text directly; it never rebuilds the file from a document tree. Open and save without editing, and the file is byte-for-byte what it was: line endings (LF / CRLF), BOM, final newline, whitespace, tabs, escapes and Unicode spelling untouched. When you edit, only what you touched changes, with a few exceptions, each pinned by a test: a table you edit (or click into a cell of) is re-aligned as a whole when you leave it — alignment, cell text and escapes kept; table commands rewrite that table the same way; indenting a list item or pressing Enter in a numbered list renumbers the affected items; Enter in a list item drops trailing spaces before the cursor; a file with mixed line endings gets uniform ones once you edit it; typing on a lone blank line between two blocks adds a line break after that line, so the block below stays separate.
42
42
 
43
43
  The claim holds when your host reads and writes the file with the package's
44
44
  `openMarkdownFile` and `saveMarkdownFile`. The editor keeps the document as a string in `\n`
package/dist/editor.js CHANGED
@@ -4436,7 +4436,7 @@ function strandedIn(state) {
4436
4436
  const sel = state.selection.main;
4437
4437
  if (!sel.empty) return null;
4438
4438
  const pos = sel.head;
4439
- const block = hooks.editable(state).find((b) => b.from < pos && pos < b.to);
4439
+ const block = hooks.editable(state).find((b) => b.from < pos && pos <= b.to);
4440
4440
  if (!block || hooks.leave(state).length > 0) return null;
4441
4441
  return { block, pos };
4442
4442
  }
@@ -4462,6 +4462,9 @@ function navigateTo(view, pos) {
4462
4462
  });
4463
4463
  view.focus();
4464
4464
  }
4465
+ function isRevealed(state, range) {
4466
+ return state.facet(livePreviewConfig).revealOnSelection && state.selection.ranges.some((r) => r.from <= range.to && r.to >= (range.revealFrom ?? range.from));
4467
+ }
4465
4468
  function loadMathThenRedraw(view) {
4466
4469
  ensureMath(() => {
4467
4470
  try {
@@ -5193,26 +5196,72 @@ function mermaidReason(e) {
5193
5196
  const reason = `${lines[0]} ${lines[lines.length - 1]}`;
5194
5197
  return reason.length > 200 ? `${reason.slice(0, 199)}…` : reason;
5195
5198
  }
5196
- function requestMermaid(body, scheme, load, done) {
5199
+ function requestMermaid(body, scheme, load, done, wanted) {
5197
5200
  const key = cacheKey(body, scheme);
5198
5201
  if (cache.has(key)) return;
5199
5202
  const waiting = inflight.get(key);
5200
5203
  if (waiting) {
5201
- waiting.add(done);
5204
+ waiting.set(done, wanted);
5202
5205
  return;
5203
5206
  }
5204
- const waiters = /* @__PURE__ */ new Set([done]);
5207
+ const waiters = /* @__PURE__ */ new Map([[done, wanted]]);
5205
5208
  inflight.set(key, waiters);
5206
5209
  queue = queue.then(async () => {
5207
5210
  const mermaid = await loadMermaid(load);
5208
- inflight.delete(key);
5209
- if (!mermaid) return;
5211
+ if (!mermaid || ![...waiters.values()].some((w) => w())) {
5212
+ inflight.delete(key);
5213
+ return;
5214
+ }
5210
5215
  const result = await attemptRenderAsync(() => renderWith(mermaid, body, scheme));
5211
5216
  rememberMermaid(body, scheme, result);
5212
- for (const fn of waiters) queueMicrotask(fn);
5217
+ inflight.delete(key);
5218
+ for (const fn of waiters.keys()) queueMicrotask(fn);
5213
5219
  });
5214
5220
  }
5215
5221
  const schemeOf = (state) => state.facet(EditorView.darkTheme) ? "dark" : "light";
5222
+ function drawnMermaidBodies(state, viewport) {
5223
+ const bodies = [];
5224
+ progressiveTree(state).iterate({
5225
+ enter: (node) => {
5226
+ if (node.name !== "FencedCode") return;
5227
+ const fence = mermaidFence(state, node);
5228
+ if (fence && !isRevealed(state, fence)) {
5229
+ bodies.push({ body: fence.body, near: fence.to >= viewport.from && fence.from <= viewport.to });
5230
+ }
5231
+ return false;
5232
+ }
5233
+ });
5234
+ bodies.sort((a, b) => Number(b.near) - Number(a.near));
5235
+ return bodies.map((b) => b.body);
5236
+ }
5237
+ class MermaidRequests {
5238
+ constructor(redraw) {
5239
+ this.redraw = redraw;
5240
+ }
5241
+ redraw;
5242
+ alive = true;
5243
+ wanted = /* @__PURE__ */ new Set();
5244
+ request(state, viewport) {
5245
+ const load = state.facet(mermaidLoader);
5246
+ if (!load) return;
5247
+ const scheme = schemeOf(state);
5248
+ const bodies = drawnMermaidBodies(state, viewport);
5249
+ const wanted = new Set(bodies.map((b) => `${scheme}
5250
+ ${b}`));
5251
+ this.wanted = wanted;
5252
+ const done = () => {
5253
+ if (this.alive) this.redraw();
5254
+ };
5255
+ for (const body of bodies) {
5256
+ const key = `${scheme}
5257
+ ${body}`;
5258
+ requestMermaid(body, scheme, load, done, () => this.alive && this.wanted.has(key));
5259
+ }
5260
+ }
5261
+ destroy() {
5262
+ this.alive = false;
5263
+ }
5264
+ }
5216
5265
  const mermaidEntry = {
5217
5266
  id: "mermaid",
5218
5267
  mode: "A",
@@ -5233,38 +5282,22 @@ const mermaidEntry = {
5233
5282
  class {
5234
5283
  constructor(view) {
5235
5284
  this.view = view;
5285
+ this.requests = new MermaidRequests(() => view.dispatch({ effects: asyncRenderDone.of(null) }));
5236
5286
  this.request();
5237
5287
  }
5238
5288
  view;
5239
- alive = true;
5289
+ requests;
5240
5290
  update(u) {
5241
- if (u.docChanged || treeChanged(u) || u.startState.facet(EditorView.darkTheme) !== u.state.facet(EditorView.darkTheme)) {
5291
+ if (u.docChanged || // Which diagrams are drawn follows the selection (reveal) — design D3.
5292
+ u.selectionSet || treeChanged(u) || u.startState.facet(EditorView.darkTheme) !== u.state.facet(EditorView.darkTheme)) {
5242
5293
  this.request();
5243
5294
  }
5244
5295
  }
5245
5296
  destroy() {
5246
- this.alive = false;
5297
+ this.requests.destroy();
5247
5298
  }
5248
- /** Ask for every diagram in the document, those in view first. */
5249
5299
  request() {
5250
- const { state, viewport } = this.view;
5251
- const load = state.facet(mermaidLoader);
5252
- if (!load) return;
5253
- const scheme = schemeOf(state);
5254
- const bodies = [];
5255
- progressiveTree(state).iterate({
5256
- enter: (node) => {
5257
- if (node.name !== "FencedCode") return;
5258
- const fence = mermaidFence(state, node);
5259
- if (fence) bodies.push({ body: fence.body, near: fence.to >= viewport.from && fence.from <= viewport.to });
5260
- return false;
5261
- }
5262
- });
5263
- bodies.sort((a, b) => Number(b.near) - Number(a.near));
5264
- const redraw = () => {
5265
- if (this.alive) this.view.dispatch({ effects: asyncRenderDone.of(null) });
5266
- };
5267
- for (const { body } of bodies) requestMermaid(body, scheme, load, redraw);
5300
+ this.requests.request(this.view.state, this.view.viewport);
5268
5301
  }
5269
5302
  }
5270
5303
  )
@@ -5339,9 +5372,8 @@ function buildBlockDecorations(state) {
5339
5372
  const config = state.facet(livePreviewConfig);
5340
5373
  const deco = [];
5341
5374
  const failed = [];
5342
- const revealed = (c) => config.revealOnSelection && state.selection.ranges.some((r) => r.from <= c.to && r.to >= (c.revealFrom ?? c.from));
5343
5375
  const push2 = (entry, c) => {
5344
- if (entry.mode === "A" && revealed(c)) return;
5376
+ if (entry.mode === "A" && isRevealed(state, c)) return;
5345
5377
  deco.push(
5346
5378
  Decoration.replace({
5347
5379
  // Every block carries its selected state from here, the one exit, so no
@@ -5366,7 +5398,7 @@ function buildBlockDecorations(state) {
5366
5398
  if (!entry || !entry.range || !entry.widget) return;
5367
5399
  const range = entry.range(state, node);
5368
5400
  if (!range) return false;
5369
- if (entry.mode === "A" && revealed({ ...range })) return false;
5401
+ if (entry.mode === "A" && isRevealed(state, range)) return false;
5370
5402
  const widget = entry.widget;
5371
5403
  const out = attemptRender(() => widget(state, node, range));
5372
5404
  if (out.kind === "rendered") push2(entry, { ...range, widget: out.value });
@@ -6136,12 +6168,12 @@ const headingFoldToggle = ViewPlugin.fromClass(
6136
6168
  function itemFoldRange(state, item) {
6137
6169
  const first = state.doc.lineAt(item.from);
6138
6170
  let last = state.doc.lineAt(Math.max(item.from, item.to));
6139
- while (last.number > first.number && (isBlank(state, last.from) || item.to <= last.from)) {
6171
+ while (last.number > first.number && (isBlank$1(state, last.from) || item.to <= last.from)) {
6140
6172
  last = state.doc.line(last.number - 1);
6141
6173
  }
6142
6174
  return last.number > first.number ? { from: first.to, to: last.to } : null;
6143
6175
  }
6144
- function isBlank(state, lineFrom) {
6176
+ function isBlank$1(state, lineFrom) {
6145
6177
  const p = linePrefix(state, lineFrom);
6146
6178
  return !/\S/.test(state.sliceDoc(p.end, p.line.to));
6147
6179
  }
@@ -7021,6 +7053,43 @@ function imagePaste(docPath, getSave, getNotify = () => void 0) {
7021
7053
  }
7022
7054
  });
7023
7055
  }
7056
+ const USER_INPUT = ["input.type", "input.paste", "input.drop"];
7057
+ const isBlank = (line) => /^[ \t]*$/.test(line.text);
7058
+ function loneSeparator(state, doc, n) {
7059
+ if (n <= 1 || n >= doc.lines) return null;
7060
+ const line = doc.line(n);
7061
+ if (!isBlank(line) || isBlank(doc.line(n - 1)) || isBlank(doc.line(n + 1))) return null;
7062
+ const tree = treeUpTo(state, line.from);
7063
+ for (const side of [-1, 1]) {
7064
+ const pos = side < 0 ? line.from : line.to;
7065
+ if (tree.resolveInner(pos, side).name !== "Document") return null;
7066
+ }
7067
+ return line;
7068
+ }
7069
+ function separatorBreak(tr) {
7070
+ if (!tr.docChanged || !USER_INPUT.some((e) => tr.isUserEvent(e))) return null;
7071
+ const ranges = [];
7072
+ tr.changes.iterChanges((fromA2, toA2, _fromB, toB2) => ranges.push({ fromA: fromA2, toA: toA2, toB: toB2 }));
7073
+ if (ranges.length !== 1) return null;
7074
+ const { fromA, toA, toB } = ranges[0];
7075
+ const start = tr.startState;
7076
+ const at = start.doc.lineAt(fromA);
7077
+ if (start.doc.lineAt(toA).number !== at.number) return null;
7078
+ const sep = loneSeparator(start, start.doc, at.number);
7079
+ if (!sep) return null;
7080
+ const doc = tr.newDoc;
7081
+ const edited = doc.lineAt(toB);
7082
+ if (isBlank(edited)) return null;
7083
+ const below = doc.lineAt(tr.changes.mapPos(sep.to + 1, 1));
7084
+ if (below.number !== edited.number + 1) return null;
7085
+ return { from: sep.to, insert: start.lineBreak };
7086
+ }
7087
+ const separatorKeepsBlocks = EditorState.transactionFilter.of((tr) => {
7088
+ const add2 = separatorBreak(tr);
7089
+ if (!add2) return tr;
7090
+ const spec = { changes: add2 };
7091
+ return [tr, spec];
7092
+ });
7024
7093
  function changedRange(current, next) {
7025
7094
  if (current === next) return null;
7026
7095
  const max = Math.min(current.length, next.length);
@@ -7341,6 +7410,10 @@ function buildEditorState(options, loadMermaid2) {
7341
7410
  // caret-rules R2 / R2a / R2b) — one filter for every undo entry point.
7342
7411
  undoLanding,
7343
7412
  strandedCaret,
7413
+ // Typing on the only blank line between two blocks keeps the block below
7414
+ // separate (spec: editor-fidelity N8). Every mode: the file's structure
7415
+ // is the same whether it is rendered or not.
7416
+ separatorKeepsBlocks,
7344
7417
  drawSelection(),
7345
7418
  // Drop caret: draws a cursor at the drop position while dragging over the
7346
7419
  // editor, so a sidebar file drag (sidebar-drag-link) shows exactly where
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openmarkdown-dev/editor",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "A live-preview Markdown editor on CodeMirror 6 that edits the Markdown source itself, for a lossless round-trip: open and save without editing, and the file is byte-for-byte what it was.",
5
5
  "license": "MIT",
6
6
  "type": "module",