@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.
- package/README.md +1 -1
- package/dist/editor.js +107 -34
- 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
|
|
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.
|
|
5204
|
+
waiting.set(done, wanted);
|
|
5202
5205
|
return;
|
|
5203
5206
|
}
|
|
5204
|
-
const waiters = /* @__PURE__ */ new
|
|
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
|
-
|
|
5209
|
-
|
|
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
|
-
|
|
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
|
-
|
|
5289
|
+
requests;
|
|
5240
5290
|
update(u) {
|
|
5241
|
-
if (u.docChanged ||
|
|
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.
|
|
5297
|
+
this.requests.destroy();
|
|
5247
5298
|
}
|
|
5248
|
-
/** Ask for every diagram in the document, those in view first. */
|
|
5249
5299
|
request() {
|
|
5250
|
-
|
|
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" &&
|
|
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" &&
|
|
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.
|
|
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",
|