lexxy 0.9.28 → 0.9.29

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: afb455253a99563e4938f9f1634f997fdb3231f52e7b760e53cb590eb4873934
4
- data.tar.gz: 8334d0fb26282b75e340e5d5c5eeb65000a876700038331d2f58347865ef6232
3
+ metadata.gz: 3c6392cdd1a3fd5cc9535778cb84e544b5864e6d75378af4e19b6d7c6c0a5628
4
+ data.tar.gz: 88bccc783d7e3de2f7650e2d80ad7899369053182de3fbe4b51c50f7566421fc
5
5
  SHA512:
6
- metadata.gz: 46e58bf9d6993735988227cea4c883a8cb8f6e485626c8a7a28cce6e128b2d417faaef93ff404b0d0c64505263df7424ef909c91fe463576f9fe0087a823ef31
7
- data.tar.gz: 9c5250b597e25d858378e6b219fab8b3c09b6e472f224fff879b22a7b02c85313b3c74a574142566aca9dcc645ceb0691793d30c8b86e1f1cc2c3d5f69382261
6
+ metadata.gz: 0aa7bf890d287539720cb79cd58cee9e32833c26ed4bd0d6418ab7f94c0bed4fee739a14299093a3333e8337d23a0d4cd00fe1524defefa8f14842ceb0b278a9
7
+ data.tar.gz: c0dfc34fee283b34b76281b77b9dd24a57fcd7ef8d488e70628107fb1aa7a537c8811986e1a5f77d5df2573e6d040214a62f037c5f3845de136f44b813f2e71c
@@ -8029,7 +8029,7 @@ class CustomActionTextAttachmentNode extends Ji {
8029
8029
  return Lexxy.global.get("attachmentTagName")
8030
8030
  }
8031
8031
 
