lexxy 0.9.19 → 0.9.20

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b6868388203b7cfd6a7026208872b75268aafab61e5e29abc57ed35825dfdb8d
4
- data.tar.gz: cdbb2c3702b5d204c2820a8a0fdf5c2c851c4d8272b1aa32e9dd8680adae409d
3
+ metadata.gz: 3e6fda495d92cad60dac933e1fbe6466f0813def098debbe4bea232130db0721
4
+ data.tar.gz: 0e579b377be3610f91228422dcd3480bae586942aee4294c5d3bd580f60f0de4
5
5
  SHA512:
6
- metadata.gz: 6a0ced04355235c795d5329b620f75e39ead9abfb64e4111939c2ce40c0a099b6d8aeed6222105d9e17aa909be1cc1e7a83037b2b9ff2fc8c49849f61f21a346
7
- data.tar.gz: 26d5669519354092c672bc86bfe37f0939c16bc24336f3242e87ef4d9a5e7416bd856b275238c1bca32b5dd20a98f2c20784aee6c064a86033cc6edb34ced695
6
+ metadata.gz: 4527448422497989de11f0173dbaab24f357c477e8661f7511f8b1d3f9794b60e0073496266205ae641975a08439cabbbe084a435f6b9499e3d9675a394959bf
7
+ data.tar.gz: f43f34d4a40a05f0fdd13d29afa67c68abb9e55b13f0d3703e36181af4f7e1d12fcb4ddf29d706bdf301d877cd95147cb734310f9b5110fd449556984b35293f
@@ -6348,8 +6348,8 @@ function dispatch(element, eventName, detail = null, cancelable = false) {
6348
6348
  }
6349
6349
 
