markupeditor 0.9.51 → 0.9.53

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.
@@ -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) {
@@ -35495,8 +35529,27 @@ function getLinkAttributes() {
35495
35529
  const linkMarks = selectedNode.marks.filter(mark => mark.type === schema.marks.link);
35496
35530
  if (linkMarks.length === 1) {
35497
35531
  return {href: linkMarks[0].attrs.href, link: selectedNode.text};
35532
+ } } // A linked image carries the link mark on the image node, which has no text to report as the link.
35533
+ const image = _selectedImageNode(view.state);
35534
+ if (image) {
35535
+ const linkMarks = image.marks.filter(mark => mark.type === schema.marks.link);
35536
+ if (linkMarks.length === 1) {
35537
+ return {href: linkMarks[0].attrs.href, link: image.attrs.alt ?? ''};
35498
35538
  } } return {};
35499
35539
  }
35540
+ function _selectedImageNode(state) {
35541
+ const {from, to} = state.selection;
35542
+ let image = null;
35543
+ let count = 0;
35544
+ state.doc.nodesBetween(from, to, node => {
35545
+ if (node.type === state.schema.nodes.image) {
35546
+ image = node;
35547
+ count++;
35548
+ }
35549
+ });
35550
+ return (count === 1) ? image : null
35551
+ }
35552
+
35500
35553
  function _getImageAttributes() {
35501
35554
  const view = activeView();
35502
35555
  return getImageAttributes(view.state)
@@ -36228,12 +36281,10 @@ function idForInternalLinkCommand(hTag, index) {
36228
36281
  }
36229
36282
 
36230
36283
  /**
36231
- * Return a unique identifier for the heading `node` by lowercasing its trimmed textContent
36232
- * and replacing blanks with `-`, then appending a number until its unique if required.
36233
- * If the heading `node` has an id, then just return it.
36234
- *
36235
- * Since the `node.textContent` can be arbitrarily large, we limit the id to 40 characters
36236
- * just to avoid unwieldy IDs.
36284
+ * Return a unique identifier for the heading `node`: its slug as `headingSlugs` numbers it by its
36285
+ * position among all the headings in `state.doc`, as GitHub does. If another node already uses that
36286
+ * id, append `-1`, `-2`, and so on until it is unique. If the heading `node` has an id, then just
36287
+ * return it.
36237
36288
  *
36238
36289
  * @ignore
36239
36290
  * @param {Node} node A ProseMirror Node that is of heading type
@@ -36242,15 +36293,20 @@ function idForInternalLinkCommand(hTag, index) {
36242
36293
  */
36243
36294
  function idForHeader(node, state) {
36244
36295
  if (node.attrs.id) return node.attrs.id
36245
- let id = node.textContent.toLowerCase().substring(0, 40);
36246
- id = id.replaceAll(' ', '-');
36247
- let {node: idNode} = nodeWithId(id, state);
36296
+ const texts = [];
36297
+ let position = -1;
36298
+ state.doc.descendants(child => {
36299
+ if (child.type !== state.schema.nodes.heading) return true
36300
+ if (child === node) position = texts.length;
36301
+ texts.push(child.textContent);
36302
+ return false
36303
+ });
36304
+ const base = (position >= 0) ? headingSlugs(texts)[position] : headingSlug(node.textContent);
36305
+ let id = base;
36248
36306
  let index = 0;
36249
- while (idNode) {
36307
+ while (nodeWithId(id, state).node) {
36250
36308
  index++;
36251
- id = id + index.toString();
36252
- let {node} = nodeWithId(id, state);
36253
- idNode = node;
36309
+ id = base + '-' + index.toString();
36254
36310
  }
36255
36311
  return id
36256
36312
  }
@@ -36289,20 +36345,25 @@ function headerMatching(hTag, index, state) {
36289
36345
 
36290
36346
  /**
36291
36347
  * 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
36348
+ * order within each level. Each entry is `{hTag, index, text, id, slug}`, where `hTag` and `index` identify
36293
36349
  * the header the same way `insertInternalLink(hTag, index)` and `idForInternalLinkCommand(hTag, index)`
36294
- * do, and `id` is the header's current id attribute (null if it hasn't been assigned one yet).
36350
+ * do, `id` is the header's current id attribute (null if it hasn't been assigned one yet), and `slug`
36351
+ * is the id a link to the header would give it, numbered by position among all the headers.
36295
36352
  *
36296
36353
  * An empty header is omitted -- it can't form a usable id, so there's nothing to link to -- but
36297
36354
  * `index` still reflects its true position among ALL headers at that level (empty ones included),
36298
36355
  * matching what `headerMatching` expects; it's only the reported entries that skip empty headers,
36299
36356
  * not the numbering.
36300
36357
  *
36301
- * @returns {string} JSON-stringified array of `{hTag, index, text, id}`
36358
+ * @returns {string} JSON-stringified array of `{hTag, index, text, id, slug}`
36302
36359
  */
36303
36360
  function getHeaders() {
36304
36361
  const view = activeView();
36305
36362
  const headersByLevel = headers(view.state);
36363
+ // Slugs are numbered by position among all the headings in the document
36364
+ const slugs = new Map();
36365
+ const all = Object.values(headersByLevel).flat().sort((a, b) => a.pos - b.pos);
36366
+ headingSlugs(all.map(h => h.node.textContent)).forEach((slug, i) => slugs.set(all[i].node, slug));
36306
36367
  let result = [];
36307
36368
  for (let level = 1; level < 7; level++) {
36308
36369
  let hNodes = headersByLevel[level];
@@ -36310,7 +36371,7 @@ function getHeaders() {
36310
36371
  hNodes.forEach((h, index) => {
36311
36372
  const text = h.node.textContent;
36312
36373
  if (text.trim().length === 0) return
36313
- result.push({hTag: 'H' + level.toString(), index: index, text: text, id: h.node.attrs.id ?? null});
36374
+ result.push({hTag: 'H' + level.toString(), index: index, text: text, id: h.node.attrs.id ?? null, slug: slugs.get(h.node)});
36314
36375
  });
36315
36376
  }
36316
36377
  }
@@ -42044,7 +42105,16 @@ const codeHighlightPlugin = new Plugin({
42044
42105
  }
42045
42106
  },
42046
42107
  props: {
42047
- decorations(state) { return codeHighlightPlugin.getState(state).decorations }
42108
+ // The code_block holding the selection is left unhighlighted. Highlighting
42109
+ // wraps the text in spans that are rebuilt as it changes, which breaks the
42110
+ // system's inline predictions while typing. The block is highlighted again
42111
+ // when the selection leaves it.
42112
+ decorations(state) {
42113
+ const {decorations} = codeHighlightPlugin.getState(state);
42114
+ const editing = codeBlockAtSelection(state);
42115
+ if (!editing) return decorations
42116
+ return decorations.remove(decorations.find(editing.pos, editing.pos + editing.node.nodeSize))
42117
+ }
42048
42118
  }
42049
42119
  });
42050
42120
 
@@ -43324,7 +43394,13 @@ class Searcher {
43324
43394
  * Create a new SearchQuery and highlight all the matches in the document.
43325
43395
  */
43326
43396
  _buildQuery() {
43327
- this._searchQuery = new SearchQuery({search: this._searchString, caseSensitive: this._caseSensitive});
43397
+ // The search string has straight quotes and apostrophes. Match them against the straight and
43398
+ // curly forms in the document text, without changing the document.
43399
+ const pattern = this._searchString
43400
+ .replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
43401
+ .replaceAll("'", "['‘’]")
43402
+ .replaceAll('"', '["“”]');
43403
+ this._searchQuery = new SearchQuery({search: pattern, regexp: true, caseSensitive: this._caseSensitive});
43328
43404
  }
43329
43405
 
43330
43406
  }
@@ -43580,6 +43656,8 @@ const MU = {
43580
43656
  getLocalImages,
43581
43657
  getHTML,
43582
43658
  getHeaders,
43659
+ headingSlug,
43660
+ headingSlugs,
43583
43661
  getHeight,
43584
43662
  getImageAttributes,
43585
43663
  getLinkAttributes,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "markupeditor",
3
- "version": "0.9.51",
3
+ "version": "0.9.53",
4
4
  "description": "A web component and API for WYSIWYG HTML editing.",
5
5
  "keywords": [
6
6
  "wysiwyg",