markupeditor 0.9.52 → 0.9.54
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/markup-editor.js +184 -75
- package/package.json +1 -1
package/dist/markup-editor.js
CHANGED
|
@@ -621,6 +621,40 @@ class MUError {
|
|
|
621
621
|
|
|
622
622
|
}
|
|
623
623
|
|
|
624
|
+
/**
|
|
625
|
+
* Return the slug for a heading's `text` the way GitHub makes it: lowercase, keep alphabetic
|
|
626
|
+
* characters, combining marks, decimal digits, connector punctuation (which includes `_`) and `-`,
|
|
627
|
+
* and replace blanks with `-`. The slug is cut to 40 characters, counting characters rather than
|
|
628
|
+
* UTF-16 units, just to avoid unwieldy IDs.
|
|
629
|
+
*
|
|
630
|
+
* @param {string} text The text content of a heading
|
|
631
|
+
* @returns {string} The slug, which is empty if nothing in `text` is kept
|
|
632
|
+
*/
|
|
633
|
+
function headingSlug(text) {
|
|
634
|
+
const slug = text.toLowerCase()
|
|
635
|
+
.replace(/[^\p{Alphabetic}\p{M}\p{Nd}\p{Pc} -]/gu, '')
|
|
636
|
+
.replaceAll(' ', '-');
|
|
637
|
+
return Array.from(slug).slice(0, 40).join('')
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
/**
|
|
641
|
+
* Return the slugs for the headings with the `texts` given in document order, numbering repeats
|
|
642
|
+
* the way GitHub does: the first heading with a slug gets it as-is, and later ones get `-1`, `-2`,
|
|
643
|
+
* and so on. Every heading counts, whether or not anything links to it.
|
|
644
|
+
*
|
|
645
|
+
* @param {string[]} texts The text content of every heading, in document order
|
|
646
|
+
* @returns {string[]} The slug for each of `texts`, in the same order
|
|
647
|
+
*/
|
|
648
|
+
function headingSlugs(texts) {
|
|
649
|
+
const seen = new Map();
|
|
650
|
+
return texts.map(text => {
|
|
651
|
+
const slug = headingSlug(text);
|
|
652
|
+
const count = seen.get(slug) ?? 0;
|
|
653
|
+
seen.set(slug, count + 1);
|
|
654
|
+
return count === 0 ? slug : slug + '-' + count.toString()
|
|
655
|
+
})
|
|
656
|
+
}
|
|
657
|
+
|
|
624
658
|
// ::- Persistent data structure representing an ordered mapping from
|
|
625
659
|
// strings to values, with some convenient update methods.
|
|
626
660
|
function OrderedMap(content) {
|
|
@@ -35485,18 +35519,31 @@ function _getMarkTypes() {
|
|
|
35485
35519
|
*/
|
|
35486
35520
|
function getLinkAttributes() {
|
|
35487
35521
|
const view = activeView();
|
|
35488
|
-
const
|
|
35489
|
-
|
|
35490
|
-
|
|
35491
|
-
if (
|
|
35492
|
-
}
|
|
35493
|
-
const
|
|
35494
|
-
if (
|
|
35495
|
-
const linkMarks =
|
|
35522
|
+
const link = linkRangeAt(view.state);
|
|
35523
|
+
if (link) {
|
|
35524
|
+
const text = view.state.doc.textBetween(link.from, link.to);
|
|
35525
|
+
if (text) return {href: link.mark.attrs.href, link: text};
|
|
35526
|
+
} // A linked image carries the link mark on the image node, which has no text to report as the link.
|
|
35527
|
+
const image = _selectedImageNode(view.state);
|
|
35528
|
+
if (image) {
|
|
35529
|
+
const linkMarks = image.marks.filter(mark => mark.type === schema.marks.link);
|
|
35496
35530
|
if (linkMarks.length === 1) {
|
|
35497
|
-
return {href: linkMarks[0].attrs.href, link:
|
|
35531
|
+
return {href: linkMarks[0].attrs.href, link: image.attrs.alt ?? ''};
|
|
35498
35532
|
} } return {};
|
|
35499
35533
|
}
|
|
35534
|
+
function _selectedImageNode(state) {
|
|
35535
|
+
const {from, to} = state.selection;
|
|
35536
|
+
let image = null;
|
|
35537
|
+
let count = 0;
|
|
35538
|
+
state.doc.nodesBetween(from, to, node => {
|
|
35539
|
+
if (node.type === state.schema.nodes.image) {
|
|
35540
|
+
image = node;
|
|
35541
|
+
count++;
|
|
35542
|
+
}
|
|
35543
|
+
});
|
|
35544
|
+
return (count === 1) ? image : null
|
|
35545
|
+
}
|
|
35546
|
+
|
|
35500
35547
|
function _getImageAttributes() {
|
|
35501
35548
|
const view = activeView();
|
|
35502
35549
|
return getImageAttributes(view.state)
|
|
@@ -36129,6 +36176,9 @@ function testCodeLanguageTabInfo() {
|
|
|
36129
36176
|
* Insert a link to url. When the selection is collapsed, the url is inserted
|
|
36130
36177
|
* at the selection point as a link.
|
|
36131
36178
|
*
|
|
36179
|
+
* When the selection is in an existing link, or contains one, the href of the whole link is
|
|
36180
|
+
* changed to url and its text is left alone.
|
|
36181
|
+
*
|
|
36132
36182
|
* When done, leave the link selected.
|
|
36133
36183
|
*
|
|
36134
36184
|
* @param {string} url The url/href to use for the link
|
|
@@ -36143,25 +36193,94 @@ function insertLinkCommand(url) {
|
|
|
36143
36193
|
const commandAdapter = (state, dispatch, view) => {
|
|
36144
36194
|
const selection = state.selection;
|
|
36145
36195
|
const linkMark = state.schema.marks.link.create({ href: url });
|
|
36146
|
-
|
|
36196
|
+
const transaction = state.tr;
|
|
36197
|
+
if (!addLinkMark(transaction, state, linkMark)) {
|
|
36198
|
+
// A collapsed selection outside any link
|
|
36147
36199
|
const textNode = state.schema.text(url).mark([linkMark]);
|
|
36148
|
-
|
|
36200
|
+
transaction.replaceSelectionWith(textNode, false);
|
|
36149
36201
|
const linkSelection = TextSelection.create(transaction.doc, selection.from, selection.from + textNode.nodeSize);
|
|
36150
36202
|
transaction.setSelection(linkSelection);
|
|
36151
|
-
dispatch(transaction);
|
|
36152
|
-
stateChanged(view);
|
|
36153
|
-
} else {
|
|
36154
|
-
const toggle = toggleMark(linkMark.type, linkMark.attrs);
|
|
36155
|
-
if (toggle) {
|
|
36156
|
-
toggle(state, dispatch);
|
|
36157
|
-
stateChanged(view);
|
|
36158
|
-
}
|
|
36159
36203
|
}
|
|
36204
|
+
dispatch(transaction);
|
|
36205
|
+
stateChanged(view);
|
|
36160
36206
|
return true;
|
|
36161
36207
|
};
|
|
36162
36208
|
return commandAdapter;
|
|
36163
36209
|
}
|
|
36164
36210
|
|
|
36211
|
+
/**
|
|
36212
|
+
* Add `linkMark` to `transaction`, replacing the href of the link the selection is in if there is one.
|
|
36213
|
+
*
|
|
36214
|
+
* The selection can be anywhere within a link or contain the full link. A link that spans several
|
|
36215
|
+
* text nodes, because part of it has other marks, is changed as a whole.
|
|
36216
|
+
*
|
|
36217
|
+
* @ignore
|
|
36218
|
+
* @param {Transaction} transaction
|
|
36219
|
+
* @param {EditorState} state The state `transaction` was created from
|
|
36220
|
+
* @param {Mark} linkMark
|
|
36221
|
+
* @returns {boolean} False, with `transaction` unchanged, when the selection is collapsed and not in a link
|
|
36222
|
+
*/
|
|
36223
|
+
function addLinkMark(transaction, state, linkMark) {
|
|
36224
|
+
const range = linkRangeAt(state);
|
|
36225
|
+
if (range) {
|
|
36226
|
+
transaction.addMark(range.from, range.to, linkMark);
|
|
36227
|
+
const {from, to} = state.selection;
|
|
36228
|
+
if (range.from !== from || range.to !== to) {
|
|
36229
|
+
transaction.setSelection(TextSelection.create(transaction.doc, range.from, range.to));
|
|
36230
|
+
} return true;
|
|
36231
|
+
} if (state.selection.empty) return false;
|
|
36232
|
+
state.selection.ranges.forEach(range => transaction.addMark(range.$from.pos, range.$to.pos, linkMark));
|
|
36233
|
+
return true;
|
|
36234
|
+
}
|
|
36235
|
+
|
|
36236
|
+
/**
|
|
36237
|
+
* The extent of the link the selection is in, or null if it is not in one.
|
|
36238
|
+
*
|
|
36239
|
+
* Every inline node in the selection has to carry the same link, and a collapsed selection has to be inside the
|
|
36240
|
+
* link, not at its edge.
|
|
36241
|
+
*
|
|
36242
|
+
* @ignore
|
|
36243
|
+
* @param {EditorState} state
|
|
36244
|
+
* @returns {{from: number, to: number, mark: Mark}|null}
|
|
36245
|
+
*/
|
|
36246
|
+
function linkRangeAt(state) {
|
|
36247
|
+
const linkType = state.schema.marks.link;
|
|
36248
|
+
const {from, to, $from, $to} = state.selection;
|
|
36249
|
+
if (!$from.sameParent($to)) return null;
|
|
36250
|
+
let target = null;
|
|
36251
|
+
let uniform = true;
|
|
36252
|
+
const check = node => {
|
|
36253
|
+
const mark = linkType.isInSet(node.marks);
|
|
36254
|
+
if (!mark || (target && !target.eq(mark))) uniform = false;
|
|
36255
|
+
else target = mark;
|
|
36256
|
+
};
|
|
36257
|
+
if (state.selection.empty) {
|
|
36258
|
+
// A collapsed selection is in a link when the same link is on both sides of it, including between its text nodes
|
|
36259
|
+
if (!$from.nodeBefore || !$from.nodeAfter) return null;
|
|
36260
|
+
check($from.nodeBefore);
|
|
36261
|
+
check($from.nodeAfter);
|
|
36262
|
+
} else {
|
|
36263
|
+
state.doc.nodesBetween(from, to, node => {
|
|
36264
|
+
if (!node.isInline) return true;
|
|
36265
|
+
check(node);
|
|
36266
|
+
return false;
|
|
36267
|
+
});
|
|
36268
|
+
} if (!uniform || !target) return null;
|
|
36269
|
+
// Widen to the contiguous children carrying the same link
|
|
36270
|
+
const base = $from.start();
|
|
36271
|
+
const spans = [];
|
|
36272
|
+
$from.parent.forEach((child, offset) => {
|
|
36273
|
+
spans.push({start: base + offset, end: base + offset + child.nodeSize, linked: !!target.isInSet(child.marks)});
|
|
36274
|
+
});
|
|
36275
|
+
const collapsed = state.selection.empty;
|
|
36276
|
+
let first = spans.findIndex(span => collapsed ? span.end >= from : span.end > from);
|
|
36277
|
+
let last = spans.findLastIndex(span => collapsed ? span.start <= to : span.start < to);
|
|
36278
|
+
if (first < 0 || last < first) return null;
|
|
36279
|
+
while (first > 0 && spans[first - 1].linked) first--;
|
|
36280
|
+
while (last < spans.length - 1 && spans[last + 1].linked) last++;
|
|
36281
|
+
return {from: spans[first].start, to: spans[last].end, mark: target};
|
|
36282
|
+
}
|
|
36283
|
+
|
|
36165
36284
|
/**
|
|
36166
36285
|
* Insert a link to the header identified by hTag ('H1'-'H6') and index (its position among
|
|
36167
36286
|
* headers with that tag), assigning the header a unique id if it doesn't already have one.
|
|
@@ -36184,27 +36303,16 @@ function insertInternalLinkCommand(hTag, index) {
|
|
|
36184
36303
|
let id = idForHeader(node, state);
|
|
36185
36304
|
// Same transaction as the mark insertion below, so both land as one undo step.
|
|
36186
36305
|
let tr = state.tr.setNodeMarkup(pos, undefined, { ...node.attrs, id });
|
|
36187
|
-
const selection = state.selection;
|
|
36188
36306
|
const linkMark = state.schema.marks.link.create({ href: '#' + id });
|
|
36189
|
-
if (
|
|
36307
|
+
if (!addLinkMark(tr, state, linkMark)) {
|
|
36190
36308
|
// In case of an empty selection, insert the textContent of the header and then use
|
|
36191
36309
|
// that to link-to the header
|
|
36192
36310
|
const textNode = state.schema.text(node.textContent, [linkMark]);
|
|
36193
36311
|
tr.replaceSelectionWith(textNode, false);
|
|
36194
|
-
dispatch(tr);
|
|
36195
|
-
stateChanged(view);
|
|
36196
|
-
return true;
|
|
36197
|
-
} else {
|
|
36198
|
-
// Capture toggleMark's steps rather than let it dispatch, then replay them onto
|
|
36199
|
-
// `tr` (setNodeMarkup doesn't shift positions, so they apply cleanly).
|
|
36200
|
-
let captured = null;
|
|
36201
|
-
toggleMark(linkMark.type, linkMark.attrs)(state, t => { captured = t; });
|
|
36202
|
-
if (!captured) return false;
|
|
36203
|
-
for (const step of captured.steps) tr.step(step);
|
|
36204
|
-
dispatch(tr);
|
|
36205
|
-
stateChanged(view);
|
|
36206
|
-
return true;
|
|
36207
36312
|
}
|
|
36313
|
+
dispatch(tr);
|
|
36314
|
+
stateChanged(view);
|
|
36315
|
+
return true;
|
|
36208
36316
|
};
|
|
36209
36317
|
return commandAdapter;
|
|
36210
36318
|
}
|
|
@@ -36228,12 +36336,10 @@ function idForInternalLinkCommand(hTag, index) {
|
|
|
36228
36336
|
}
|
|
36229
36337
|
|
|
36230
36338
|
/**
|
|
36231
|
-
* Return a unique identifier for the heading `node` by
|
|
36232
|
-
*
|
|
36233
|
-
* If the heading `node` has an id, then just
|
|
36234
|
-
*
|
|
36235
|
-
* Since the `node.textContent` can be arbitrarily large, we limit the id to 40 characters
|
|
36236
|
-
* just to avoid unwieldy IDs.
|
|
36339
|
+
* Return a unique identifier for the heading `node`: its slug as `headingSlugs` numbers it by its
|
|
36340
|
+
* position among all the headings in `state.doc`, as GitHub does. If another node already uses that
|
|
36341
|
+
* id, append `-1`, `-2`, and so on until it is unique. If the heading `node` has an id, then just
|
|
36342
|
+
* return it.
|
|
36237
36343
|
*
|
|
36238
36344
|
* @ignore
|
|
36239
36345
|
* @param {Node} node A ProseMirror Node that is of heading type
|
|
@@ -36242,15 +36348,20 @@ function idForInternalLinkCommand(hTag, index) {
|
|
|
36242
36348
|
*/
|
|
36243
36349
|
function idForHeader(node, state) {
|
|
36244
36350
|
if (node.attrs.id) return node.attrs.id
|
|
36245
|
-
|
|
36246
|
-
|
|
36247
|
-
|
|
36351
|
+
const texts = [];
|
|
36352
|
+
let position = -1;
|
|
36353
|
+
state.doc.descendants(child => {
|
|
36354
|
+
if (child.type !== state.schema.nodes.heading) return true
|
|
36355
|
+
if (child === node) position = texts.length;
|
|
36356
|
+
texts.push(child.textContent);
|
|
36357
|
+
return false
|
|
36358
|
+
});
|
|
36359
|
+
const base = (position >= 0) ? headingSlugs(texts)[position] : headingSlug(node.textContent);
|
|
36360
|
+
let id = base;
|
|
36248
36361
|
let index = 0;
|
|
36249
|
-
while (
|
|
36362
|
+
while (nodeWithId(id, state).node) {
|
|
36250
36363
|
index++;
|
|
36251
|
-
id =
|
|
36252
|
-
let {node} = nodeWithId(id, state);
|
|
36253
|
-
idNode = node;
|
|
36364
|
+
id = base + '-' + index.toString();
|
|
36254
36365
|
}
|
|
36255
36366
|
return id
|
|
36256
36367
|
}
|
|
@@ -36289,20 +36400,25 @@ function headerMatching(hTag, index, state) {
|
|
|
36289
36400
|
|
|
36290
36401
|
/**
|
|
36291
36402
|
* Return a JSON-stringified array describing every non-empty header in the document, in document
|
|
36292
|
-
* order within each level. Each entry is `{hTag, index, text, id}`, where `hTag` and `index` identify
|
|
36403
|
+
* order within each level. Each entry is `{hTag, index, text, id, slug}`, where `hTag` and `index` identify
|
|
36293
36404
|
* the header the same way `insertInternalLink(hTag, index)` and `idForInternalLinkCommand(hTag, index)`
|
|
36294
|
-
* do,
|
|
36405
|
+
* do, `id` is the header's current id attribute (null if it hasn't been assigned one yet), and `slug`
|
|
36406
|
+
* is the id a link to the header would give it, numbered by position among all the headers.
|
|
36295
36407
|
*
|
|
36296
36408
|
* An empty header is omitted -- it can't form a usable id, so there's nothing to link to -- but
|
|
36297
36409
|
* `index` still reflects its true position among ALL headers at that level (empty ones included),
|
|
36298
36410
|
* matching what `headerMatching` expects; it's only the reported entries that skip empty headers,
|
|
36299
36411
|
* not the numbering.
|
|
36300
36412
|
*
|
|
36301
|
-
* @returns {string} JSON-stringified array of `{hTag, index, text, id}`
|
|
36413
|
+
* @returns {string} JSON-stringified array of `{hTag, index, text, id, slug}`
|
|
36302
36414
|
*/
|
|
36303
36415
|
function getHeaders() {
|
|
36304
36416
|
const view = activeView();
|
|
36305
36417
|
const headersByLevel = headers(view.state);
|
|
36418
|
+
// Slugs are numbered by position among all the headings in the document
|
|
36419
|
+
const slugs = new Map();
|
|
36420
|
+
const all = Object.values(headersByLevel).flat().sort((a, b) => a.pos - b.pos);
|
|
36421
|
+
headingSlugs(all.map(h => h.node.textContent)).forEach((slug, i) => slugs.set(all[i].node, slug));
|
|
36306
36422
|
let result = [];
|
|
36307
36423
|
for (let level = 1; level < 7; level++) {
|
|
36308
36424
|
let hNodes = headersByLevel[level];
|
|
@@ -36310,7 +36426,7 @@ function getHeaders() {
|
|
|
36310
36426
|
hNodes.forEach((h, index) => {
|
|
36311
36427
|
const text = h.node.textContent;
|
|
36312
36428
|
if (text.trim().length === 0) return
|
|
36313
|
-
result.push({hTag: 'H' + level.toString(), index: index, text: text, id: h.node.attrs.id ?? null});
|
|
36429
|
+
result.push({hTag: 'H' + level.toString(), index: index, text: text, id: h.node.attrs.id ?? null, slug: slugs.get(h.node)});
|
|
36314
36430
|
});
|
|
36315
36431
|
}
|
|
36316
36432
|
}
|
|
@@ -36384,29 +36500,14 @@ function deleteLinkCommand() {
|
|
|
36384
36500
|
}
|
|
36385
36501
|
|
|
36386
36502
|
function selectFullLink(view) {
|
|
36387
|
-
|
|
36388
|
-
|
|
36389
|
-
|
|
36390
|
-
|
|
36391
|
-
const
|
|
36392
|
-
|
|
36393
|
-
|
|
36394
|
-
|
|
36395
|
-
return false;
|
|
36396
|
-
} return true;
|
|
36397
|
-
});
|
|
36398
|
-
if (nodePos.length !== 1) return;
|
|
36399
|
-
const selectedNode = nodePos[0].node;
|
|
36400
|
-
const selectedPos = nodePos[0].pos;
|
|
36401
|
-
const linkMarks = selectedNode && selectedNode.marks.filter(mark => mark.type === linkType);
|
|
36402
|
-
if (linkMarks.length !== 1) return;
|
|
36403
|
-
|
|
36404
|
-
// Select the entire text of selectedNode
|
|
36405
|
-
const anchor = selectedPos;
|
|
36406
|
-
const head = anchor + selectedNode.nodeSize;
|
|
36407
|
-
const linkSelection = TextSelection.create(view.state.doc, anchor, head);
|
|
36408
|
-
const transaction = view.state.tr.setSelection(linkSelection);
|
|
36409
|
-
view.dispatch(transaction);
|
|
36503
|
+
// A node selection (a linked image, for example) is left alone
|
|
36504
|
+
if (view.state.selection instanceof NodeSelection) return;
|
|
36505
|
+
const range = linkRangeAt(view.state);
|
|
36506
|
+
if (!range) return;
|
|
36507
|
+
const {from, to} = view.state.selection;
|
|
36508
|
+
if (range.from === from && range.to === to) return;
|
|
36509
|
+
const linkSelection = TextSelection.create(view.state.doc, range.from, range.to);
|
|
36510
|
+
view.dispatch(view.state.tr.setSelection(linkSelection));
|
|
36410
36511
|
}
|
|
36411
36512
|
|
|
36412
36513
|
/********************************************************************************
|
|
@@ -43333,7 +43434,13 @@ class Searcher {
|
|
|
43333
43434
|
* Create a new SearchQuery and highlight all the matches in the document.
|
|
43334
43435
|
*/
|
|
43335
43436
|
_buildQuery() {
|
|
43336
|
-
|
|
43437
|
+
// The search string has straight quotes and apostrophes. Match them against the straight and
|
|
43438
|
+
// curly forms in the document text, without changing the document.
|
|
43439
|
+
const pattern = this._searchString
|
|
43440
|
+
.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
43441
|
+
.replaceAll("'", "['‘’]")
|
|
43442
|
+
.replaceAll('"', '["“”]');
|
|
43443
|
+
this._searchQuery = new SearchQuery({search: pattern, regexp: true, caseSensitive: this._caseSensitive});
|
|
43337
43444
|
}
|
|
43338
43445
|
|
|
43339
43446
|
}
|
|
@@ -43589,6 +43696,8 @@ const MU = {
|
|
|
43589
43696
|
getLocalImages,
|
|
43590
43697
|
getHTML,
|
|
43591
43698
|
getHeaders,
|
|
43699
|
+
headingSlug,
|
|
43700
|
+
headingSlugs,
|
|
43592
43701
|
getHeight,
|
|
43593
43702
|
getImageAttributes,
|
|
43594
43703
|
getLinkAttributes,
|