lexxy 0.9.28 → 0.9.30

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: 29ffce96e1428f47448c9dd8dba203d14209fed8614c09319de418275e7d7c4b
4
+ data.tar.gz: 54b507d1fa6cbbb69e0c34ae60fe9d2ffca21b3f9a9312cb11d690c9a46bd39d
5
5
  SHA512:
6
- metadata.gz: 46e58bf9d6993735988227cea4c883a8cb8f6e485626c8a7a28cce6e128b2d417faaef93ff404b0d0c64505263df7424ef909c91fe463576f9fe0087a823ef31
7
- data.tar.gz: 9c5250b597e25d858378e6b219fab8b3c09b6e472f224fff879b22a7b02c85313b3c74a574142566aca9dcc645ceb0691793d30c8b86e1f1cc2c3d5f69382261
6
+ metadata.gz: a018d5b7719927ffa23b3dd7f83487c1988899b32cb14178415e460358bc86e908a583ec8e064f9692b0deee3f2a7c57f85c0f2dcf761b01653783749ab2c498
7
+ data.tar.gz: d9c7cf6b9653ec1938009ad6c6b6d341dbe6fa79a202432ea3b49090f30d054cf29ec6b580fdb31228868f55fe1b114a1e746dc2a806531cba84bc6fc070a636
@@ -6479,6 +6479,22 @@ function styleFilterHook(_currentNode, hookEvent) {
6479
6479
 
6480
6480
  purify.addHook("uponSanitizeAttribute", styleFilterHook);
6481
6481
 
6482
+ const FORBIDDEN_STIMULUS_ATTRIBUTES = [ "data-controller", "data-action" ];
6483
+
6484
+ // Stimulus behavior attributes must never survive sanitization, whatever an
6485
+ // extension's allowedElements declares. FORBID_ATTR alone isn't enough: in
6486
+ // DOMPurify 3.x the functional ADD_ATTR — which Lexxy builds from the public
6487
+ // allowedElements API — is evaluated ahead of FORBID_ATTR, so an extension that
6488
+ // listed one of these on a tag would otherwise reinstate it. This hook drops
6489
+ // them unconditionally, keeping the class-level prohibition config-independent.
6490
+ function stimulusAttributeFilterHook(_currentNode, hookEvent) {
6491
+ if (FORBIDDEN_STIMULUS_ATTRIBUTES.includes(hookEvent.attrName)) {
6492
+ hookEvent.keepAttr = false;
6493
+ }
6494
+ }
6495
+
6496
+ purify.addHook("uponSanitizeAttribute", stimulusAttributeFilterHook);
6497
+
6482
6498
  purify.addHook("uponSanitizeElement", (node, data) => {
6483
6499
  if (data.tagName === "strong" || data.tagName === "em") {
6484
6500
  node.removeAttribute("class");
@@ -6502,7 +6518,12 @@ function buildConfig(allowedElements ) {
6502
6518
  ALLOWED_ATTR: ALLOWED_HTML_ATTRIBUTES,
6503
6519
  ADD_ATTR: (attribute, tag) => tagAttributes[tag]?.includes(attribute),
6504
6520
  ADD_URI_SAFE_ATTR: [ "caption", "filename" ],
6505
- SAFE_FOR_XML: false // So that it does not strip attributes that contains serialized HTML (like content)
6521
+ SAFE_FOR_XML: false, // So that it does not strip attributes that contains serialized HTML (like content)
6522
+ // Stimulus behavior attributes must never survive sanitization: they let stored content
6523
+ // wire up arbitrary controllers/actions in the viewer's session. FORBID_ATTR wins over
6524
+ // ALLOWED_ATTR/ADD_ATTR/ALLOW_DATA_ATTR in DOMPurify, so this holds even though other
6525
+ // data-* attributes (data-language, data-trix-*, etc.) are otherwise allowed through.
6526
+ FORBID_ATTR: [ "data-controller", "data-action" ]
6506
6527
  }
6507
6528
  }
6508
6529
 
@@ -8029,7 +8050,7 @@ class CustomActionTextAttachmentNode extends Ji {
8029
8050
  return Lexxy.global.get("attachmentTagName")
8030
8051
  }
8031
8052
 
8032
- constructor({ tagName, sgid, contentType, innerHtml, plainText }, key) {
8053
+ constructor({ tagName, sgid, contentType, innerHtml, plainText } = {}, key) {
8033
8054
  super(key);
8034
8055
 
8035
8056
  const contentTypeNamespace = Lexxy.global.get("attachmentContentTypeNamespace");
@@ -8840,7 +8861,7 @@ class ActionTextAttachmentNode extends Ji {
8840
8861
  return Lexxy.global.get("attachmentTagName")
8841
8862
  }
8842
8863
 
8843
- constructor({ tagName, sgid, src, previewSrc, previewable, previewStatusUrl, pendingPreview, altText, caption, contentType, fileName, fileSize, width, height, uploadError }, key) {
8864
+ constructor({ tagName, sgid, src, previewSrc, previewable, previewStatusUrl, pendingPreview, altText, caption, contentType, fileName, fileSize, width, height, uploadError } = {}, key) {
8844
8865
  super(key);
8845
8866
 
8846
8867
  this.tagName = tagName || ActionTextAttachmentNode.TAG_NAME;
@@ -12034,7 +12055,7 @@ class ActionTextAttachmentUploadNode extends ActionTextAttachmentNode {
12034
12055
  return null
12035
12056
  }
12036
12057
 
12037
- constructor(node, key) {
12058
+ constructor(node = {}, key) {
12038
12059
  const { file, uploadUrl, blobUrlTemplate, progress, width, height, uploadError, fileName, contentType } = node;
12039
12060
  super({ ...node, contentType: file?.type ?? contentType }, key);
12040
12061
  this.file = file ?? null;
@@ -12055,9 +12076,10 @@ class ActionTextAttachmentUploadNode extends ActionTextAttachmentNode {
12055
12076
  // node is reloaded from saved state such as from history.
12056
12077
  this.#startUploadIfNeeded();
12057
12078
 
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;
12079
+ // Bridge-managed uploads (uploadUrl is null) and restored or remote
12080
+ // instances (no local File) don't have file data to show an image
12081
+ // preview, so always show the file icon during upload.
12082
+ const canPreviewFile = this.isPreviewableAttachment && this.uploadUrl != null && this.file != null;
12061
12083
  const figure = this.createAttachmentFigure(canPreviewFile);
12062
12084
 
12063
12085
  if (canPreviewFile) {
@@ -12128,9 +12150,12 @@ class ActionTextAttachmentUploadNode extends ActionTextAttachmentNode {
12128
12150
  const figcaption = createElement("figcaption", { className: "attachment__caption" });
12129
12151
 
12130
12152
  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
12153
  figcaption.appendChild(nameSpan);
12133
- figcaption.appendChild(sizeSpan);
12154
+
12155
+ if (this.file) {
12156
+ const sizeSpan = createElement("span", { className: "attachment__size", textContent: bytesToHumanSize(this.file.size) });
12157
+ figcaption.appendChild(sizeSpan);
12158
+ }
12134
12159
 
12135
12160
  return figcaption
12136
12161
  }
@@ -12152,6 +12177,7 @@ class ActionTextAttachmentUploadNode extends ActionTextAttachmentNode {
12152
12177
  async #startUploadIfNeeded() {
12153
12178
  if (this.#uploadStarted) return
12154
12179
  if (!this.uploadUrl) return // Bridge-managed upload — skip DirectUpload
12180
+ if (!this.file) return // Restored or remote instance — no local File to upload
12155
12181
 
12156
12182
  this.#setUploadStarted();
12157
12183
 
@@ -12643,12 +12669,24 @@ class OfficeFormatter {
12643
12669
  }
12644
12670
  }
12645
12671
 
12672
+ // Properties worth rescuing from a foreign style sheet before #stripStyleElements
12673
+ // drops it, keyed by the elements that carry them. Word and Outlook declare
12674
+ // paragraph spacing in the sheet (p.MsoNormal { margin: 0cm }) and never inline,
12675
+ // so once the sheet goes there is nothing left to say how tightly the author's
12676
+ // paragraphs sat, and hosts that turn pasted paragraph margins into explicit
12677
+ // blank lines read tight Office paragraphs as spaced ones and multiply every
12678
+ // line break.
12679
+ const PRESERVABLE_STYLES = {
12680
+ p: [ "margin-top", "margin-bottom" ]
12681
+ };
12682
+
12646
12683
  class PastedContentFormatter {
12647
12684
  constructor(doc) {
12648
12685
  this.doc = doc;
12649
12686
  }
12650
12687
 
12651
12688
  format() {
12689
+ this.#inlinePreservableStyles();
12652
12690
  this.#stripStyleElements();
12653
12691
  this.#unwrapPlaceholderAnchors();
12654
12692
  this.#stripTableCellColorStyles();
@@ -12656,9 +12694,23 @@ class PastedContentFormatter {
12656
12694
  this.#unwrapWrappedListChildren();
12657
12695
  this.#nestStrayListChildren();
12658
12696
  this.#stripStrayListChildren();
12697
+ this.#replaceGmailEmojiImgTags();
12659
12698
  return this.doc
12660
12699
  }
12661
12700
 
12701
+ // Runs before #stripStyleElements, to inline anything we still need
12702
+ // from an external style before we remove it
12703
+ #inlinePreservableStyles() {
12704
+ const rules = this.#styleRules();
12705
+ if (rules.length === 0) return
12706
+
12707
+ for (const [ selector, properties ] of Object.entries(PRESERVABLE_STYLES)) {
12708
+ for (const element of this.doc.querySelectorAll(selector)) {
12709
+ this.#inlineDeclaredStyles(element, properties, rules);
12710
+ }
12711
+ }
12712
+ }
12713
+
12662
12714
  // Spreadsheets (e.g. Excel) copy a <style> block whose rules (td { color:
12663
12715
  // black }, .xlNN { ... }) cascade onto the imported text. That color rides
12664
12716
  // in through the cascade rather than an inline style, so it slips past both
@@ -12753,6 +12805,72 @@ class PastedContentFormatter {
12753
12805
  }
12754
12806
  }
12755
12807
 
12808
+ // Read the rules from a sheet we build rather than from this.doc.styleSheets: the
12809
+ // document came from DOMParser, so it has no browsing context, and WebKit leaves
12810
+ // its styleSheets empty. Parsing the text ourselves still expands the shorthands
12811
+ // Word writes (margin: 0cm) into the longhands we ask for.
12812
+ #styleRules() {
12813
+ const css = Array.from(this.doc.querySelectorAll("style"), (style) => style.textContent).join("\n");
12814
+ if (css.trim() === "") return []
12815
+
12816
+ const sheet = new CSSStyleSheet();
12817
+ sheet.replaceSync(css);
12818
+
12819
+ const rules = [];
12820
+ for (const rule of sheet.cssRules) {
12821
+ if (rule instanceof CSSStyleRule) {
12822
+ rules.push(rule);
12823
+ }
12824
+ }
12825
+ return rules
12826
+ }
12827
+
12828
+ // A declaration the element already carries inline is the author's own and wins.
12829
+ #inlineDeclaredStyles(element, properties, rules) {
12830
+ for (const property of properties) {
12831
+ if (element.style.getPropertyValue(property) === "") {
12832
+ const value = this.#declaredValue(rules, element, property);
12833
+ if (value) {
12834
+ this.#appendStyleProperty(element, property, value);
12835
+ }
12836
+ }
12837
+ }
12838
+ }
12839
+
12840
+ // Source order alone doesn't decide the cascade, so when the matching rules
12841
+ // disagree we can't say which value wins without resolving specificity. Skip
12842
+ // the property rather than guess: guessing is permanent, since the sheet is
12843
+ // about to go, and skipping only leaves the host on its own default.
12844
+ #declaredValue(rules, element, property) {
12845
+ const declared = new Set();
12846
+ for (const rule of rules) {
12847
+ const value = rule.style.getPropertyValue(property);
12848
+ if (value !== "" && element.matches(rule.selectorText)) {
12849
+ declared.add(value);
12850
+ }
12851
+ }
12852
+
12853
+ if (declared.size === 1) {
12854
+ return declared.values().next().value
12855
+ } else {
12856
+ return null
12857
+ }
12858
+ }
12859
+
12860
+ // Assigning through element.style would reserialize the style attribute and
12861
+ // drop the mso-* declarations Word leaves inline (they aren't valid CSS),
12862
+ // and OfficeFormatter still needs to read mso-list from them. Append to the
12863
+ // attribute instead so the original declarations survive untouched.
12864
+ #appendStyleProperty(element, property, value) {
12865
+ const style = element.getAttribute("style");
12866
+ const declaration = `${property}:${value}`;
12867
+ if (style === null || style.trim() === "") {
12868
+ element.setAttribute("style", declaration);
12869
+ } else {
12870
+ element.setAttribute("style", `${style.replace(/;\s*$/, "")};${declaration}`);
12871
+ }
12872
+ }
12873
+
12756
12874
  #firstStrayListChild(list) {
12757
12875
  for (const child of list.childNodes) {
12758
12876
  if (child.nodeType !== Node.ELEMENT_NODE || child.tagName !== "LI") {
@@ -12769,6 +12887,16 @@ class PastedContentFormatter {
12769
12887
  #containsListItems(node) {
12770
12888
  return node.nodeType === Node.ELEMENT_NODE && node.querySelector("li") !== null
12771
12889
  }
12890
+
12891
+ // 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;">
12892
+ #replaceGmailEmojiImgTags() {
12893
+ for (const emojiImg of this.doc.querySelectorAll("img[data-emoji]")) {
12894
+ const emoji = emojiImg.dataset.emoji;
12895
+ if (emoji === emojiImg.alt) {
12896
+ emojiImg.replaceWith(emoji);
12897
+ }
12898
+ }
12899
+ }
12772
12900
  }
12773
12901
 
12774
12902
  class Contents {
@@ -12803,7 +12931,7 @@ class Contents {
12803
12931
  insertText(text, { tag } = {}) {
12804
12932
  this.editor.update(() => {
12805
12933
  const paragraph = eo();
12806
- text.split("\n").forEach((line, index) => {
12934
+ text.split(/\r\n|\r|\n/).forEach((line, index) => {
12807
12935
  if (index > 0) paragraph.append(or());
12808
12936
  paragraph.append(kr(line));
12809
12937
  });
@@ -12811,20 +12939,6 @@ class Contents {
12811
12939
  }, { tag });
12812
12940
  }
12813
12941
 
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
12942
  insertAtCursor(...nodes) {
12829
12943
  const selection = this.#insertableSelection();
12830
12944
  const inserter = BaseNodeInserter.for(selection);
@@ -13531,6 +13645,12 @@ ${e}</tr>
13531
13645
  `+this.renderer.text(o);t?n+=this.renderer.paragraph({type:"paragraph",raw:a,text:a,tokens:[{type:"text",raw:a,text:a,escaped:true}]}):n+=a;continue}default:{let o='Token with "'+s.type+'" type was not found.';if(this.options.silent)return console.error(o),"";throw new Error(o)}}}return n}parseInline(e,t=this.renderer){let n="";for(let r=0;r<e.length;r++){let i=e[r];if(this.options.extensions?.renderers?.[i.type]){let o=this.options.extensions.renderers[i.type].call({parser:this},i);if(o!==false||!["escape","html","link","image","strong","em","codespan","br","del","text"].includes(i.type)){n+=o||"";continue}}let s=i;switch(s.type){case "escape":{n+=t.text(s);break}case "html":{n+=t.html(s);break}case "link":{n+=t.link(s);break}case "image":{n+=t.image(s);break}case "strong":{n+=t.strong(s);break}case "em":{n+=t.em(s);break}case "codespan":{n+=t.codespan(s);break}case "br":{n+=t.br(s);break}case "del":{n+=t.del(s);break}case "text":{n+=t.text(s);break}default:{let o='Token with "'+s.type+'" type was not found.';if(this.options.silent)return console.error(o),"";throw new Error(o)}}}return n}};var S=class{options;block;constructor(e){this.options=e||T;}static passThroughHooks=new Set(["preprocess","postprocess","processAllTokens","emStrongMask"]);static passThroughHooksRespectAsync=new Set(["preprocess","postprocess","processAllTokens"]);preprocess(e){return e}postprocess(e){return e}processAllTokens(e){return e}emStrongMask(e){return e}provideLexer(){return this.block?x.lex:x.lexInline}provideParser(){return this.block?b.parse:b.parseInline}};var B=class{defaults=L();options=this.setOptions;parse=this.parseMarkdown(true);parseInline=this.parseMarkdown(false);Parser=b;Renderer=P;TextRenderer=$;Lexer=x;Tokenizer=y;Hooks=S;constructor(...e){this.use(...e);}walkTokens(e,t){let n=[];for(let r of e)switch(n=n.concat(t.call(this,r)),r.type){case "table":{let i=r;for(let s of i.header)n=n.concat(this.walkTokens(s.tokens,t));for(let s of i.rows)for(let o of s)n=n.concat(this.walkTokens(o.tokens,t));break}case "list":{let i=r;n=n.concat(this.walkTokens(i.items,t));break}default:{let i=r;this.defaults.extensions?.childTokens?.[i.type]?this.defaults.extensions.childTokens[i.type].forEach(s=>{let o=i[s].flat(1/0);n=n.concat(this.walkTokens(o,t));}):i.tokens&&(n=n.concat(this.walkTokens(i.tokens,t)));}}return n}use(...e){let t=this.defaults.extensions||{renderers:{},childTokens:{}};return e.forEach(n=>{let r={...n};if(r.async=this.defaults.async||r.async||false,n.extensions&&(n.extensions.forEach(i=>{if(!i.name)throw new Error("extension name required");if("renderer"in i){let s=t.renderers[i.name];s?t.renderers[i.name]=function(...o){let a=i.renderer.apply(this,o);return a===false&&(a=s.apply(this,o)),a}:t.renderers[i.name]=i.renderer;}if("tokenizer"in i){if(!i.level||i.level!=="block"&&i.level!=="inline")throw new Error("extension level must be 'block' or 'inline'");let s=t[i.level];s?s.unshift(i.tokenizer):t[i.level]=[i.tokenizer],i.start&&(i.level==="block"?t.startBlock?t.startBlock.push(i.start):t.startBlock=[i.start]:i.level==="inline"&&(t.startInline?t.startInline.push(i.start):t.startInline=[i.start]));}"childTokens"in i&&i.childTokens&&(t.childTokens[i.name]=i.childTokens);}),r.extensions=t),n.renderer){let i=this.defaults.renderer||new P(this.defaults);for(let s in n.renderer){if(!(s in i))throw new Error(`renderer '${s}' does not exist`);if(["options","parser"].includes(s))continue;let o=s,a=n.renderer[o],l=i[o];i[o]=(...c)=>{let p=a.apply(i,c);return p===false&&(p=l.apply(i,c)),p||""};}r.renderer=i;}if(n.tokenizer){let i=this.defaults.tokenizer||new y(this.defaults);for(let s in n.tokenizer){if(!(s in i))throw new Error(`tokenizer '${s}' does not exist`);if(["options","rules","lexer"].includes(s))continue;let o=s,a=n.tokenizer[o],l=i[o];i[o]=(...c)=>{let p=a.apply(i,c);return p===false&&(p=l.apply(i,c)),p};}r.tokenizer=i;}if(n.hooks){let i=this.defaults.hooks||new S;for(let s in n.hooks){if(!(s in i))throw new Error(`hook '${s}' does not exist`);if(["options","block"].includes(s))continue;let o=s,a=n.hooks[o],l=i[o];S.passThroughHooks.has(s)?i[o]=c=>{if(this.defaults.async&&S.passThroughHooksRespectAsync.has(s))return (async()=>{let g=await a.call(i,c);return l.call(i,g)})();let p=a.call(i,c);return l.call(i,p)}:i[o]=(...c)=>{if(this.defaults.async)return (async()=>{let g=await a.apply(i,c);return g===false&&(g=await l.apply(i,c)),g})();let p=a.apply(i,c);return p===false&&(p=l.apply(i,c)),p};}r.hooks=i;}if(n.walkTokens){let i=this.defaults.walkTokens,s=n.walkTokens;r.walkTokens=function(o){let a=[];return a.push(s.call(this,o)),i&&(a=a.concat(i.call(this,o))),a};}this.defaults={...this.defaults,...r};}),this}setOptions(e){return this.defaults={...this.defaults,...e},this}lexer(e,t){return x.lex(e,t??this.defaults)}parser(e,t){return b.parse(e,t??this.defaults)}parseMarkdown(e){return (n,r)=>{let i={...r},s={...this.defaults,...i},o=this.onError(!!s.silent,!!s.async);if(this.defaults.async===true&&i.async===false)return o(new Error("marked(): The async option was set to true by an extension. Remove async: false from the parse options object to return a Promise."));if(typeof n>"u"||n===null)return o(new Error("marked(): input parameter is undefined or null"));if(typeof n!="string")return o(new Error("marked(): input parameter is of type "+Object.prototype.toString.call(n)+", string expected"));if(s.hooks&&(s.hooks.options=s,s.hooks.block=e),s.async)return (async()=>{let a=s.hooks?await s.hooks.preprocess(n):n,c=await(s.hooks?await s.hooks.provideLexer():e?x.lex:x.lexInline)(a,s),p=s.hooks?await s.hooks.processAllTokens(c):c;s.walkTokens&&await Promise.all(this.walkTokens(p,s.walkTokens));let d=await(s.hooks?await s.hooks.provideParser():e?b.parse:b.parseInline)(p,s);return s.hooks?await s.hooks.postprocess(d):d})().catch(o);try{s.hooks&&(n=s.hooks.preprocess(n));let l=(s.hooks?s.hooks.provideLexer():e?x.lex:x.lexInline)(n,s);s.hooks&&(l=s.hooks.processAllTokens(l)),s.walkTokens&&this.walkTokens(l,s.walkTokens);let p=(s.hooks?s.hooks.provideParser():e?b.parse:b.parseInline)(l,s);return s.hooks&&(p=s.hooks.postprocess(p)),p}catch(a){return o(a)}}}onError(e,t){return n=>{if(n.message+=`
13532
13646
  Please report this to https://github.com/markedjs/marked.`,e){let r="<p>An error occurred:</p><pre>"+w(n.message+"",true)+"</pre>";return t?Promise.resolve(r):r}if(t)return Promise.reject(n);throw n}}};var _=new B;function k(u,e){return _.parse(u,e)}k.options=k.setOptions=function(u){return _.setOptions(u),k.defaults=_.defaults,G(k.defaults),k};k.getDefaults=L;k.defaults=T;k.use=function(...u){return _.use(...u),k.defaults=_.defaults,G(k.defaults),k};k.walkTokens=function(u,e){return _.walkTokens(u,e)};k.parseInline=_.parseInline;k.Parser=b;k.parser=b.parse;k.Renderer=P;k.TextRenderer=$;k.Lexer=x;k.lexer=x.lex;k.Tokenizer=y;k.Hooks=S;k.parse=k;k.options;k.setOptions;k.use;k.walkTokens;k.parseInline;b.parse;x.lex;
13533
13647
 
13648
+ // Markdown reads any line indented by a tab or four spaces as a code block. Pasted
13649
+ // plain text is full of incidental indentation — outlines, notes, logs — and nobody
13650
+ // indents a note meaning "this is code"; they fence it. Keep fences, drop the
13651
+ // indentation rule.
13652
+ const pastedMarkdown = new B({ breaks: true }).use({ tokenizer: { code: () => undefined } });
13653
+
13534
13654
  class Clipboard {
13535
13655
  #listeners = new ListenerBin()
13536
13656
 
@@ -13717,7 +13837,7 @@ class Clipboard {
13717
13837
  }
13718
13838
 
13719
13839
  #pasteMarkdown(text) {
13720
- const html = k(text, { breaks: true });
13840
+ const html = pastedMarkdown.parse(text);
13721
13841
  const doc = parseHtml(html);
13722
13842
 
13723
13843
  if (this.#isPlainTextWithoutMarkdown(doc)) {
@@ -15542,6 +15662,39 @@ class CustomAttachmentDragAndDropExtension extends LexxyExtension {
15542
15662
  }
15543
15663
  }
15544
15664
 
15665
+ const LINE_SEPARATORS = /\r\n|[\n\r\u2028\u2029]/;
15666
+
15667
+ // A literal line separator inside a text node renders as a break in the editor, or not at all,
15668
+ // but never survives serialization to HTML. macOS text replacements deliver U+2028 LINE SEPARATOR,
15669
+ // and other insertions can carry "\n", so however the text arrives, a transform converts whatever
15670
+ // lands in a text node into line break nodes.
15671
+ class LineSeparatorsExtension extends LexxyExtension {
15672
+ get lexicalExtension() {
15673
+ return this.defineExtension({
15674
+ name: "lexxy/line-separators",
15675
+ register(editor) {
15676
+ return editor.registerNodeTransform(gr, $replaceLineSeparatorWithLineBreak)
15677
+ }
15678
+ })
15679
+ }
15680
+ }
15681
+
15682
+ // Replaces the first separator only: the split leaves dirty text nodes behind, so Lexical re-runs
15683
+ // the transform until none remain.
15684
+ function $replaceLineSeparatorWithLineBreak(textNode) {
15685
+ const match = textNode.getTextContent().match(LINE_SEPARATORS);
15686
+
15687
+ if (match) {
15688
+ let separatorNode;
15689
+ if (match.index === 0) {
15690
+ [ separatorNode ] = textNode.splitText(match[0].length);
15691
+ } else {
15692
+ [ , separatorNode ] = textNode.splitText(match.index, match.index + match[0].length);
15693
+ }
15694
+ separatorNode.replace(or());
15695
+ }
15696
+ }
15697
+
15545
15698
  class LexicalEditorElement extends HTMLElement {
15546
15699
  static formAssociated = true
15547
15700
  static debug = false
@@ -15700,7 +15853,8 @@ class LexicalEditorElement extends HTMLElement {
15700
15853
  FormatEscapeExtension,
15701
15854
  LinkOpenerExtension,
15702
15855
  PreventLexicalTripleClickExtension,
15703
- CustomAttachmentDragAndDropExtension
15856
+ CustomAttachmentDragAndDropExtension,
15857
+ LineSeparatorsExtension
15704
15858
  ]
15705
15859
  }
15706
15860
 
@@ -15888,7 +16042,6 @@ class LexicalEditorElement extends HTMLElement {
15888
16042
  #initialize() {
15889
16043
  this.#registerComponents();
15890
16044
  this.#handleEnter();
15891
- this.#handleReplacementText();
15892
16045
  this.#registerFocusEvents();
15893
16046
  this.#registerHistoryEvents();
15894
16047
  this.#registerFileAcceptFilter();
@@ -16152,14 +16305,6 @@ class LexicalEditorElement extends HTMLElement {
16152
16305
  ));
16153
16306
  }
16154
16307
 
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
16308
  #registerFocusEvents() {
16164
16309
  this.#listeners.track(
16165
16310
  registerEventListener(this, "focusin", this.#handleFocusIn),
Binary file
Binary file