8032
- constructor({ tagName, sgid, contentType, innerHtml, plainText }, key) {
8032
+ constructor({ tagName, sgid, contentType, innerHtml, plainText } = {}, key) {
8033
8033
  super(key);
8034
8034
 
8035
8035
  const contentTypeNamespace = Lexxy.global.get("attachmentContentTypeNamespace");
@@ -8840,7 +8840,7 @@ class ActionTextAttachmentNode extends Ji {
8840
8840
  return Lexxy.global.get("attachmentTagName")
8841
8841
  }
8842
8842
 
8843
- constructor({ tagName, sgid, src, previewSrc, previewable, previewStatusUrl, pendingPreview, altText, caption, contentType, fileName, fileSize, width, height, uploadError }, key) {
8843
+ constructor({ tagName, sgid, src, previewSrc, previewable, previewStatusUrl, pendingPreview, altText, caption, contentType, fileName, fileSize, width, height, uploadError } = {}, key) {
8844
8844
  super(key);
8845
8845
 
8846
8846
  this.tagName = tagName || ActionTextAttachmentNode.TAG_NAME;
@@ -12034,7 +12034,7 @@ class ActionTextAttachmentUploadNode extends ActionTextAttachmentNode {
12034
12034
  return null
12035
12035
  }
12036
12036
 
12037
- constructor(node, key) {
12037
+ constructor(node = {}, key) {
12038
12038
  const { file, uploadUrl, blobUrlTemplate, progress, width, height, uploadError, fileName, contentType } = node;
12039
12039
  super({ ...node, contentType: file?.type ?? contentType }, key);
12040
12040
  this.file = file ?? null;
@@ -12055,9 +12055,10 @@ class ActionTextAttachmentUploadNode extends ActionTextAttachmentNode {
12055
12055
  // node is reloaded from saved state such as from history.
12056
12056
  this.#startUploadIfNeeded();
12057
12057
 
12058
- // Bridge-managed uploads (uploadUrl is null) don't have file data to show
12059
- // an image preview, so always show the file icon during upload.
12060
- const canPreviewFile = this.isPreviewableAttachment && this.uploadUrl != null;
12058
+ // Bridge-managed uploads (uploadUrl is null) and restored or remote
12059
+ // instances (no local File) don't have file data to show an image
12060
+ // preview, so always show the file icon during upload.
12061
+ const canPreviewFile = this.isPreviewableAttachment && this.uploadUrl != null && this.file != null;
12061
12062
  const figure = this.createAttachmentFigure(canPreviewFile);
12062
12063
 
12063
12064
  if (canPreviewFile) {
@@ -12128,9 +12129,12 @@ class ActionTextAttachmentUploadNode extends ActionTextAttachmentNode {
12128
12129
  const figcaption = createElement("figcaption", { className: "attachment__caption" });
12129
12130
 
12130
12131
  const nameSpan = createElement("span", { className: "attachment__name", textContent: this.caption || this.fileName || "" });
12131
- const sizeSpan = createElement("span", { className: "attachment__size", textContent: bytesToHumanSize(this.file?.size) });
12132
12132
  figcaption.appendChild(nameSpan);
12133
- figcaption.appendChild(sizeSpan);
12133
+
12134
+ if (this.file) {
12135
+ const sizeSpan = createElement("span", { className: "attachment__size", textContent: bytesToHumanSize(this.file.size) });
12136
+ figcaption.appendChild(sizeSpan);
12137
+ }
12134
12138
 
12135
12139
  return figcaption
12136
12140
  }
@@ -12152,6 +12156,7 @@ class ActionTextAttachmentUploadNode extends ActionTextAttachmentNode {
12152
12156
  async #startUploadIfNeeded() {
12153
12157
  if (this.#uploadStarted) return
12154
12158
  if (!this.uploadUrl) return // Bridge-managed upload — skip DirectUpload
12159
+ if (!this.file) return // Restored or remote instance — no local File to upload
12155
12160
 
12156
12161
  this.#setUploadStarted();
12157
12162
 
@@ -12643,12 +12648,24 @@ class OfficeFormatter {
12643
12648
  }
12644
12649
  }
12645
12650
 
12651
+ // Properties worth rescuing from a foreign style sheet before #stripStyleElements
12652
+ // drops it, keyed by the elements that carry them. Word and Outlook declare
12653
+ // paragraph spacing in the sheet (p.MsoNormal { margin: 0cm }) and never inline,
12654
+ // so once the sheet goes there is nothing left to say how tightly the author's
12655
+ // paragraphs sat, and hosts that turn pasted paragraph margins into explicit
12656
+ // blank lines read tight Office paragraphs as spaced ones and multiply every
12657
+ // line break.
12658
+ const PRESERVABLE_STYLES = {
12659
+ p: [ "margin-top", "margin-bottom" ]
12660
+ };
12661
+
12646
12662
  class PastedContentFormatter {
12647
12663
  constructor(doc) {
12648
12664
  this.doc = doc;
12649
12665
  }
12650
12666
 
12651
12667
  format() {
12668
+ this.#inlinePreservableStyles();
12652
12669
  this.#stripStyleElements();
12653
12670
  this.#unwrapPlaceholderAnchors();
12654
12671
  this.#stripTableCellColorStyles();
@@ -12656,9 +12673,23 @@ class PastedContentFormatter {
12656
12673
  this.#unwrapWrappedListChildren();
12657
12674
  this.#nestStrayListChildren();
12658
12675
  this.#stripStrayListChildren();
12676
+ this.#replaceGmailEmojiImgTags();
12659
12677
  return this.doc
12660
12678
  }
12661
12679
 
12680
+ // Runs before #stripStyleElements, to inline anything we still need
12681
+ // from an external style before we remove it
12682
+ #inlinePreservableStyles() {
12683
+ const rules = this.#styleRules();
12684
+ if (rules.length === 0) return
12685
+
12686
+ for (const [ selector, properties ] of Object.entries(PRESERVABLE_STYLES)) {
12687
+ for (const element of this.doc.querySelectorAll(selector)) {
12688
+ this.#inlineDeclaredStyles(element, properties, rules);
12689
+ }
12690
+ }
12691
+ }
12692
+
12662
12693
  // Spreadsheets (e.g. Excel) copy a <style> block whose rules (td { color:
12663
12694
  // black }, .xlNN { ... }) cascade onto the imported text. That color rides
12664
12695
  // in through the cascade rather than an inline style, so it slips past both
@@ -12753,6 +12784,72 @@ class PastedContentFormatter {
12753
12784
  }
12754
12785
  }
12755
12786
 
12787
+ // Read the rules from a sheet we build rather than from this.doc.styleSheets: the
12788
+ // document came from DOMParser, so it has no browsing context, and WebKit leaves
12789
+ // its styleSheets empty. Parsing the text ourselves still expands the shorthands
12790
+ // Word writes (margin: 0cm) into the longhands we ask for.
12791
+ #styleRules() {
12792
+ const css = Array.from(this.doc.querySelectorAll("style"), (style) => style.textContent).join("\n");
12793
+ if (css.trim() === "") return []
12794
+
12795
+ const sheet = new CSSStyleSheet();
12796
+ sheet.replaceSync(css);
12797
+
12798
+ const rules = [];
12799
+ for (const rule of sheet.cssRules) {
12800
+ if (rule instanceof CSSStyleRule) {
12801
+ rules.push(rule);
12802
+ }
12803
+ }
12804
+ return rules
12805
+ }
12806
+
12807
+ // A declaration the element already carries inline is the author's own and wins.
12808
+ #inlineDeclaredStyles(element, properties, rules) {
12809
+ for (const property of properties) {
12810
+ if (element.style.getPropertyValue(property) === "") {
12811
+ const value = this.#declaredValue(rules, element, property);
12812
+ if (value) {
12813
+ this.#appendStyleProperty(element, property, value);
12814
+ }
12815
+ }
12816
+ }
12817
+ }
12818
+
12819
+ // Source order alone doesn't decide the cascade, so when the matching rules
12820
+ // disagree we can't say which value wins without resolving specificity. Skip
12821
+ // the property rather than guess: guessing is permanent, since the sheet is
12822
+ // about to go, and skipping only leaves the host on its own default.
12823
+ #declaredValue(rules, element, property) {
12824
+ const declared = new Set();
12825
+ for (const rule of rules) {
12826
+ const value = rule.style.getPropertyValue(property);
12827
+ if (value !== "" && element.matches(rule.selectorText)) {
12828
+ declared.add(value);
12829
+ }
12830
+ }
12831
+
12832
+ if (declared.size === 1) {
12833
+ return declared.values().next().value
12834
+ } else {
12835
+ return null
12836
+ }
12837
+ }
12838
+
12839
+ // Assigning through element.style would reserialize the style attribute and
12840
+ // drop the mso-* declarations Word leaves inline (they aren't valid CSS),
12841
+ // and OfficeFormatter still needs to read mso-list from them. Append to the
12842
+ // attribute instead so the original declarations survive untouched.
12843
+ #appendStyleProperty(element, property, value) {
12844
+ const style = element.getAttribute("style");
12845
+ const declaration = `${property}:${value}`;
12846
+ if (style === null || style.trim() === "") {
12847
+ element.setAttribute("style", declaration);
12848
+ } else {
12849
+ element.setAttribute("style", `${style.replace(/;\s*$/, "")};${declaration}`);
12850
+ }
12851
+ }
12852
+
12756
12853
  #firstStrayListChild(list) {
12757
12854
  for (const child of list.childNodes) {
12758
12855
  if (child.nodeType !== Node.ELEMENT_NODE || child.tagName !== "LI") {
@@ -12769,6 +12866,16 @@ class PastedContentFormatter {
12769
12866
  #containsListItems(node) {
12770
12867
  return node.nodeType === Node.ELEMENT_NODE && node.querySelector("li") !== null
12771
12868
  }
12869
+
12870
+ // Emoji in gmail: <img data-emoji="😂" class="an1" alt="😂" aria-label="😂" draggable="false" src="https://fonts.gstatic.com/s/e/notoemoji/17.0/1f602/32.png" loading="lazy" style="height: 1.2em; width: 1.2em; vertical-align: middle;">
12871
+ #replaceGmailEmojiImgTags() {
12872
+ for (const emojiImg of this.doc.querySelectorAll("img[data-emoji]")) {
12873
+ const emoji = emojiImg.dataset.emoji;
12874
+ if (emoji === emojiImg.alt) {
12875
+ emojiImg.replaceWith(emoji);
12876
+ }
12877
+ }
12878
+ }
12772
12879
  }
12773
12880
 
12774
12881
  class Contents {
@@ -12811,20 +12918,6 @@ class Contents {
12811
12918
  }, { tag });
12812
12919
  }
12813
12920
 
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
-
12828
12921
  insertAtCursor(...nodes) {
12829
12922
  const selection = this.#insertableSelection();
12830
12923
  const inserter = BaseNodeInserter.for(selection);
@@ -15542,6 +15635,39 @@ class CustomAttachmentDragAndDropExtension extends LexxyExtension {
15542
15635
  }
15543
15636
  }
15544
15637
 
15638
+ const LINE_SEPARATORS = /\r\n|[\n\r\u2028\u2029]/;
15639
+
15640
+ // A literal line separator inside a text node renders as a break in the editor, or not at all,
15641
+ // but never survives serialization to HTML. macOS text replacements deliver U+2028 LINE SEPARATOR,
15642
+ // and other insertions can carry "\n", so however the text arrives, a transform converts whatever
15643
+ // lands in a text node into line break nodes.
15644
+ class LineSeparatorsExtension extends LexxyExtension {
15645
+ get lexicalExtension() {
15646
+ return this.defineExtension({
15647
+ name: "lexxy/line-separators",
15648
+ register(editor) {
15649
+ return editor.registerNodeTransform(gr, $replaceLineSeparatorWithLineBreak)
15650
+ }
15651
+ })
15652
+ }
15653
+ }
15654
+
15655
+ // Replaces the first separator only: the split leaves dirty text nodes behind, so Lexical re-runs
15656
+ // the transform until none remain.
15657
+ function $replaceLineSeparatorWithLineBreak(textNode) {
15658
+ const match = textNode.getTextContent().match(LINE_SEPARATORS);
15659
+
15660
+ if (match) {
15661
+ let separatorNode;
15662
+ if (match.index === 0) {
15663
+ [ separatorNode ] = textNode.splitText(match[0].length);
15664
+ } else {
15665
+ [ , separatorNode ] = textNode.splitText(match.index, match.index + match[0].length);
15666
+ }
15667
+ separatorNode.replace(or());
15668
+ }
15669
+ }
15670
+
15545
15671
  class LexicalEditorElement extends HTMLElement {
15546
15672
  static formAssociated = true
15547
15673
  static debug = false
@@ -15700,7 +15826,8 @@ class LexicalEditorElement extends HTMLElement {
15700
15826
  FormatEscapeExtension,
15701
15827
  LinkOpenerExtension,
15702
15828
  PreventLexicalTripleClickExtension,
15703
- CustomAttachmentDragAndDropExtension
15829
+ CustomAttachmentDragAndDropExtension,
15830
+ LineSeparatorsExtension
15704
15831
  ]
15705
15832
  }
15706
15833
 
@@ -15888,7 +16015,6 @@ class LexicalEditorElement extends HTMLElement {
15888
16015
  #initialize() {
15889
16016
  this.#registerComponents();
15890
16017
  this.#handleEnter();
15891
- this.#handleReplacementText();
15892
16018
  this.#registerFocusEvents();
15893
16019
  this.#registerHistoryEvents();
15894
16020
  this.#registerFileAcceptFilter();
@@ -16152,14 +16278,6 @@ class LexicalEditorElement extends HTMLElement {
16152
16278
  ));
16153
16279
  }
16154
16280
 
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
-
16163
16281
  #registerFocusEvents() {
16164
16282
  this.#listeners.track(
16165
16283
  registerEventListener(this, "focusin", this.#handleFocusIn),
Binary file
Binary file