6350
6350
  function addBlockSpacing(doc) {
6351
- const blocks = doc.querySelectorAll("body > :not(h1, h2, h3, h4, h5, h6) + *");
6352
- for (const block of blocks) {
6351
+ const selector = "body > :not(h1, h2, h3, h4, h5, h6) + *, blockquote > :not(h1, h2, h3, h4, h5, h6) + *";
6352
+ for (const block of doc.querySelectorAll(selector)) {
6353
6353
  const spacer = doc.createElement("p");
6354
6354
  spacer.appendChild(doc.createElement("br"));
6355
6355
  block.before(spacer);
@@ -8018,6 +8018,7 @@ function $splitSelectedParagraphsAtInnerLineBreaks(selection) {
8018
8018
 
8019
8019
  function $expandSelectionToLineBreaksAndSplitAtEdges(selection, fallbackAncestor = (node) => node.getTopLevelElement()) {
8020
8020
  j$6(selection);
8021
+ $shrinkSelectionPastBlockEdges(selection);
8021
8022
 
8022
8023
  const focusCaret = ql(selection.focus, "next");
8023
8024
  const anchorCaret = ql(selection.anchor, "previous");
@@ -8049,6 +8050,52 @@ function $expandSelectionToLineBreaksAndSplitAtEdges(selection, fallbackAncestor
8049
8050
  ));
8050
8051
  }
8051
8052
 
8053
+ // A selection whose anchor sits at the very end of one block while its focus
8054
+ // lives in a later block (e.g. selecting a pasted paragraph when the browser
8055
+ // anchors at the end of the line above) contributes nothing from the anchor's
8056
+ // block. Pull each endpoint that is flush against a block edge into the block
8057
+ // that actually holds the selected content, so we don't wrap the empty edge
8058
+ // block too.
8059
+ function $shrinkSelectionPastBlockEdges(selection) {
8060
+ if (selection.isCollapsed()) return
8061
+
8062
+ const anchorBlock = selection.anchor.getNode().getTopLevelElement();
8063
+ const focusBlock = selection.focus.getNode().getTopLevelElement();
8064
+ if (!anchorBlock || !focusBlock || anchorBlock.is(focusBlock)) return
8065
+
8066
+ if ($isAtBlockEnd(selection.anchor, anchorBlock)) {
8067
+ const nextBlock = anchorBlock.getNextSibling();
8068
+ if (nextBlock) selection.anchor.set(nextBlock.getKey(), 0, "element");
8069
+ }
8070
+
8071
+ if ($isAtBlockStart(selection.focus, focusBlock)) {
8072
+ const previousBlock = focusBlock.getPreviousSibling();
8073
+ if (previousBlock) selection.focus.set(previousBlock.getKey(), previousBlock.getChildrenSize(), "element");
8074
+ }
8075
+ }
8076
+
8077
+ function $isAtBlockEnd(point, block) {
8078
+ return $isAtBlockBoundary(ql(point, "next"), block)
8079
+ }
8080
+
8081
+ function $isAtBlockStart(point, block) {
8082
+ return $isAtBlockBoundary(ql(point, "previous"), block)
8083
+ }
8084
+
8085
+ // A text point sitting mid-node still has content ahead of it in the caret's
8086
+ // direction, even though that content is not a sibling node. $getNodeAtCaret
8087
+ // only sees siblings, so check the text edge before walking the block.
8088
+ function $isAtBlockBoundary(caret, block) {
8089
+ if (vl(caret) && ic(caret)) return false
8090
+
8091
+ let cursor = rc(caret);
8092
+ while (cursor && block.isParentOf(cursor.origin)) {
8093
+ if (cursor.getNodeAtCaret()) return false
8094
+ cursor = cursor.getParentCaret();
8095
+ }
8096
+ return true
8097
+ }
8098
+
8052
8099
  function $getCaretAtLineBreakBoundary(caret, skipInwardEdge = false) {
8053
8100
  const paragraph = caret.origin.getTopLevelElement();
8054
8101
  if (!paragraph || !no(paragraph)) return null
@@ -9469,7 +9516,7 @@ function $registerPreConversion(editor) {
9469
9516
  // and applied after retokenization via a mutation listener.
9470
9517
  function $preConversionWithHighlightsFactory(editor) {
9471
9518
  return function $preConversionWithHighlights(domNode) {
9472
- const highlights = extractHighlightRanges$1(domNode);
9519
+ const highlights = extractHighlightRanges(domNode);
9473
9520
  if (highlights.length === 0) return null
9474
9521
 
9475
9522
  return {
@@ -9486,7 +9533,7 @@ function $preConversionWithHighlightsFactory(editor) {
9486
9533
 
9487
9534
  // Walk the DOM tree inside a <pre> element and build a list of
9488
9535
  // { start, end, style } ranges for every <mark> element found.
9489
- function extractHighlightRanges$1(preElement) {
9536
+ function extractHighlightRanges(preElement) {
9490
9537
  const ranges = [];
9491
9538
  const codeElement = preElement.querySelector("code") || preElement;
9492
9539
 
@@ -12019,6 +12066,7 @@ class PastedContentFormatter {
12019
12066
  format() {
12020
12067
  this.#unwrapPlaceholderAnchors();
12021
12068
  this.#stripTableCellColorStyles();
12069
+ this.#nestStrayListChildren();
12022
12070
  this.#stripStrayListChildren();
12023
12071
  return this.doc
12024
12072
  }
@@ -12047,6 +12095,22 @@ class PastedContentFormatter {
12047
12095
  }
12048
12096
  }
12049
12097
 
12098
+ // Some sources (e.g. Gmail) nest a sublist as a direct child of the parent
12099
+ // <ol>/<ul> instead of inside a <li>. Move each nested list into its
12100
+ // preceding <li> so the import preserves the nesting instead of dropping it.
12101
+ #nestStrayListChildren() {
12102
+ for (const list of this.doc.querySelectorAll("ol, ul")) {
12103
+ for (const child of Array.from(list.children)) {
12104
+ if (child.tagName !== "OL" && child.tagName !== "UL") continue
12105
+
12106
+ const previousItem = child.previousElementSibling;
12107
+ if (previousItem && previousItem.tagName === "LI") {
12108
+ previousItem.appendChild(child);
12109
+ }
12110
+ }
12111
+ }
12112
+ }
12113
+
12050
12114
  // Only <li> is a valid child of a list; drop stray <br>/whitespace so the
12051
12115
  // import doesn't wrap them into an empty leading item.
12052
12116
  #stripStrayListChildren() {
@@ -12090,7 +12154,11 @@ class Contents {
12090
12154
 
12091
12155
  insertText(text, { tag } = {}) {
12092
12156
  this.editor.update(() => {
12093
- const paragraph = eo().append(kr(text));
12157
+ const paragraph = eo();
12158
+ text.split("\n").forEach((line, index) => {
12159
+ if (index > 0) paragraph.append(or());
12160
+ paragraph.append(kr(line));
12161
+ });
12094
12162
  this.insertAtCursor(paragraph);
12095
12163
  }, { tag });
12096
12164
  }
@@ -12958,14 +13026,16 @@ class Clipboard {
12958
13026
 
12959
13027
  // Markdown conversion collapses runs of whitespace and unescapes backslashes,
12960
13028
  // silently corrupting plain text such as Windows/UNC file paths. When the text
12961
- // carries no Markdown structure, paste it verbatim instead.
13029
+ // carries no Markdown structure, paste it verbatim instead. A path that wrapped
13030
+ // across lines renders as a single paragraph with <br> line breaks (marked runs
13031
+ // with breaks: true), which is still plain text we should preserve untouched.
12962
13032
  #isPlainTextWithoutMarkdown(doc) {
12963
13033
  const elements = Array.from(doc.body.children);
12964
13034
  if (elements.length !== 1) return false
12965
13035
 
12966
13036
  const paragraph = elements[0];
12967
13037
  return paragraph.nodeName === "P"
12968
- && Array.from(paragraph.childNodes).every((node) => node.nodeType === Node.TEXT_NODE)
13038
+ && Array.from(paragraph.childNodes).every((node) => node.nodeType === Node.TEXT_NODE || node.nodeName === "BR")
12969
13039
  }
12970
13040
 
12971
13041
  #pasteRichText(clipboardData) {
@@ -14253,24 +14323,46 @@ class FormatEscapeExtension extends LexxyExtension {
14253
14323
  }
14254
14324
 
14255
14325
  function $escapeFromBlockquote() {
14326
+ return $escapeBeforeBlockquoteStart() || $escapeFromBlankBlockquoteParagraph()
14327
+ }
14328
+
14329
+ function $escapeBeforeBlockquoteStart() {
14330
+ const selection = Qr();
14331
+ if (!Fr(selection) || !selection.isCollapsed() || selection.anchor.offset !== 0) return false
14332
+
14333
+ const paragraph = At$2(selection.anchor.getNode(), Zi);
14334
+ if (paragraph && !$isBlankNode(paragraph) && !paragraph.getPreviousSibling()) {
14335
+ const blockquote = paragraph.getParent();
14336
+ if (Mt$2(blockquote)) {
14337
+ blockquote.insertBefore(eo());
14338
+ return true
14339
+ }
14340
+ }
14341
+
14342
+ return false
14343
+ }
14344
+
14345
+ function $escapeFromBlankBlockquoteParagraph() {
14256
14346
  const anchorNode = Qr().anchor.getNode();
14257
14347
 
14258
14348
  const paragraph = At$2(anchorNode, Zi);
14259
14349
  if (!paragraph || !$isBlankNode(paragraph)) return false
14260
14350
 
14261
14351
  const blockquote = paragraph.getParent();
14262
- if (!blockquote || !Mt$2(blockquote)) return false
14352
+ if (Mt$2(blockquote)) {
14353
+ const nonEmptySiblings = paragraph.getNextSiblings().filter(sibling => !$isBlankNode(sibling));
14263
14354
 
14264
- const nonEmptySiblings = paragraph.getNextSiblings().filter(sibling => !$isBlankNode(sibling));
14355
+ if (nonEmptySiblings.length > 0) {
14356
+ $splitQuoteNode(blockquote, paragraph);
14357
+ } else {
14358
+ blockquote.insertAfter(paragraph);
14359
+ paragraph.selectStart();
14360
+ }
14265
14361
 
14266
- if (nonEmptySiblings.length > 0) {
14267
- $splitQuoteNode(blockquote, paragraph);
14268
- } else {
14269
- blockquote.insertAfter(paragraph);
14270
- paragraph.selectStart();
14362
+ return true
14271
14363
  }
14272
14364
 
14273
- return true
14365
+ return false
14274
14366
  }
14275
14367
 
14276
14368
  function $splitQuoteNode(node, paragraph) {
@@ -14528,6 +14620,15 @@ class CustomAttachmentDragAndDrop {
14528
14620
  // they only drop onto an existing line, so snap to the nearest one.
14529
14621
  if (caret.node === rootElement) {
14530
14622
  return this.#nearestLineCaret(rootElement, event.clientY)
14623
+ }
14624
+
14625
+ // When mentions sit next to each other with no text between them, the caret
14626
+ // lands inside the neighbouring decorator's DOM. Lexical can't resolve a point
14627
+ // inside a decorator to an editable position, and the cursor has no business
14628
+ // showing there anyway, so snap to just before or after that mention.
14629
+ const decorator = this.#decoratorElementContaining(caret.node);
14630
+ if (decorator) {
14631
+ return this.#dropPointBesideDecorator(decorator, event.clientX)
14531
14632
  } else {
14532
14633
  return caret
14533
14634
  }
@@ -14556,24 +14657,55 @@ class CustomAttachmentDragAndDrop {
14556
14657
  }
14557
14658
  }
14558
14659
 
14660
+ #decoratorElementContaining(node) {
14661
+ const element = node.nodeType === Node.ELEMENT_NODE ? node : node.parentElement;
14662
+ return element?.closest("[data-lexxy-decorator][data-lexical-node-key]")
14663
+ }
14664
+
14665
+ #dropPointBesideDecorator(decorator, clientX) {
14666
+ const rect = decorator.getBoundingClientRect();
14667
+ const placement = clientX > rect.left + rect.width / 2 ? "after" : "before";
14668
+ return { decoratorKey: decorator.dataset.lexicalNodeKey, placement }
14669
+ }
14670
+
14559
14671
  #moveAttachment(draggedKey, dropPoint) {
14560
14672
  this.#editor.update(() => {
14561
14673
  const draggedNode = Yo(draggedKey);
14562
14674
  if (!$isCustomActionTextAttachmentNode(draggedNode)) return
14563
14675
 
14564
- const selection = Gr({
14565
- anchorNode: dropPoint.node,
14566
- anchorOffset: dropPoint.offset,
14567
- focusNode: dropPoint.node,
14568
- focusOffset: dropPoint.offset
14569
- }, this.#editor);
14570
- if (!selection) return
14676
+ if (dropPoint.decoratorKey) {
14677
+ this.#moveBesideNode(draggedNode, dropPoint);
14678
+ } else {
14679
+ this.#moveToCaret(draggedNode, dropPoint);
14680
+ }
14681
+ });
14682
+ }
14571
14683
 
14572
- es(selection);
14684
+ #moveBesideNode(draggedNode, { decoratorKey, placement }) {
14685
+ const targetNode = Yo(decoratorKey);
14686
+ if (!targetNode || targetNode === draggedNode) return
14573
14687
 
14574
- draggedNode.remove();
14575
- selection.insertNodes([ draggedNode ]);
14576
- });
14688
+ draggedNode.remove();
14689
+ if (placement === "after") {
14690
+ targetNode.insertAfter(draggedNode);
14691
+ } else {
14692
+ targetNode.insertBefore(draggedNode);
14693
+ }
14694
+ }
14695
+
14696
+ #moveToCaret(draggedNode, dropPoint) {
14697
+ const selection = Gr({
14698
+ anchorNode: dropPoint.node,
14699
+ anchorOffset: dropPoint.offset,
14700
+ focusNode: dropPoint.node,
14701
+ focusOffset: dropPoint.offset
14702
+ }, this.#editor);
14703
+ if (!selection) return
14704
+
14705
+ es(selection);
14706
+
14707
+ draggedNode.remove();
14708
+ selection.insertNodes([ draggedNode ]);
14577
14709
  }
14578
14710
 
14579
14711
  #updateDropIndicator(event) {
@@ -14583,7 +14715,12 @@ class CustomAttachmentDragAndDrop {
14583
14715
  if (dropPoint) this.#showCaret(this.#caretRectFor(dropPoint));
14584
14716
  }
14585
14717
 
14586
- #caretRectFor({ node, offset }) {
14718
+ #caretRectFor(dropPoint) {
14719
+ if (dropPoint.decoratorKey) {
14720
+ return this.#decoratorEdgeRect(dropPoint)
14721
+ }
14722
+
14723
+ const { node, offset } = dropPoint;
14587
14724
  const rect = caretRect(node, offset);
14588
14725
  if (rect) return rect
14589
14726
 
@@ -14595,6 +14732,15 @@ class CustomAttachmentDragAndDrop {
14595
14732
  return { left: lineRect.left, top: lineRect.top, height: lineRect.height }
14596
14733
  }
14597
14734
 
14735
+ #decoratorEdgeRect({ decoratorKey, placement }) {
14736
+ const decorator = this.#editor.getRootElement()?.querySelector(`[data-lexical-node-key="${decoratorKey}"]`);
14737
+ if (!decorator) return null
14738
+
14739
+ const rect = decorator.getBoundingClientRect();
14740
+ const left = placement === "after" ? rect.right : rect.left;
14741
+ return { left, top: rect.top, height: rect.height }
14742
+ }
14743
+
14598
14744
  #showCaret(rect) {
14599
14745
  if (!rect) return
14600
14746
 
@@ -17209,16 +17355,16 @@ function highlightElement(preElement) {
17209
17355
  if (preElement.dataset.highlighted === "true") return
17210
17356
 
17211
17357
  const language = preElement.getAttribute("data-language");
17212
- let code = preElement.innerHTML.replace(/<br\s*\/?>/gi, "\n");
17213
17358
 
17214
17359
  const grammar = Prism$1.languages?.[language];
17215
17360
  if (!grammar) return
17216
17361
 
17217
- // Extract highlight ranges before Prism destroys <mark> elements
17218
- const highlights = extractHighlightRanges(preElement);
17219
-
17220
- // unescape HTML entities in the code block
17221
- code = new DOMParser().parseFromString(code, "text/html").body.textContent || "";
17362
+ // Read the source text and <mark> ranges in a single walk, before Prism
17363
+ // rewrites the element. Sharing one traversal keeps the highlight offsets
17364
+ // aligned with the code string and preserves leading whitespace — deriving
17365
+ // either of them separately (e.g. textContent through DOMParser) collapses
17366
+ // leading whitespace and shifts every range, re-indenting the rendered block.
17367
+ const { code, highlights } = extractCodeAndHighlights(preElement);
17222
17368
 
17223
17369
  const highlightedHtml = Prism$1.highlight(code, grammar, language);
17224
17370
  preElement.innerHTML = highlightedHtml;
@@ -17230,34 +17376,35 @@ function highlightElement(preElement) {
17230
17376
  preElement.dataset.highlighted = "true";
17231
17377
  }
17232
17378
 
17233
- // Walk the DOM tree inside a <pre> element and build a list of
17234
- // { start, end, style } ranges for every <mark> element found.
17235
- function extractHighlightRanges(preElement) {
17236
- const ranges = [];
17379
+ // Walk the <pre> once, building Prism's source text and the <mark> ranges
17380
+ // together: a text node contributes its text verbatim, a <br> contributes a
17381
+ // newline, and a <mark> records the slice of code it covers. Because both
17382
+ // outputs come from the same walk, every range offset is just a position in
17383
+ // `code` — so the highlights can't drift out of sync with the source, and the
17384
+ // block's leading whitespace survives (HTML parsing would collapse it).
17385
+ function extractCodeAndHighlights(preElement) {
17237
17386
  const root = preElement.querySelector("code") || preElement;
17238
-
17239
- let offset = 0;
17387
+ const highlights = [];
17388
+ let code = "";
17240
17389
 
17241
17390
  function walk(node) {
17242
17391
  if (node.nodeType === Node.TEXT_NODE) {
17243
- offset += node.textContent.length;
17392
+ code += node.textContent;
17244
17393
  } else if (node.nodeType === Node.ELEMENT_NODE) {
17245
17394
  if (node.tagName === "BR") {
17246
- offset += 1;
17247
- return
17248
- }
17249
-
17250
- const isMark = node.tagName === "MARK";
17251
- const start = offset;
17252
-
17253
- for (const child of node.childNodes) {
17254
- walk(child);
17255
- }
17256
-
17257
- if (isMark) {
17395
+ code += "\n";
17396
+ } else if (node.tagName === "MARK") {
17397
+ const start = code.length;
17398
+ for (const child of node.childNodes) {
17399
+ walk(child);
17400
+ }
17258
17401
  const style = extractStyle(node);
17259
17402
  if (style) {
17260
- ranges.push({ start, end: offset, style });
17403
+ highlights.push({ start, end: code.length, style });
17404
+ }
17405
+ } else {
17406
+ for (const child of node.childNodes) {
17407
+ walk(child);
17261
17408
  }
17262
17409
  }
17263
17410
  }
@@ -17267,7 +17414,7 @@ function extractHighlightRanges(preElement) {
17267
17414
  walk(child);
17268
17415
  }
17269
17416
 
17270
- return ranges
17417
+ return { code, highlights }
17271
17418
  }
17272
17419
 
17273
17420
  function extractStyle(element) {
@@ -17436,5 +17583,5 @@ const configure = Lexxy.configure;
17436
17583
  // Pushing elements definition to after the current call stack to allow global configuration to take place first
17437
17584
  setTimeout(defineElements, 0);
17438
17585
 
17439
- export { $createActionTextAttachmentNode, $createActionTextAttachmentUploadNode, $isActionTextAttachmentNode, $isCustomActionTextAttachmentNode, ActionTextAttachmentNode, ActionTextAttachmentUploadNode, CustomActionTextAttachmentNode, LexxyExtension as Extension, HorizontalDividerNode, NativeAdapter, configure, highlightCode, highlightElement };
17586
+ export { $createActionTextAttachmentNode, $createActionTextAttachmentUploadNode, $isActionTextAttachmentNode, $isCustomActionTextAttachmentNode, ActionTextAttachmentNode, ActionTextAttachmentUploadNode, CustomActionTextAttachmentNode, LexxyExtension as Extension, HorizontalDividerNode, NativeAdapter, REWRITE_HISTORY_COMMAND, configure, highlightCode, highlightElement };
17440
17587
  //# sourceMappingURL=lexxy.js.map
Binary file
Binary file