@openmarkdown-dev/editor 0.1.0 → 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 +2 -2
- package/dist/editor.js +167 -48
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -16,8 +16,8 @@ const editor = mountEditor(document.getElementById("editor")!, createEditorState
|
|
|
16
16
|
const bytes = saveMarkdownFile(editor.getDoc(), file); // unedited: the file's own bytes
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
+
- **Lossless round-trip:** open a Markdown file and save it without editing, and the file is byte-for-byte what it was. What an edit may rewrite is listed under [Fidelity](#fidelity), and the [fidelity page](https://openmarkdown.dev/editor/docs/fidelity/) has a check you can run yourself.
|
|
19
20
|
- **Live preview:** headings, lists, tables, math, Mermaid diagrams, callouts and YAML properties render in place; the Markdown shows under the caret.
|
|
20
|
-
- **Your file stays yours:** open and save without editing, and the file is byte-for-byte what it was.
|
|
21
21
|
- **Tables in place:** edit a cell where it is drawn.
|
|
22
22
|
- **Images:** paste or drop them in, drag a corner to resize.
|
|
23
23
|
- **Framework-free:** plain TypeScript and DOM. Works with React, Vue, Svelte or nothing. Not build-free: with no bundler, [self-host it](https://github.com/OpenMarkdown-dev/OpenMarkdown-editor/blob/main/docs/guides/self-hosting.md).
|
|
@@ -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
|
@@ -1377,6 +1377,16 @@ function tableArrowNav(p, row, col, dir) {
|
|
|
1377
1377
|
function flattenCellInsert(text) {
|
|
1378
1378
|
return text.replace(/\r?\n/g, " ");
|
|
1379
1379
|
}
|
|
1380
|
+
function tableCellInsert(text, before) {
|
|
1381
|
+
let run = /\\*$/.exec(before)[0].length;
|
|
1382
|
+
let out = "";
|
|
1383
|
+
for (const ch of flattenCellInsert(text)) {
|
|
1384
|
+
if (ch === "|" && run % 2 === 0) out += "\\";
|
|
1385
|
+
out += ch;
|
|
1386
|
+
run = ch === "\\" ? run + 1 : 0;
|
|
1387
|
+
}
|
|
1388
|
+
return out;
|
|
1389
|
+
}
|
|
1380
1390
|
function tableRangeAt(state, pos) {
|
|
1381
1391
|
for (const bias of [-1, 1]) {
|
|
1382
1392
|
let node = treeUpTo(state, pos).resolveInner(pos, bias);
|
|
@@ -3930,18 +3940,7 @@ function buildPartEditor(outer, text, hooks, opts = {}) {
|
|
|
3930
3940
|
}
|
|
3931
3941
|
})
|
|
3932
3942
|
),
|
|
3933
|
-
opts.
|
|
3934
|
-
let broken = false;
|
|
3935
|
-
tr.changes.iterChanges((_a, _b, _c, _d, inserted) => {
|
|
3936
|
-
if (/\r?\n/.test(inserted.toString())) broken = true;
|
|
3937
|
-
});
|
|
3938
|
-
if (!broken) return tr;
|
|
3939
|
-
const changes = [];
|
|
3940
|
-
tr.changes.iterChanges((fromA, toA, _b, _c, inserted) => {
|
|
3941
|
-
changes.push({ from: fromA, to: toA, insert: flattenCellInsert(inserted.toString()) });
|
|
3942
|
-
});
|
|
3943
|
-
return { changes, selection: tr.selection, userEvent: tr.annotation(Transaction.userEvent) };
|
|
3944
|
-
}) : [],
|
|
3943
|
+
opts.accept ? acceptFilter(opts.accept) : [],
|
|
3945
3944
|
EditorView.updateListener.of((u) => {
|
|
3946
3945
|
if (!u.docChanged) return;
|
|
3947
3946
|
if (u.transactions.some((t) => t.annotation(reseed))) return;
|
|
@@ -3966,7 +3965,7 @@ function buildPartEditor(outer, text, hooks, opts = {}) {
|
|
|
3966
3965
|
})
|
|
3967
3966
|
});
|
|
3968
3967
|
parts.set(host, view);
|
|
3969
|
-
|
|
3968
|
+
queueMicrotask(() => {
|
|
3970
3969
|
if (!host.isConnected) return;
|
|
3971
3970
|
const selection = entrySelection(opts.caret, view.state.doc.length, pointerPlaced);
|
|
3972
3971
|
if (!selection) return;
|
|
@@ -3993,6 +3992,29 @@ function reseedPart(view, text) {
|
|
|
3993
3992
|
annotations: reseed.of(true)
|
|
3994
3993
|
});
|
|
3995
3994
|
}
|
|
3995
|
+
function acceptFilter(accept) {
|
|
3996
|
+
return EditorState.transactionFilter.of((tr) => {
|
|
3997
|
+
if (!tr.docChanged) return tr;
|
|
3998
|
+
const changes = [];
|
|
3999
|
+
let rewritten = false;
|
|
4000
|
+
tr.changes.iterChanges((fromA, toA, _b, _c, inserted) => {
|
|
4001
|
+
const text = inserted.toString();
|
|
4002
|
+
const ok = accept(text, tr.startState.sliceDoc(0, fromA));
|
|
4003
|
+
if (ok !== text) rewritten = true;
|
|
4004
|
+
changes.push({ from: fromA, to: toA, insert: ok });
|
|
4005
|
+
});
|
|
4006
|
+
if (!rewritten) return tr;
|
|
4007
|
+
const next = tr.startState.changes(changes);
|
|
4008
|
+
const back = tr.changes.invert(tr.startState.doc);
|
|
4009
|
+
const selection = tr.selection ? EditorSelection.create(
|
|
4010
|
+
tr.selection.ranges.map(
|
|
4011
|
+
(r) => EditorSelection.range(next.mapPos(back.mapPos(r.anchor, -1), 1), next.mapPos(back.mapPos(r.head, -1), 1))
|
|
4012
|
+
),
|
|
4013
|
+
tr.selection.mainIndex
|
|
4014
|
+
) : void 0;
|
|
4015
|
+
return { changes: next, selection, userEvent: tr.annotation(Transaction.userEvent) };
|
|
4016
|
+
});
|
|
4017
|
+
}
|
|
3996
4018
|
function headText(doc) {
|
|
3997
4019
|
const n = Math.min(doc.lines, 100);
|
|
3998
4020
|
return doc.sliceString(0, doc.line(n).to);
|
|
@@ -4198,7 +4220,7 @@ class PropertiesWidget extends WidgetType {
|
|
|
4198
4220
|
},
|
|
4199
4221
|
onKey: (e) => this.onKey(e, view, index, part)
|
|
4200
4222
|
},
|
|
4201
|
-
{
|
|
4223
|
+
{ accept: flattenCellInsert, caret: view.state.field(activePropField, false)?.caret }
|
|
4202
4224
|
);
|
|
4203
4225
|
return host;
|
|
4204
4226
|
}
|
|
@@ -4408,6 +4430,29 @@ const undoLanding = ViewPlugin.define((view) => ({
|
|
|
4408
4430
|
if (land) queueMicrotask(() => settleLanding(view, u.state, land));
|
|
4409
4431
|
}
|
|
4410
4432
|
}));
|
|
4433
|
+
function strandedIn(state) {
|
|
4434
|
+
const hooks = state.facet(blockCaret);
|
|
4435
|
+
if (!hooks) return null;
|
|
4436
|
+
const sel = state.selection.main;
|
|
4437
|
+
if (!sel.empty) return null;
|
|
4438
|
+
const pos = sel.head;
|
|
4439
|
+
const block = hooks.editable(state).find((b) => b.from < pos && pos <= b.to);
|
|
4440
|
+
if (!block || hooks.leave(state).length > 0) return null;
|
|
4441
|
+
return { block, pos };
|
|
4442
|
+
}
|
|
4443
|
+
const strandedCaret = ViewPlugin.define((view) => ({
|
|
4444
|
+
update(u) {
|
|
4445
|
+
if (!u.docChanged && !u.selectionSet) return;
|
|
4446
|
+
if (u.transactions.some((tr) => tr.isUserEvent("undo") || tr.isUserEvent("redo"))) return;
|
|
4447
|
+
if (!strandedIn(u.state)) return;
|
|
4448
|
+
queueMicrotask(() => {
|
|
4449
|
+
if (!view.dom.isConnected || view.state !== u.state || view.composing) return;
|
|
4450
|
+
const hit = strandedIn(view.state);
|
|
4451
|
+
const hooks = view.state.facet(blockCaret);
|
|
4452
|
+
if (hit && hooks) hooks.enterAt(view, hit.block, hit.pos);
|
|
4453
|
+
});
|
|
4454
|
+
}
|
|
4455
|
+
}));
|
|
4411
4456
|
function navigateTo(view, pos) {
|
|
4412
4457
|
const line = view.state.doc.lineAt(Math.max(0, Math.min(pos, view.state.doc.length)));
|
|
4413
4458
|
view.dispatch({
|
|
@@ -4417,6 +4462,9 @@ function navigateTo(view, pos) {
|
|
|
4417
4462
|
});
|
|
4418
4463
|
view.focus();
|
|
4419
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
|
+
}
|
|
4420
4468
|
function loadMathThenRedraw(view) {
|
|
4421
4469
|
ensureMath(() => {
|
|
4422
4470
|
try {
|
|
@@ -4876,7 +4924,7 @@ class TableWidget extends WidgetType {
|
|
|
4876
4924
|
},
|
|
4877
4925
|
onKey: (e, part) => this.onCellKey(e, view, part, row, col)
|
|
4878
4926
|
},
|
|
4879
|
-
{
|
|
4927
|
+
{ accept: tableCellInsert, caret }
|
|
4880
4928
|
);
|
|
4881
4929
|
if (align && align !== "none") host.style.textAlign = align;
|
|
4882
4930
|
return host;
|
|
@@ -5148,26 +5196,72 @@ function mermaidReason(e) {
|
|
|
5148
5196
|
const reason = `${lines[0]} ${lines[lines.length - 1]}`;
|
|
5149
5197
|
return reason.length > 200 ? `${reason.slice(0, 199)}…` : reason;
|
|
5150
5198
|
}
|
|
5151
|
-
function requestMermaid(body, scheme, load, done) {
|
|
5199
|
+
function requestMermaid(body, scheme, load, done, wanted) {
|
|
5152
5200
|
const key = cacheKey(body, scheme);
|
|
5153
5201
|
if (cache.has(key)) return;
|
|
5154
5202
|
const waiting = inflight.get(key);
|
|
5155
5203
|
if (waiting) {
|
|
5156
|
-
waiting.
|
|
5204
|
+
waiting.set(done, wanted);
|
|
5157
5205
|
return;
|
|
5158
5206
|
}
|
|
5159
|
-
const waiters = /* @__PURE__ */ new
|
|
5207
|
+
const waiters = /* @__PURE__ */ new Map([[done, wanted]]);
|
|
5160
5208
|
inflight.set(key, waiters);
|
|
5161
5209
|
queue = queue.then(async () => {
|
|
5162
5210
|
const mermaid = await loadMermaid(load);
|
|
5163
|
-
|
|
5164
|
-
|
|
5211
|
+
if (!mermaid || ![...waiters.values()].some((w) => w())) {
|
|
5212
|
+
inflight.delete(key);
|
|
5213
|
+
return;
|
|
5214
|
+
}
|
|
5165
5215
|
const result = await attemptRenderAsync(() => renderWith(mermaid, body, scheme));
|
|
5166
5216
|
rememberMermaid(body, scheme, result);
|
|
5167
|
-
|
|
5217
|
+
inflight.delete(key);
|
|
5218
|
+
for (const fn of waiters.keys()) queueMicrotask(fn);
|
|
5168
5219
|
});
|
|
5169
5220
|
}
|
|
5170
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
|
+
}
|
|
5171
5265
|
const mermaidEntry = {
|
|
5172
5266
|
id: "mermaid",
|
|
5173
5267
|
mode: "A",
|
|
@@ -5188,38 +5282,22 @@ const mermaidEntry = {
|
|
|
5188
5282
|
class {
|
|
5189
5283
|
constructor(view) {
|
|
5190
5284
|
this.view = view;
|
|
5285
|
+
this.requests = new MermaidRequests(() => view.dispatch({ effects: asyncRenderDone.of(null) }));
|
|
5191
5286
|
this.request();
|
|
5192
5287
|
}
|
|
5193
5288
|
view;
|
|
5194
|
-
|
|
5289
|
+
requests;
|
|
5195
5290
|
update(u) {
|
|
5196
|
-
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)) {
|
|
5197
5293
|
this.request();
|
|
5198
5294
|
}
|
|
5199
5295
|
}
|
|
5200
5296
|
destroy() {
|
|
5201
|
-
this.
|
|
5297
|
+
this.requests.destroy();
|
|
5202
5298
|
}
|
|
5203
|
-
/** Ask for every diagram in the document, those in view first. */
|
|
5204
5299
|
request() {
|
|
5205
|
-
|
|
5206
|
-
const load = state.facet(mermaidLoader);
|
|
5207
|
-
if (!load) return;
|
|
5208
|
-
const scheme = schemeOf(state);
|
|
5209
|
-
const bodies = [];
|
|
5210
|
-
progressiveTree(state).iterate({
|
|
5211
|
-
enter: (node) => {
|
|
5212
|
-
if (node.name !== "FencedCode") return;
|
|
5213
|
-
const fence = mermaidFence(state, node);
|
|
5214
|
-
if (fence) bodies.push({ body: fence.body, near: fence.to >= viewport.from && fence.from <= viewport.to });
|
|
5215
|
-
return false;
|
|
5216
|
-
}
|
|
5217
|
-
});
|
|
5218
|
-
bodies.sort((a, b) => Number(b.near) - Number(a.near));
|
|
5219
|
-
const redraw = () => {
|
|
5220
|
-
if (this.alive) this.view.dispatch({ effects: asyncRenderDone.of(null) });
|
|
5221
|
-
};
|
|
5222
|
-
for (const { body } of bodies) requestMermaid(body, scheme, load, redraw);
|
|
5300
|
+
this.requests.request(this.view.state, this.view.viewport);
|
|
5223
5301
|
}
|
|
5224
5302
|
}
|
|
5225
5303
|
)
|
|
@@ -5294,9 +5372,8 @@ function buildBlockDecorations(state) {
|
|
|
5294
5372
|
const config = state.facet(livePreviewConfig);
|
|
5295
5373
|
const deco = [];
|
|
5296
5374
|
const failed = [];
|
|
5297
|
-
const revealed = (c) => config.revealOnSelection && state.selection.ranges.some((r) => r.from <= c.to && r.to >= (c.revealFrom ?? c.from));
|
|
5298
5375
|
const push2 = (entry, c) => {
|
|
5299
|
-
if (entry.mode === "A" &&
|
|
5376
|
+
if (entry.mode === "A" && isRevealed(state, c)) return;
|
|
5300
5377
|
deco.push(
|
|
5301
5378
|
Decoration.replace({
|
|
5302
5379
|
// Every block carries its selected state from here, the one exit, so no
|
|
@@ -5321,7 +5398,7 @@ function buildBlockDecorations(state) {
|
|
|
5321
5398
|
if (!entry || !entry.range || !entry.widget) return;
|
|
5322
5399
|
const range = entry.range(state, node);
|
|
5323
5400
|
if (!range) return false;
|
|
5324
|
-
if (entry.mode === "A" &&
|
|
5401
|
+
if (entry.mode === "A" && isRevealed(state, range)) return false;
|
|
5325
5402
|
const widget = entry.widget;
|
|
5326
5403
|
const out = attemptRender(() => widget(state, node, range));
|
|
5327
5404
|
if (out.kind === "rendered") push2(entry, { ...range, widget: out.value });
|
|
@@ -6091,12 +6168,12 @@ const headingFoldToggle = ViewPlugin.fromClass(
|
|
|
6091
6168
|
function itemFoldRange(state, item) {
|
|
6092
6169
|
const first = state.doc.lineAt(item.from);
|
|
6093
6170
|
let last = state.doc.lineAt(Math.max(item.from, item.to));
|
|
6094
|
-
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)) {
|
|
6095
6172
|
last = state.doc.line(last.number - 1);
|
|
6096
6173
|
}
|
|
6097
6174
|
return last.number > first.number ? { from: first.to, to: last.to } : null;
|
|
6098
6175
|
}
|
|
6099
|
-
function isBlank(state, lineFrom) {
|
|
6176
|
+
function isBlank$1(state, lineFrom) {
|
|
6100
6177
|
const p = linePrefix(state, lineFrom);
|
|
6101
6178
|
return !/\S/.test(state.sliceDoc(p.end, p.line.to));
|
|
6102
6179
|
}
|
|
@@ -6976,6 +7053,43 @@ function imagePaste(docPath, getSave, getNotify = () => void 0) {
|
|
|
6976
7053
|
}
|
|
6977
7054
|
});
|
|
6978
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
|
+
});
|
|
6979
7093
|
function changedRange(current, next) {
|
|
6980
7094
|
if (current === next) return null;
|
|
6981
7095
|
const max = Math.min(current.length, next.length);
|
|
@@ -7295,6 +7409,11 @@ function buildEditorState(options, loadMermaid2) {
|
|
|
7295
7409
|
// Undo / redo land at the change, not at history's old caret (spec:
|
|
7296
7410
|
// caret-rules R2 / R2a / R2b) — one filter for every undo entry point.
|
|
7297
7411
|
undoLanding,
|
|
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,
|
|
7298
7417
|
drawSelection(),
|
|
7299
7418
|
// Drop caret: draws a cursor at the drop position while dragging over the
|
|
7300
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",
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
"markdown-editor",
|
|
34
34
|
"editor",
|
|
35
35
|
"codemirror",
|
|
36
|
+
"codemirror-6",
|
|
36
37
|
"live-preview",
|
|
37
38
|
"wysiwyg",
|
|
38
39
|
"embeddable",
|