lexxy 0.9.27 → 0.9.28

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: 1ef504d8ff6c3ee45519815bb36ea8c2d21316f8032c87967602138c88f4457b
4
- data.tar.gz: 95e79bbbccdffe031d6938acce662d7d466fc248c63b609d0b6a058e9f586da1
3
+ metadata.gz: afb455253a99563e4938f9f1634f997fdb3231f52e7b760e53cb590eb4873934
4
+ data.tar.gz: 8334d0fb26282b75e340e5d5c5eeb65000a876700038331d2f58347865ef6232
5
5
  SHA512:
6
- metadata.gz: d600ab9f59fa036b56143d081ecadecde64c24f674639d64067add0bebb276ab4da142c0022802e6cfe5c656902f866ae4fecc80a37ba63e8385d567e3517caa
7
- data.tar.gz: a0eb4b38a69a6f436a611f5023e078bfff26b37152603f2f3d923256c1e57672845c0bed337cec6c38989582e400be3a24ff7d9e803782d0c56dc94bb5f7995f
6
+ metadata.gz: 46e58bf9d6993735988227cea4c883a8cb8f6e485626c8a7a28cce6e128b2d417faaef93ff404b0d0c64505263df7424ef909c91fe463576f9fe0087a823ef31
7
+ data.tar.gz: 9c5250b597e25d858378e6b219fab8b3c09b6e472f224fff879b22a7b02c85313b3c74a574142566aca9dcc645ceb0691793d30c8b86e1f1cc2c3d5f69382261
@@ -12500,6 +12500,149 @@ BaseNodeInserter.for = (selection) => {
12500
12500
  return inserterClass ? new inserterClass(selection) : selection
12501
12501
  };
12502
12502
 
