prosemirror-view 1.42.1 → 1.42.3

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.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,21 @@
1
+ ## 1.42.3 (2026-08-25)
2
+
3
+ ### Bug fixes
4
+
5
+ Don't set `contenteditable=false` on widgets that already have a `contenteditable` attribute.
6
+
7
+ Fix a bug that broke pasting when there was a default trusted type policy defined without a `createHTML` function.
8
+
9
+ Run attribute validators on attributes provided via slice context in clipboard content. This closes an XSS vulnerability (found by Pedro Paniago).
10
+
11
+ ## 1.42.2 (2026-07-24)
12
+
13
+ ### Bug fixes
14
+
15
+ Enable a workaround for backspace not taking effect on Android for Firefox as well as Chrome (for which it already kicked in).
16
+
17
+ Work around the situation where Chrome/Webkit replace a node's content DOM with a completely different element.
18
+
1
19
  ## 1.42.1 (2026-07-11)
2
20
 
3
21
  ### Bug fixes
package/dist/index.cjs CHANGED
@@ -687,7 +687,7 @@ var ViewDesc = function () {
687
687
  }
688
688
  }, {
689
689
  key: "parseRule",
690
- value: function parseRule() {
690
+ value: function parseRule(addedNodes) {
691
691
  return null;
692
692
  }
693
693
  }, {
@@ -1105,7 +1105,7 @@ var WidgetViewDesc = function (_ViewDesc) {
1105
1105
  wrap.appendChild(dom);
1106
1106
  dom = wrap;
1107
1107
  }
1108
- dom.contentEditable = "false";
1108
+ if (!dom.hasAttribute("contenteditable")) dom.contentEditable = "false";
1109
1109
  dom.classList.add("ProseMirror-widget");
1110
1110
  }
1111
1111
  _this = _super.call(this, parent, [], dom, null);
@@ -1285,7 +1285,7 @@ var NodeViewDesc = function (_ViewDesc4) {
1285
1285
  }
1286
1286
  _createClass(NodeViewDesc, [{
1287
1287
  key: "parseRule",
1288
- value: function parseRule() {
1288
+ value: function parseRule(addedNodes) {
1289
1289
  var _this5 = this;
1290
1290
  if (this.node.type.spec.reparseInView) return null;
1291
1291
  var rule = {
@@ -1307,9 +1307,14 @@ var NodeViewDesc = function (_ViewDesc4) {
1307
1307
  break;
1308
1308
  }
1309
1309
  }
1310
- if (!rule.contentElement) rule.getContent = function () {
1311
- return prosemirrorModel.Fragment.empty;
1312
- };
1310
+ if (!rule.contentElement) {
1311
+ var found = addedNodes && addedNodes.find(function (n) {
1312
+ return n.nodeType == 1 && addedNodes.indexOf(n.parentNode) < 0 && _this5.dom.contains(n);
1313
+ });
1314
+ if (found) rule.contentElement = found;else rule.getContent = function () {
1315
+ return prosemirrorModel.Fragment.empty;
1316
+ };
1317
+ }
1313
1318
  }
1314
1319
  return rule;
1315
1320
  }
@@ -2847,11 +2852,16 @@ var _policy = null;
2847
2852
  function maybeWrapTrusted(html) {
2848
2853
  var trustedTypes = window.trustedTypes;
2849
2854
  if (!trustedTypes) return html;
2850
- if (!_policy) _policy = trustedTypes.defaultPolicy || trustedTypes.createPolicy("ProseMirrorClipboard", {
2851
- createHTML: function createHTML(s) {
2852
- return s;
2853
- }
2854
- });
2855
+ if (!_policy) {
2856
+ if (_policy = trustedTypes.defaultPolicy) try {
2857
+ return _policy.createHTML(html);
2858
+ } catch (_a) {}
2859
+ _policy = trustedTypes.createPolicy("ProseMirrorClipboard", {
2860
+ createHTML: function createHTML(s) {
2861
+ return s;
2862
+ }
2863
+ });
2864
+ }
2855
2865
  return _policy.createHTML(html);
2856
2866
  }
2857
2867
  function readHTML(html) {
@@ -2902,6 +2912,11 @@ function addContext(slice, context) {
2902
2912
  for (var i = array.length - 2; i >= 0; i -= 2) {
2903
2913
  var type = schema.nodes[array[i]];
2904
2914
  if (!type || type.hasRequiredAttrs()) break;
2915
+ try {
2916
+ type.checkAttrs(array[i + 1]);
2917
+ } catch (e) {
2918
+ break;
2919
+ }
2905
2920
  content = prosemirrorModel.Fragment.from(type.create(array[i + 1], content));
2906
2921
  openStart++;
2907
2922
  openEnd++;
@@ -3669,7 +3684,7 @@ handlers.blur = function (view, _event) {
3669
3684
  };
3670
3685
  handlers.beforeinput = function (view, _event) {
3671
3686
  var event = _event;
3672
- if (chrome && android && event.inputType == "deleteContentBackward") {
3687
+ if (android && event.inputType == "deleteContentBackward") {
3673
3688
  view.domObserver.flushSoon();
3674
3689
  var domChangeCount = view.input.domChangeCount;
3675
3690
  setTimeout(function () {
@@ -4706,7 +4721,7 @@ function fixUpBadSafariComposition(view, addedNodes) {
4706
4721
  _iterator5.f();
4707
4722
  }
4708
4723
  }
4709
- function parseBetween(view, from_, to_) {
4724
+ function parseBetween(view, from_, to_, addedNodes) {
4710
4725
  var _view$docView$parseRa = view.docView.parseRange(from_, to_),
4711
4726
  parent = _view$docView$parseRa.node,
4712
4727
  fromOffset = _view$docView$parseRa.fromOffset,
@@ -4749,7 +4764,7 @@ function parseBetween(view, from_, to_) {
4749
4764
  to: toOffset,
4750
4765
  preserveWhitespace: $from.parent.type.whitespace == "pre" ? "full" : true,
4751
4766
  findPositions: find,
4752
- ruleFromNode: ruleFromNode,
4767
+ ruleFromNode: ruleFromNode(addedNodes),
4753
4768
  context: $from
4754
4769
  });
4755
4770
  if (find && find[0].pos != null) {
@@ -4768,29 +4783,31 @@ function parseBetween(view, from_, to_) {
4768
4783
  to: to
4769
4784
  };
4770
4785
  }
4771
- function ruleFromNode(dom) {
4772
- var desc = dom.pmViewDesc;
4773
- if (desc) {
4774
- return desc.parseRule();
4775
- } else if (dom.nodeName == "BR" && dom.parentNode) {
4776
- if (safari && /^(ul|ol)$/i.test(dom.parentNode.nodeName)) {
4777
- var skip = document.createElement("div");
4778
- skip.appendChild(document.createElement("li"));
4779
- return {
4780
- skip: skip
4781
- };
4782
- } else if (dom.parentNode.lastChild == dom || safari && /^(tr|table)$/i.test(dom.parentNode.nodeName)) {
4786
+ var ruleFromNode = function ruleFromNode(added) {
4787
+ return function (dom) {
4788
+ var desc = dom.pmViewDesc;
4789
+ if (desc) {
4790
+ return desc.parseRule(added);
4791
+ } else if (dom.nodeName == "BR" && dom.parentNode) {
4792
+ if (safari && /^(ul|ol)$/i.test(dom.parentNode.nodeName)) {
4793
+ var skip = document.createElement("div");
4794
+ skip.appendChild(document.createElement("li"));
4795
+ return {
4796
+ skip: skip
4797
+ };
4798
+ } else if (dom.parentNode.lastChild == dom || safari && /^(tr|table)$/i.test(dom.parentNode.nodeName)) {
4799
+ return {
4800
+ ignore: true
4801
+ };
4802
+ }
4803
+ } else if (dom.nodeName == "IMG" && dom.getAttribute("mark-placeholder")) {
4783
4804
  return {
4784
4805
  ignore: true
4785
4806
  };
4786
4807
  }
4787
- } else if (dom.nodeName == "IMG" && dom.getAttribute("mark-placeholder")) {
4788
- return {
4789
- ignore: true
4790
- };
4791
- }
4792
- return null;
4793
- }
4808
+ return null;
4809
+ };
4810
+ };
4794
4811
  var isInline = /^(a|abbr|acronym|b|bd[io]|big|br|button|cite|code|data(list)?|del|dfn|em|i|img|ins|kbd|label|map|mark|meter|output|q|ruby|s|samp|small|span|strong|su[bp]|time|u|tt|var)$/i;
4795
4812
  function readDOMChange(view, from, to, typeOver, addedNodes) {
4796
4813
  var compositionID = view.input.compositionPendingChanges || (view.composing ? view.input.compositionID : 0);
@@ -4814,7 +4831,7 @@ function readDOMChange(view, from, to, typeOver, addedNodes) {
4814
4831
  from = $before.before(shared + 1);
4815
4832
  to = view.state.doc.resolve(to).after(shared + 1);
4816
4833
  var sel = view.state.selection;
4817
- var parse = parseBetween(view, from, to);
4834
+ var parse = parseBetween(view, from, to, addedNodes);
4818
4835
  var doc = view.state.doc,
4819
4836
  compare = doc.slice(parse.from, parse.to);
4820
4837
  var preferredPos, preferredSide;
package/dist/index.js CHANGED
@@ -764,7 +764,7 @@ class ViewDesc {
764
764
  // When parsing in-editor content (in domchange.js), we allow
765
765
  // descriptions to determine the parse rules that should be used to
766
766
  // parse them.
767
- parseRule() { return null; }
767
+ parseRule(addedNodes) { return null; }
768
768
  // Used by the editor's event handler to ignore events that come
769
769
  // from certain descs.
770
770
  stopEvent(event) { return false; }
@@ -1169,7 +1169,8 @@ class WidgetViewDesc extends ViewDesc {
1169
1169
  wrap.appendChild(dom);
1170
1170
  dom = wrap;
1171
1171
  }
1172
- dom.contentEditable = "false";
1172
+ if (!dom.hasAttribute("contenteditable"))
1173
+ dom.contentEditable = "false";
1173
1174
  dom.classList.add("ProseMirror-widget");
1174
1175
  }
1175
1176
  super(parent, [], dom, null);
@@ -1328,7 +1329,7 @@ class NodeViewDesc extends ViewDesc {
1328
1329
  else
1329
1330
  return new NodeViewDesc(parent, node, outerDeco, innerDeco, dom, contentDOM || null, nodeDOM);
1330
1331
  }
1331
- parseRule() {
1332
+ parseRule(addedNodes) {
1332
1333
  // Experimental kludge to allow opt-in re-parsing of nodes
1333
1334
  if (this.node.type.spec.reparseInView)
1334
1335
  return null;
@@ -1356,8 +1357,14 @@ class NodeViewDesc extends ViewDesc {
1356
1357
  break;
1357
1358
  }
1358
1359
  }
1359
- if (!rule.contentElement)
1360
- rule.getContent = () => Fragment.empty;
1360
+ if (!rule.contentElement) {
1361
+ let found = addedNodes &&
1362
+ addedNodes.find(n => n.nodeType == 1 && addedNodes.indexOf(n.parentNode) < 0 && this.dom.contains(n));
1363
+ if (found)
1364
+ rule.contentElement = found;
1365
+ else
1366
+ rule.getContent = () => Fragment.empty;
1367
+ }
1361
1368
  }
1362
1369
  return rule;
1363
1370
  }
@@ -3011,8 +3018,14 @@ function maybeWrapTrusted(html) {
3011
3018
  // With the require-trusted-types-for CSP, Chrome will block
3012
3019
  // innerHTML, even on a detached document. This wraps the string in
3013
3020
  // a way that makes the browser allow us to use its parser again.
3014
- if (!_policy)
3015
- _policy = trustedTypes.defaultPolicy || trustedTypes.createPolicy("ProseMirrorClipboard", { createHTML: (s) => s });
3021
+ if (!_policy) {
3022
+ if (_policy = trustedTypes.defaultPolicy)
3023
+ try {
3024
+ return _policy.createHTML(html);
3025
+ }
3026
+ catch (_a) { }
3027
+ _policy = trustedTypes.createPolicy("ProseMirrorClipboard", { createHTML: (s) => s });
3028
+ }
3016
3029
  return _policy.createHTML(html);
3017
3030
  }
3018
3031
  function readHTML(html) {
@@ -3069,6 +3082,12 @@ function addContext(slice, context) {
3069
3082
  let type = schema.nodes[array[i]];
3070
3083
  if (!type || type.hasRequiredAttrs())
3071
3084
  break;
3085
+ try {
3086
+ type.checkAttrs(array[i + 1]);
3087
+ }
3088
+ catch (e) {
3089
+ break;
3090
+ }
3072
3091
  content = Fragment.from(type.create(array[i + 1], content));
3073
3092
  openStart++;
3074
3093
  openEnd++;
@@ -3911,8 +3930,8 @@ handlers.beforeinput = (view, _event) => {
3911
3930
  // We should probably do more with beforeinput events, but support
3912
3931
  // is so spotty that I'm still waiting to see where they are going.
3913
3932
  // Very specific hack to deal with backspace sometimes failing on
3914
- // Chrome Android when after an uneditable node.
3915
- if (chrome && android && event.inputType == "deleteContentBackward") {
3933
+ // Chrome and Firefox Android when after an uneditable node.
3934
+ if (android && event.inputType == "deleteContentBackward") {
3916
3935
  view.domObserver.flushSoon();
3917
3936
  let { domChangeCount } = view.input;
3918
3937
  setTimeout(() => {
@@ -5016,7 +5035,7 @@ function fixUpBadSafariComposition(view, addedNodes) {
5016
5035
  // that the DOM represents. If any changes came in in the meantime,
5017
5036
  // the modification is mapped over those before it is applied, in
5018
5037
  // readDOMChange.
5019
- function parseBetween(view, from_, to_) {
5038
+ function parseBetween(view, from_, to_, addedNodes) {
5020
5039
  let { node: parent, fromOffset, toOffset, from, to } = view.docView.parseRange(from_, to_);
5021
5040
  let domSel = view.domSelectionRange();
5022
5041
  let find;
@@ -5050,7 +5069,7 @@ function parseBetween(view, from_, to_) {
5050
5069
  to: toOffset,
5051
5070
  preserveWhitespace: $from.parent.type.whitespace == "pre" ? "full" : true,
5052
5071
  findPositions: find,
5053
- ruleFromNode,
5072
+ ruleFromNode: ruleFromNode(addedNodes),
5054
5073
  context: $from
5055
5074
  });
5056
5075
  if (find && find[0].pos != null) {
@@ -5061,10 +5080,10 @@ function parseBetween(view, from_, to_) {
5061
5080
  }
5062
5081
  return { doc, sel, from, to };
5063
5082
  }
5064
- function ruleFromNode(dom) {
5083
+ const ruleFromNode = (added) => (dom) => {
5065
5084
  let desc = dom.pmViewDesc;
5066
5085
  if (desc) {
5067
- return desc.parseRule();
5086
+ return desc.parseRule(added);
5068
5087
  }
5069
5088
  else if (dom.nodeName == "BR" && dom.parentNode) {
5070
5089
  // Safari replaces the list item or table cell with a BR
@@ -5083,7 +5102,7 @@ function ruleFromNode(dom) {
5083
5102
  return { ignore: true };
5084
5103
  }
5085
5104
  return null;
5086
- }
5105
+ };
5087
5106
  const isInline = /^(a|abbr|acronym|b|bd[io]|big|br|button|cite|code|data(list)?|del|dfn|em|i|img|ins|kbd|label|map|mark|meter|output|q|ruby|s|samp|small|span|strong|su[bp]|time|u|tt|var)$/i;
5088
5107
  function readDOMChange(view, from, to, typeOver, addedNodes) {
5089
5108
  let compositionID = view.input.compositionPendingChanges || (view.composing ? view.input.compositionID : 0);
@@ -5112,7 +5131,7 @@ function readDOMChange(view, from, to, typeOver, addedNodes) {
5112
5131
  from = $before.before(shared + 1);
5113
5132
  to = view.state.doc.resolve(to).after(shared + 1);
5114
5133
  let sel = view.state.selection;
5115
- let parse = parseBetween(view, from, to);
5134
+ let parse = parseBetween(view, from, to, addedNodes);
5116
5135
  let doc = view.state.doc, compare = doc.slice(parse.from, parse.to);
5117
5136
  let preferredPos, preferredSide;
5118
5137
  // Prefer anchoring to end when Backspace is pressed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prosemirror-view",
3
- "version": "1.42.1",
3
+ "version": "1.42.3",
4
4
  "description": "ProseMirror's view component",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",
package/src/clipboard.ts CHANGED
@@ -215,8 +215,11 @@ function maybeWrapTrusted(html: string): string {
215
215
  // With the require-trusted-types-for CSP, Chrome will block
216
216
  // innerHTML, even on a detached document. This wraps the string in
217
217
  // a way that makes the browser allow us to use its parser again.
218
- if (!_policy)
219
- _policy = trustedTypes.defaultPolicy || trustedTypes.createPolicy("ProseMirrorClipboard", {createHTML: (s: string) => s})
218
+ if (!_policy) {
219
+ if (_policy = trustedTypes.defaultPolicy)
220
+ try { return _policy.createHTML(html) } catch {}
221
+ _policy = trustedTypes.createPolicy("ProseMirrorClipboard", {createHTML: (s: string) => s})
222
+ }
220
223
  return _policy.createHTML(html)
221
224
  }
222
225
 
@@ -267,6 +270,8 @@ function addContext(slice: Slice, context: string) {
267
270
  for (let i = array.length - 2; i >= 0; i -= 2) {
268
271
  let type = schema.nodes[array[i]]
269
272
  if (!type || type.hasRequiredAttrs()) break
273
+ try { type.checkAttrs(array[i + 1]) }
274
+ catch(e) { break }
270
275
  content = Fragment.from(type.create(array[i + 1], content))
271
276
  openStart++; openEnd++
272
277
  }
package/src/domchange.ts CHANGED
@@ -12,7 +12,7 @@ import {EditorView} from "./index"
12
12
  // the modification is mapped over those before it is applied, in
13
13
  // readDOMChange.
14
14
 
15
- function parseBetween(view: EditorView, from_: number, to_: number) {
15
+ function parseBetween(view: EditorView, from_: number, to_: number, addedNodes: readonly DOMNode[]) {
16
16
  let {node: parent, fromOffset, toOffset, from, to} = view.docView.parseRange(from_, to_)
17
17
 
18
18
  let domSel = view.domSelectionRange()
@@ -44,7 +44,7 @@ function parseBetween(view: EditorView, from_: number, to_: number) {
44
44
  to: toOffset,
45
45
  preserveWhitespace: $from.parent.type.whitespace == "pre" ? "full" : true,
46
46
  findPositions: find,
47
- ruleFromNode,
47
+ ruleFromNode: ruleFromNode(addedNodes),
48
48
  context: $from
49
49
  })
50
50
  if (find && find[0].pos != null) {
@@ -55,10 +55,10 @@ function parseBetween(view: EditorView, from_: number, to_: number) {
55
55
  return {doc, sel, from, to}
56
56
  }
57
57
 
58
- function ruleFromNode(dom: DOMNode): Omit<TagParseRule, "tag"> | null {
58
+ const ruleFromNode = (added: readonly DOMNode[]) => (dom: DOMNode): Omit<TagParseRule, "tag"> | null => {
59
59
  let desc = dom.pmViewDesc
60
60
  if (desc) {
61
- return desc.parseRule()
61
+ return desc.parseRule(added)
62
62
  } else if (dom.nodeName == "BR" && dom.parentNode) {
63
63
  // Safari replaces the list item or table cell with a BR
64
64
  // directly in the list node (?!) if you delete the last
@@ -105,7 +105,7 @@ export function readDOMChange(view: EditorView, from: number, to: number, typeOv
105
105
  to = view.state.doc.resolve(to).after(shared + 1)
106
106
 
107
107
  let sel = view.state.selection
108
- let parse = parseBetween(view, from, to)
108
+ let parse = parseBetween(view, from, to, addedNodes)
109
109
 
110
110
  let doc = view.state.doc, compare = doc.slice(parse.from, parse.to)
111
111
  let preferredPos, preferredSide: "start" | "end"
package/src/input.ts CHANGED
@@ -864,8 +864,8 @@ handlers.beforeinput = (view, _event: Event) => {
864
864
  // is so spotty that I'm still waiting to see where they are going.
865
865
 
866
866
  // Very specific hack to deal with backspace sometimes failing on
867
- // Chrome Android when after an uneditable node.
868
- if (browser.chrome && browser.android && event.inputType == "deleteContentBackward") {
867
+ // Chrome and Firefox Android when after an uneditable node.
868
+ if (browser.android && event.inputType == "deleteContentBackward") {
869
869
  view.domObserver.flushSoon()
870
870
  let {domChangeCount} = view.input
871
871
  setTimeout(() => {
package/src/viewdesc.ts CHANGED
@@ -167,7 +167,7 @@ export class ViewDesc {
167
167
  // When parsing in-editor content (in domchange.js), we allow
168
168
  // descriptions to determine the parse rules that should be used to
169
169
  // parse them.
170
- parseRule(): Omit<TagParseRule, "tag"> | null { return null }
170
+ parseRule(addedNodes?: readonly DOMNode[]): Omit<TagParseRule, "tag"> | null { return null }
171
171
 
172
172
  // Used by the editor's event handler to ignore events that come
173
173
  // from certain descs.
@@ -555,7 +555,8 @@ class WidgetViewDesc extends ViewDesc {
555
555
  wrap.appendChild(dom)
556
556
  dom = wrap
557
557
  }
558
- ;(dom as HTMLElement).contentEditable = "false"
558
+ if (!(dom as HTMLElement).hasAttribute("contenteditable"))
559
+ (dom as HTMLElement).contentEditable = "false"
559
560
  ;(dom as HTMLElement).classList.add("ProseMirror-widget")
560
561
  }
561
562
  super(parent, [], dom, null)
@@ -727,7 +728,7 @@ export class NodeViewDesc extends ViewDesc {
727
728
  return new NodeViewDesc(parent, node, outerDeco, innerDeco, dom, contentDOM || null, nodeDOM)
728
729
  }
729
730
 
730
- parseRule() {
731
+ parseRule(addedNodes?: readonly DOMNode[]) {
731
732
  // Experimental kludge to allow opt-in re-parsing of nodes
732
733
  if (this.node.type.spec.reparseInView) return null
733
734
  // FIXME the assumption that this can always return the current
@@ -751,7 +752,14 @@ export class NodeViewDesc extends ViewDesc {
751
752
  break
752
753
  }
753
754
  }
754
- if (!rule.contentElement) rule.getContent = () => Fragment.empty
755
+ if (!rule.contentElement) {
756
+ let found = addedNodes &&
757
+ addedNodes.find(n => n.nodeType == 1 && addedNodes.indexOf(n.parentNode!) < 0 && this.dom.contains(n))
758
+ if (found)
759
+ rule.contentElement = found as HTMLElement
760
+ else
761
+ rule.getContent = () => Fragment.empty
762
+ }
755
763
  }
756
764
  return rule
757
765
  }
@@ -1,275 +0,0 @@
1
- import {Slice, Fragment, DOMParser, DOMSerializer, ResolvedPos, NodeType, Node} from "prosemirror-model"
2
- import * as browser from "./browser"
3
- import {EditorView} from "./index"
4
-
5
- export function serializeForClipboard(view: EditorView, slice: Slice) {
6
- view.someProp("transformCopied", f => { slice = f(slice!, view) })
7
-
8
- let context = [], {content, openStart, openEnd} = slice
9
- while (openStart > 1 && openEnd > 1 && content.childCount == 1 && content.firstChild!.childCount == 1) {
10
- openStart--
11
- openEnd--
12
- let node = content.firstChild!
13
- context.push(node.type.name, node.attrs != node.type.defaultAttrs ? node.attrs : null)
14
- content = node.content
15
- }
16
-
17
- let serializer = view.someProp("clipboardSerializer") || DOMSerializer.fromSchema(view.state.schema)
18
- let doc = detachedDoc(), wrap = doc.createElement("div")
19
- wrap.appendChild(serializer.serializeFragment(content, {document: doc}))
20
-
21
- let firstChild = wrap.firstChild, needsWrap, wrappers = 0
22
- while (firstChild && firstChild.nodeType == 1 && (needsWrap = wrapMap[firstChild.nodeName.toLowerCase()])) {
23
- for (let i = needsWrap.length - 1; i >= 0; i--) {
24
- let wrapper = doc.createElement(needsWrap[i])
25
- while (wrap.firstChild) wrapper.appendChild(wrap.firstChild)
26
- wrap.appendChild(wrapper)
27
- wrappers++
28
- }
29
- firstChild = wrap.firstChild
30
- }
31
-
32
- if (firstChild && firstChild.nodeType == 1)
33
- (firstChild as HTMLElement).setAttribute(
34
- "data-pm-slice", `${openStart} ${openEnd}${wrappers ? ` -${wrappers}` : ""} ${JSON.stringify(context)}`)
35
-
36
- let text = view.someProp("clipboardTextSerializer", f => f(slice, view)) ||
37
- slice.content.textBetween(0, slice.content.size, "\n\n")
38
-
39
- return {dom: wrap, text, slice}
40
- }
41
-
42
- // Read a slice of content from the clipboard (or drop data).
43
- export function parseFromClipboard(view: EditorView, text: string, html: string | null, plainText: boolean, $context: ResolvedPos) {
44
- let inCode = $context.parent.type.spec.code
45
- let dom: HTMLElement | undefined, slice: Slice | undefined
46
- if (!html && !text) return null
47
- let asText = !!text && (plainText || inCode || !html)
48
- if (asText) {
49
- view.someProp("transformPastedText", f => { text = f(text, inCode || plainText, view) })
50
- if (inCode) {
51
- slice = new Slice(Fragment.from(view.state.schema.text(text.replace(/\r\n?/g, "\n"))), 0, 0)
52
- view.someProp("transformPasted", f => { slice = f(slice!, view, true) })
53
- return slice
54
- }
55
- let parsed = view.someProp("clipboardTextParser", f => f(text, $context, plainText, view))
56
- if (parsed) {
57
- slice = parsed
58
- } else {
59
- let marks = $context.marks()
60
- let {schema} = view.state, serializer = DOMSerializer.fromSchema(schema)
61
- dom = document.createElement("div")
62
- text.split(/(?:\r\n?|\n)+/).forEach(block => {
63
- let p = dom!.appendChild(document.createElement("p"))
64
- if (block) p.appendChild(serializer.serializeNode(schema.text(block, marks)))
65
- })
66
- }
67
- } else {
68
- view.someProp("transformPastedHTML", f => { html = f(html!, view) })
69
- dom = readHTML(html!)
70
- if (browser.webkit) restoreReplacedSpaces(dom)
71
- }
72
-
73
- let contextNode = dom && dom.querySelector("[data-pm-slice]")
74
- let sliceData = contextNode && /^(\d+) (\d+)(?: -(\d+))? (.*)/.exec(contextNode.getAttribute("data-pm-slice") || "")
75
- if (sliceData && sliceData[3]) for (let i = +sliceData[3]; i > 0; i--) {
76
- let child = dom!.firstChild
77
- while (child && child.nodeType != 1) child = child.nextSibling
78
- if (!child) break
79
- dom = child as HTMLElement
80
- }
81
-
82
- if (!slice) {
83
- let parser = view.someProp("clipboardParser") || view.someProp("domParser") || DOMParser.fromSchema(view.state.schema)
84
- slice = parser.parseSlice(dom!, {
85
- preserveWhitespace: !!(asText || sliceData),
86
- context: $context,
87
- ruleFromNode(dom) {
88
- if (dom.nodeName == "BR" && !dom.nextSibling &&
89
- dom.parentNode && !inlineParents.test(dom.parentNode.nodeName)) return {ignore: true}
90
- return null
91
- }
92
- })
93
- }
94
- if (sliceData) {
95
- slice = addContext(closeSlice(slice, +sliceData[1], +sliceData[2]), sliceData[4])
96
- } else { // HTML wasn't created by ProseMirror. Make sure top-level siblings are coherent
97
- slice = Slice.maxOpen(normalizeSiblings(slice.content, $context), true)
98
- if (slice.openStart || slice.openEnd) {
99
- let openStart = 0, openEnd = 0
100
- for (let node = slice.content.firstChild; openStart < slice.openStart && !node!.type.spec.isolating;
101
- openStart++, node = node!.firstChild) {}
102
- for (let node = slice.content.lastChild; openEnd < slice.openEnd && !node!.type.spec.isolating;
103
- openEnd++, node = node!.lastChild) {}
104
- slice = closeSlice(slice, openStart, openEnd)
105
- }
106
- }
107
-
108
- view.someProp("transformPasted", f => { slice = f(slice!, view, asText) })
109
- return slice
110
- }
111
-
112
- const inlineParents = /^(a|abbr|acronym|b|cite|code|del|em|i|ins|kbd|label|output|q|ruby|s|samp|span|strong|sub|sup|time|u|tt|var)$/i
113
-
114
- // Takes a slice parsed with parseSlice, which means there hasn't been
115
- // any content-expression checking done on the top nodes, tries to
116
- // find a parent node in the current context that might fit the nodes,
117
- // and if successful, rebuilds the slice so that it fits into that parent.
118
- //
119
- // This addresses the problem that Transform.replace expects a
120
- // coherent slice, and will fail to place a set of siblings that don't
121
- // fit anywhere in the schema.
122
- function normalizeSiblings(fragment: Fragment, $context: ResolvedPos) {
123
- if (fragment.childCount < 2) return fragment
124
- for (let d = $context.depth; d >= 0; d--) {
125
- let parent = $context.node(d)
126
- let match = parent.contentMatchAt($context.index(d))
127
- let lastWrap: readonly NodeType[] | undefined, result: Node[] | null = []
128
- fragment.forEach(node => {
129
- if (!result) return
130
- let wrap = match.findWrapping(node.type), inLast
131
- if (!wrap) return result = null
132
- if (inLast = result.length && lastWrap!.length && addToSibling(wrap, lastWrap!, node, result[result.length - 1], 0)) {
133
- result[result.length - 1] = inLast
134
- } else {
135
- if (result.length) result[result.length - 1] = closeRight(result[result.length - 1], lastWrap!.length)
136
- let wrapped = withWrappers(node, wrap)
137
- result.push(wrapped)
138
- match = match.matchType(wrapped.type)!
139
- lastWrap = wrap
140
- }
141
- })
142
- if (result) return Fragment.from(result)
143
- }
144
- return fragment
145
- }
146
-
147
- function withWrappers(node: Node, wrap: readonly NodeType[], from = 0) {
148
- for (let i = wrap.length - 1; i >= from; i--)
149
- node = wrap[i].create(null, Fragment.from(node))
150
- return node
151
- }
152
-
153
- // Used to group adjacent nodes wrapped in similar parents by
154
- // normalizeSiblings into the same parent node
155
- function addToSibling(wrap: readonly NodeType[], lastWrap: readonly NodeType[],
156
- node: Node, sibling: Node, depth: number): Node | undefined {
157
- if (depth < wrap.length && depth < lastWrap.length && wrap[depth] == lastWrap[depth]) {
158
- let inner = addToSibling(wrap, lastWrap, node, sibling.lastChild!, depth + 1)
159
- if (inner) return sibling.copy(sibling.content.replaceChild(sibling.childCount - 1, inner))
160
- let match = sibling.contentMatchAt(sibling.childCount)
161
- if (match.matchType(depth == wrap.length - 1 ? node.type : wrap[depth + 1]))
162
- return sibling.copy(sibling.content.append(Fragment.from(withWrappers(node, wrap, depth + 1))))
163
- }
164
- }
165
-
166
- function closeRight(node: Node, depth: number) {
167
- if (depth == 0) return node
168
- let fragment = node.content.replaceChild(node.childCount - 1, closeRight(node.lastChild!, depth - 1))
169
- let fill = node.contentMatchAt(node.childCount).fillBefore(Fragment.empty, true)!
170
- return node.copy(fragment.append(fill))
171
- }
172
-
173
- function closeRange(fragment: Fragment, side: number, from: number, to: number, depth: number, openEnd: number) {
174
- let node = side < 0 ? fragment.firstChild! : fragment.lastChild!, inner = node.content
175
- if (fragment.childCount > 1) openEnd = 0
176
- if (depth < to - 1) inner = closeRange(inner, side, from, to, depth + 1, openEnd)
177
- if (depth >= from)
178
- inner = side < 0 ? node.contentMatchAt(0)!.fillBefore(inner, openEnd <= depth)!.append(inner)
179
- : inner.append(node.contentMatchAt(node.childCount)!.fillBefore(Fragment.empty, true)!)
180
- return fragment.replaceChild(side < 0 ? 0 : fragment.childCount - 1, node.copy(inner))
181
- }
182
-
183
- function closeSlice(slice: Slice, openStart: number, openEnd: number) {
184
- if (openStart < slice.openStart)
185
- slice = new Slice(closeRange(slice.content, -1, openStart, slice.openStart, 0, slice.openEnd), openStart, slice.openEnd)
186
- if (openEnd < slice.openEnd)
187
- slice = new Slice(closeRange(slice.content, 1, openEnd, slice.openEnd, 0, 0), slice.openStart, openEnd)
188
- return slice
189
- }
190
-
191
- // Trick from jQuery -- some elements must be wrapped in other
192
- // elements for innerHTML to work. I.e. if you do `div.innerHTML =
193
- // "<td>..</td>"` the table cells are ignored.
194
- const wrapMap: {[node: string]: string[]} = {
195
- thead: ["table"],
196
- tbody: ["table"],
197
- tfoot: ["table"],
198
- caption: ["table"],
199
- colgroup: ["table"],
200
- col: ["table", "colgroup"],
201
- tr: ["table", "tbody"],
202
- td: ["table", "tbody", "tr"],
203
- th: ["table", "tbody", "tr"]
204
- }
205
-
206
- function detachedDoc() {
207
- return document.implementation.createHTMLDocument("title")
208
- }
209
-
210
- let _policy: any = null
211
-
212
- function maybeWrapTrusted(html: string): string {
213
- let trustedTypes = (window as any).trustedTypes
214
- if (!trustedTypes) return html
215
- // With the require-trusted-types-for CSP, Chrome will block
216
- // innerHTML, even on a detached document. This wraps the string in
217
- // a way that makes the browser allow us to use its parser again.
218
- if (!_policy)
219
- _policy = trustedTypes.defaultPolicy || trustedTypes.createPolicy("ProseMirrorClipboard", {createHTML: (s: string) => s})
220
- return _policy.createHTML(html)
221
- }
222
-
223
- function readHTML(html: string) {
224
- html = "\n\n" + html + "\n\n"
225
- let metas = /^(\s*<meta [^>]*>)*/.exec(html)
226
- if (metas) html = html.slice(metas[0].length)
227
- let doc = detachedDoc(), elt = doc.createElement("div")
228
- let firstTag = /<([a-z][^>\s]+)/i.exec(html), wrap
229
- if (wrap = firstTag && wrapMap[firstTag[1].toLowerCase()])
230
- html = wrap.map(n => "<" + n + ">").join("") + html + wrap.map(n => "</" + n + ">").reverse().join("")
231
- elt.innerHTML = maybeWrapTrusted(html)
232
- if (wrap) for (let i = 0; i < wrap.length; i++) elt = elt.querySelector(wrap[i]) || elt
233
- // Inline styles defined in the pasted content, so that parse rules pick them up
234
- for (let i = 0; i < doc.styleSheets.length; i++) {
235
- let style = doc.styleSheets[i]
236
- for (let j = 0; j < style.rules.length; j++) {
237
- let rule = style.rules[j]
238
- if (rule instanceof CSSStyleRule) {
239
- let matches = elt.querySelectorAll(rule.selectorText)
240
- for (let k = 0; k < matches.length; k++)
241
- (matches[k] as HTMLElement).style.cssText += rule.style.cssText
242
- }
243
- }
244
- }
245
- return elt
246
- }
247
-
248
- // Webkit browsers do some hard-to-predict replacement of regular
249
- // spaces with non-breaking spaces when putting content on the
250
- // clipboard. This tries to convert such non-breaking spaces (which
251
- // will be wrapped in a plain span on Chrome, a span with class
252
- // Apple-converted-space on Safari) back to regular spaces.
253
- function restoreReplacedSpaces(dom: HTMLElement) {
254
- let nodes = dom.querySelectorAll(browser.chrome ? "span:not([class]):not([style])" : "span.Apple-converted-space")
255
- for (let i = 0; i < nodes.length; i++) {
256
- let node = nodes[i]
257
- if (node.childNodes.length == 1 && node.textContent == "\u00a0" && node.parentNode)
258
- node.parentNode.replaceChild(dom.ownerDocument.createTextNode(" "), node)
259
- }
260
- }
261
-
262
- function addContext(slice: Slice, context: string) {
263
- if (!slice.size) return slice
264
- let schema = slice.content.firstChild!.type.schema, array
265
- try { array = JSON.parse(context) }
266
- catch(e) { return slice }
267
- let {content, openStart, openEnd} = slice
268
- for (let i = array.length - 2; i >= 0; i -= 2) {
269
- let type = schema.nodes[array[i]]
270
- if (!type || type.hasRequiredAttrs()) break
271
- content = Fragment.from(type.create(array[i + 1], content))
272
- openStart++; openEnd++
273
- }
274
- return new Slice(content, openStart, openEnd)
275
- }