12503
+ // Word and Outlook never emit <ul>/<ol>/<li>. They simulate lists with flat
12504
+ // paragraphs that declare their depth in an inline `mso-list:l0 level2 lfo1`
12505
+ // and carry the bullet or number glyph in a <span style="mso-list:Ignore">.
12506
+ // Nothing downstream understands either, so the items land as plain paragraphs
12507
+ // with the marker baked into the text. Rebuild real lists from the level each
12508
+ // paragraph declares.
12509
+ //
12510
+ // Everything this reads lives in inline attributes, so it stays correct after
12511
+ // the pasted content formatter has dropped Office's style sheet.
12512
+ class OfficeFormatter {
12513
+ constructor(doc) {
12514
+ this.doc = doc;
12515
+ }
12516
+
12517
+ format() {
12518
+ for (const run of this.#listRuns()) {
12519
+ this.#replaceWithLists(run);
12520
+ }
12521
+ }
12522
+
12523
+ // A run is a stretch of Office list paragraphs that are siblings with nothing
12524
+ // but whitespace and Word's conditional comments between them. Anything else
12525
+ // in between (a heading, a plain paragraph) ends one list and starts another.
12526
+ #listRuns() {
12527
+ const runs = [];
12528
+ const claimed = new Set();
12529
+
12530
+ for (const paragraph of this.doc.querySelectorAll("p[style*='mso-list']")) {
12531
+ if (claimed.has(paragraph) || this.#listLevel(paragraph) === null) continue
12532
+
12533
+ const run = [];
12534
+ let node = paragraph;
12535
+ while (node) {
12536
+ run.push(node);
12537
+ claimed.add(node);
12538
+ node = this.#nextListParagraph(node);
12539
+ }
12540
+ runs.push(run);
12541
+ }
12542
+
12543
+ return runs
12544
+ }
12545
+
12546
+ #nextListParagraph(paragraph) {
12547
+ let sibling = paragraph.nextSibling;
12548
+ while (sibling && this.#isIgnorableBetweenListParagraphs(sibling)) {
12549
+ sibling = sibling.nextSibling;
12550
+ }
12551
+
12552
+ if (sibling && sibling.nodeType === Node.ELEMENT_NODE && this.#listLevel(sibling) !== null) {
12553
+ return sibling
12554
+ }
12555
+
12556
+ return null
12557
+ }
12558
+
12559
+ #isIgnorableBetweenListParagraphs(node) {
12560
+ if (node.nodeType === Node.COMMENT_NODE) {
12561
+ return true
12562
+ }
12563
+ return node.nodeType === Node.TEXT_NODE && node.textContent.trim() === ""
12564
+ }
12565
+
12566
+ // Word numbers levels from 1, but a copied selection often starts partway down
12567
+ // a list, so every paragraph declares level3 with no level1 above it. Anchoring
12568
+ // the run at its own shallowest level is what keeps the result from arriving
12569
+ // needlessly deep.
12570
+ #replaceWithLists(paragraphs) {
12571
+ const levels = paragraphs.map((paragraph) => this.#listLevel(paragraph));
12572
+ const baseLevel = Math.min(...levels);
12573
+ const roots = [];
12574
+ const stack = [];
12575
+
12576
+ paragraphs.forEach((paragraph, index) => {
12577
+ const depth = levels[index] - baseLevel + 1;
12578
+ const tagName = this.#listTagName(paragraph);
12579
+
12580
+ while (stack.length > depth) {
12581
+ stack.pop();
12582
+ }
12583
+ if (stack.length === depth && stack[depth - 1].tagName !== tagName.toUpperCase()) {
12584
+ stack.pop();
12585
+ }
12586
+
12587
+ while (stack.length < depth) {
12588
+ const list = this.doc.createElement(tagName);
12589
+ if (stack.length === 0) {
12590
+ roots.push(list);
12591
+ } else {
12592
+ this.#lastItemOf(stack[stack.length - 1]).appendChild(list);
12593
+ }
12594
+ stack.push(list);
12595
+ }
12596
+
12597
+ stack[stack.length - 1].appendChild(this.#createListItem(paragraph));
12598
+ });
12599
+
12600
+ paragraphs[0].replaceWith(...roots);
12601
+ for (const paragraph of paragraphs.slice(1)) {
12602
+ paragraph.remove();
12603
+ }
12604
+ }
12605
+
12606
+ #lastItemOf(list) {
12607
+ if (!list.lastElementChild) {
12608
+ list.appendChild(this.doc.createElement("li"));
12609
+ }
12610
+ return list.lastElementChild
12611
+ }
12612
+
12613
+ // Word writes bullets as the raw glyph of the marker font (Symbol's "·",
12614
+ // Wingdings' "§", Courier New's "o") and numbers as the rendered counter
12615
+ // ("1.", "a)", "iv)"). A bare letter with no delimiter after it is Courier
12616
+ // New's bullet, not a counter, so the delimiter is what tells them apart.
12617
+ #listTagName(paragraph) {
12618
+ const marker = paragraph.querySelector("[style*='mso-list:Ignore']");
12619
+ const text = marker?.textContent || "";
12620
+
12621
+ if (/\d/.test(text) || /^\(?[A-Za-z]+[.)]/.test(text)) {
12622
+ return "ol"
12623
+ }
12624
+ return "ul"
12625
+ }
12626
+
12627
+ #createListItem(paragraph) {
12628
+ for (const marker of paragraph.querySelectorAll("[style*='mso-list:Ignore']")) {
12629
+ marker.remove();
12630
+ }
12631
+
12632
+ const item = this.doc.createElement("li");
12633
+ item.append(...paragraph.childNodes);
12634
+ return item
12635
+ }
12636
+
12637
+ #listLevel(element) {
12638
+ const match = /mso-list:[^;"']*\blevel(\d+)/.exec(element.getAttribute("style") || "");
12639
+ if (match) {
12640
+ return parseInt(match[1], 10)
12641
+ }
12642
+ return null
12643
+ }
12644
+ }
12645
+
12503
12646
  class PastedContentFormatter {
12504
12647
  constructor(doc) {
12505
12648
  this.doc = doc;
@@ -12509,6 +12652,7 @@ class PastedContentFormatter {
12509
12652
  this.#stripStyleElements();
12510
12653
  this.#unwrapPlaceholderAnchors();
12511
12654
  this.#stripTableCellColorStyles();
12655
+ new OfficeFormatter(this.doc).format();
12512
12656
  this.#unwrapWrappedListChildren();
12513
12657
  this.#nestStrayListChildren();
12514
12658
  this.#stripStrayListChildren();
@@ -12667,6 +12811,20 @@ class Contents {
12667
12811
  }, { tag });
12668
12812
  }
12669
12813
 
12814
+ // A literal "\n" stays visible in the editor, which Lexical renders with white-space: pre-wrap,
12815
+ // but collapses to a space once serialized to HTML, so it has to become line break nodes.
12816
+ insertTextWithLineBreaks(text) {
12817
+ const normalizedText = text?.replace(/\r\n?/g, "\n");
12818
+ const selection = Qr();
12819
+
12820
+ if (normalizedText?.includes("\n") && Fr(selection)) {
12821
+ selection.insertRawText(normalizedText);
12822
+ return true
12823
+ } else {
12824
+ return false
12825
+ }
12826
+ }
12827
+
12670
12828
  insertAtCursor(...nodes) {
12671
12829
  const selection = this.#insertableSelection();
12672
12830
  const inserter = BaseNodeInserter.for(selection);
@@ -15730,6 +15888,7 @@ class LexicalEditorElement extends HTMLElement {
15730
15888
  #initialize() {
15731
15889
  this.#registerComponents();
15732
15890
  this.#handleEnter();
15891
+ this.#handleReplacementText();
15733
15892
  this.#registerFocusEvents();
15734
15893
  this.#registerHistoryEvents();
15735
15894
  this.#registerFileAcceptFilter();
@@ -15993,6 +16152,14 @@ class LexicalEditorElement extends HTMLElement {
15993
16152
  ));
15994
16153
  }
15995
16154
 
16155
+ #handleReplacementText() {
16156
+ this.#listeners.track(this.editor.registerCommand(
16157
+ ge$3,
16158
+ (event) => event instanceof InputEvent && this.contents.insertTextWithLineBreaks(event.data),
16159
+ oo
16160
+ ));
16161
+ }
16162
+
15996
16163
  #registerFocusEvents() {
15997
16164
  this.#listeners.track(
15998
16165
  registerEventListener(this, "focusin", this.#handleFocusIn),
Binary file
Binary file