@lexical/clipboard 0.2.4 → 0.2.5
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/LexicalClipboard.dev.js +182 -31
- package/LexicalClipboard.prod.js +13 -8
- package/package.json +4 -4
package/LexicalClipboard.dev.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
'use strict';
|
|
8
8
|
|
|
9
9
|
var selection = require('@lexical/selection');
|
|
10
|
+
var utils = require('@lexical/utils');
|
|
10
11
|
var lexical = require('lexical');
|
|
11
12
|
|
|
12
13
|
/**
|
|
@@ -37,39 +38,31 @@ function $convertSelectedLexicalNodeToHTMLElement(editor, selection, node) {
|
|
|
37
38
|
let nodeToConvert = node;
|
|
38
39
|
|
|
39
40
|
if (lexical.$isRangeSelection(selection) || lexical.$isGridSelection(selection)) {
|
|
40
|
-
const
|
|
41
|
-
const
|
|
42
|
-
const isAnchor = node.is(
|
|
43
|
-
const isFocus = node.is(
|
|
41
|
+
const anchorNode = selection.anchor.getNode();
|
|
42
|
+
const focusNode = selection.focus.getNode();
|
|
43
|
+
const isAnchor = node.is(anchorNode);
|
|
44
|
+
const isFocus = node.is(focusNode);
|
|
44
45
|
|
|
45
46
|
if (lexical.$isTextNode(node) && (isAnchor || isFocus)) {
|
|
46
|
-
const anchorOffset = selection.
|
|
47
|
-
const focusOffset = selection.focus.getCharacterOffset();
|
|
47
|
+
const [anchorOffset, focusOffset] = selection.getCharacterOffsets();
|
|
48
48
|
const isBackward = selection.isBackward();
|
|
49
|
-
const isSame =
|
|
50
|
-
const isFirst = node.is(isBackward ?
|
|
51
|
-
const
|
|
52
|
-
const nodeTextLength = nodeText.length;
|
|
49
|
+
const isSame = anchorNode.is(focusNode);
|
|
50
|
+
const isFirst = node.is(isBackward ? focusNode : anchorNode);
|
|
51
|
+
const isLast = node.is(isBackward ? anchorNode : focusNode);
|
|
53
52
|
|
|
54
53
|
if (isSame) {
|
|
55
54
|
const startOffset = anchorOffset > focusOffset ? focusOffset : anchorOffset;
|
|
56
55
|
const endOffset = anchorOffset > focusOffset ? anchorOffset : focusOffset;
|
|
57
56
|
const splitNodes = node.splitText(startOffset, endOffset);
|
|
58
57
|
nodeToConvert = startOffset === 0 ? splitNodes[0] : splitNodes[1];
|
|
59
|
-
} else {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
if (!isBackward && endOffset === 0) {
|
|
69
|
-
return null;
|
|
70
|
-
} else if (endOffset !== nodeTextLength) {
|
|
71
|
-
nodeToConvert = node.splitText(endOffset)[isFirst && endOffset !== 0 ? 1 : 0];
|
|
72
|
-
}
|
|
58
|
+
} else if (isFirst) {
|
|
59
|
+
const offset = isBackward ? focusOffset : anchorOffset;
|
|
60
|
+
const splitNodes = node.splitText(offset);
|
|
61
|
+
nodeToConvert = offset === 0 ? splitNodes[0] : splitNodes[1];
|
|
62
|
+
} else if (isLast) {
|
|
63
|
+
const offset = isBackward ? anchorOffset : focusOffset;
|
|
64
|
+
const splitNodes = node.splitText(offset);
|
|
65
|
+
nodeToConvert = splitNodes[0];
|
|
73
66
|
}
|
|
74
67
|
}
|
|
75
68
|
}
|
|
@@ -99,9 +92,26 @@ function $convertSelectedLexicalContentToHtml(editor, selection, state) {
|
|
|
99
92
|
const nodeKey = state.range[i];
|
|
100
93
|
const node = lexical.$getNodeByKey(nodeKey);
|
|
101
94
|
|
|
102
|
-
if (node
|
|
95
|
+
if (node) {
|
|
103
96
|
const element = $convertSelectedLexicalNodeToHTMLElement(editor, selection, node);
|
|
104
|
-
|
|
97
|
+
|
|
98
|
+
if (element) {
|
|
99
|
+
// It might be the case that the node is an element node
|
|
100
|
+
// and we're not directly selecting it, but we are selecting
|
|
101
|
+
// some of its children. So we'll need to extract that out
|
|
102
|
+
// separately.
|
|
103
|
+
if (node.isSelected()) {
|
|
104
|
+
container.append(element);
|
|
105
|
+
} else {
|
|
106
|
+
let childNode = element.firstChild;
|
|
107
|
+
|
|
108
|
+
while (childNode != null) {
|
|
109
|
+
const nextSibling = childNode.nextSibling;
|
|
110
|
+
container.append(childNode);
|
|
111
|
+
childNode = nextSibling;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
105
115
|
}
|
|
106
116
|
}
|
|
107
117
|
|
|
@@ -130,6 +140,7 @@ function $insertDataTransferForPlainText(dataTransfer, selection) {
|
|
|
130
140
|
}
|
|
131
141
|
function $insertDataTransferForRichText(dataTransfer, selection, editor) {
|
|
132
142
|
const lexicalNodesString = dataTransfer.getData('application/x-lexical-editor');
|
|
143
|
+
const isSelectionInsideOfGrid = lexical.$isGridSelection(selection) || utils.$findMatchingParent(selection.anchor.getNode(), n => lexical.$isGridCellNode(n)) !== null && utils.$findMatchingParent(selection.focus.getNode(), n => lexical.$isGridCellNode(n)) !== null;
|
|
133
144
|
|
|
134
145
|
if (lexicalNodesString) {
|
|
135
146
|
const namespace = editor._config.namespace;
|
|
@@ -140,7 +151,13 @@ function $insertDataTransferForRichText(dataTransfer, selection, editor) {
|
|
|
140
151
|
if (lexicalClipboardData.namespace === namespace) {
|
|
141
152
|
const nodeRange = lexicalClipboardData.state;
|
|
142
153
|
const nodes = $generateNodes(nodeRange);
|
|
143
|
-
|
|
154
|
+
|
|
155
|
+
if (isSelectionInsideOfGrid && nodes.length === 1 && lexical.$isGridNode(nodes[0])) {
|
|
156
|
+
$mergeGridNodesStrategy(nodes, selection, false, editor);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
$basicInsertStrategy(nodes, selection, true);
|
|
144
161
|
return;
|
|
145
162
|
}
|
|
146
163
|
} catch (e) {// Malformed, missing nodes..
|
|
@@ -153,8 +170,25 @@ function $insertDataTransferForRichText(dataTransfer, selection, editor) {
|
|
|
153
170
|
if (htmlString) {
|
|
154
171
|
const parser = new DOMParser();
|
|
155
172
|
const dom = parser.parseFromString(htmlString, textHtmlMimeType);
|
|
156
|
-
const nodes = $generateNodesFromDOM(dom, editor);
|
|
173
|
+
const nodes = $generateNodesFromDOM(dom, editor);
|
|
174
|
+
|
|
175
|
+
if (isSelectionInsideOfGrid && nodes.length === 1 && lexical.$isGridNode(nodes[0])) {
|
|
176
|
+
$mergeGridNodesStrategy(nodes, selection, false, editor);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
157
179
|
|
|
180
|
+
$basicInsertStrategy(nodes, selection, false);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
$insertDataTransferForPlainText(dataTransfer, selection);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function $basicInsertStrategy(nodes, selection, isFromLexical) {
|
|
188
|
+
let nodesToInsert;
|
|
189
|
+
|
|
190
|
+
if (!isFromLexical) {
|
|
191
|
+
// Wrap text and inline nodes in paragraph nodes so we have all blocks at the top-level
|
|
158
192
|
const topLevelBlocks = [];
|
|
159
193
|
let currentBlock = null;
|
|
160
194
|
|
|
@@ -176,11 +210,128 @@ function $insertDataTransferForRichText(dataTransfer, selection, editor) {
|
|
|
176
210
|
}
|
|
177
211
|
}
|
|
178
212
|
|
|
179
|
-
|
|
180
|
-
|
|
213
|
+
nodesToInsert = topLevelBlocks;
|
|
214
|
+
} else {
|
|
215
|
+
nodesToInsert = nodes;
|
|
181
216
|
}
|
|
182
217
|
|
|
183
|
-
|
|
218
|
+
if (lexical.$isRangeSelection(selection)) {
|
|
219
|
+
selection.insertNodes(nodesToInsert);
|
|
220
|
+
} else if (lexical.$isGridSelection(selection)) {
|
|
221
|
+
// If there's an active grid selection and a non grid is pasted, add to the anchor.
|
|
222
|
+
const anchorCell = selection.anchor.getNode();
|
|
223
|
+
|
|
224
|
+
if (!lexical.$isGridCellNode(anchorCell)) {
|
|
225
|
+
{
|
|
226
|
+
throw Error(`Expected Grid Cell in Grid Selection`);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
anchorCell.append(...nodesToInsert);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function $mergeGridNodesStrategy(nodes, selection, isFromLexical, editor) {
|
|
235
|
+
if (nodes.length !== 1 || !lexical.$isGridNode(nodes[0])) {
|
|
236
|
+
{
|
|
237
|
+
throw Error(`$mergeGridNodesStrategy: Expected Grid insertion.`);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const newGrid = nodes[0];
|
|
242
|
+
const newGridRows = newGrid.getChildren();
|
|
243
|
+
const newColumnCount = newGrid.getFirstChildOrThrow().getChildrenSize();
|
|
244
|
+
const newRowCount = newGrid.getChildrenSize();
|
|
245
|
+
const gridCellNode = utils.$findMatchingParent(selection.anchor.getNode(), n => lexical.$isGridCellNode(n));
|
|
246
|
+
const gridRowNode = gridCellNode && utils.$findMatchingParent(gridCellNode, n => lexical.$isGridRowNode(n));
|
|
247
|
+
const gridNode = gridRowNode && utils.$findMatchingParent(gridRowNode, n => lexical.$isGridNode(n));
|
|
248
|
+
|
|
249
|
+
if (!lexical.$isGridCellNode(gridCellNode) || !lexical.$isGridRowNode(gridRowNode) || !lexical.$isGridNode(gridNode)) {
|
|
250
|
+
{
|
|
251
|
+
throw Error(`$mergeGridNodesStrategy: Expected selection to be inside of a Grid.`);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const startY = gridRowNode.getIndexWithinParent();
|
|
256
|
+
const stopY = Math.min(gridNode.getChildrenSize() - 1, startY + newRowCount - 1);
|
|
257
|
+
const startX = gridCellNode.getIndexWithinParent();
|
|
258
|
+
const stopX = Math.min(gridRowNode.getChildrenSize() - 1, startX + newColumnCount - 1);
|
|
259
|
+
const fromX = Math.min(startX, stopX);
|
|
260
|
+
const fromY = Math.min(startY, stopY);
|
|
261
|
+
const toX = Math.max(startX, stopX);
|
|
262
|
+
const toY = Math.max(startY, stopY);
|
|
263
|
+
const gridRowNodes = gridNode.getChildren();
|
|
264
|
+
let newRowIdx = 0;
|
|
265
|
+
let newAnchorCellKey;
|
|
266
|
+
let newFocusCellKey;
|
|
267
|
+
|
|
268
|
+
for (let r = fromY; r <= toY; r++) {
|
|
269
|
+
const currentGridRowNode = gridRowNodes[r];
|
|
270
|
+
|
|
271
|
+
if (!lexical.$isGridRowNode(currentGridRowNode)) {
|
|
272
|
+
{
|
|
273
|
+
throw Error(`getNodes: expected to find GridRowNode`);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const newGridRowNode = newGridRows[newRowIdx];
|
|
278
|
+
|
|
279
|
+
if (!lexical.$isGridRowNode(newGridRowNode)) {
|
|
280
|
+
{
|
|
281
|
+
throw Error(`getNodes: expected to find GridRowNode`);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const gridCellNodes = currentGridRowNode.getChildren();
|
|
286
|
+
const newGridCellNodes = newGridRowNode.getChildren();
|
|
287
|
+
let newColumnIdx = 0;
|
|
288
|
+
|
|
289
|
+
for (let c = fromX; c <= toX; c++) {
|
|
290
|
+
const currentGridCellNode = gridCellNodes[c];
|
|
291
|
+
|
|
292
|
+
if (!lexical.$isGridCellNode(currentGridCellNode)) {
|
|
293
|
+
{
|
|
294
|
+
throw Error(`getNodes: expected to find GridCellNode`);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const newGridCellNode = newGridCellNodes[newColumnIdx];
|
|
299
|
+
|
|
300
|
+
if (!lexical.$isGridCellNode(newGridCellNode)) {
|
|
301
|
+
{
|
|
302
|
+
throw Error(`getNodes: expected to find GridCellNode`);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (r === fromY && c === fromX) {
|
|
307
|
+
newAnchorCellKey = currentGridCellNode.getKey();
|
|
308
|
+
} else if (r === toY && c === toX) {
|
|
309
|
+
newFocusCellKey = currentGridCellNode.getKey();
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const originalChildren = currentGridCellNode.getChildren();
|
|
313
|
+
newGridCellNode.getChildren().forEach(child => {
|
|
314
|
+
if (lexical.$isTextNode(child)) {
|
|
315
|
+
const paragraphNode = lexical.$createParagraphNode();
|
|
316
|
+
paragraphNode.append(child);
|
|
317
|
+
currentGridCellNode.append(child);
|
|
318
|
+
} else {
|
|
319
|
+
currentGridCellNode.append(child);
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
originalChildren.forEach(n => n.remove());
|
|
323
|
+
newColumnIdx++;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
newRowIdx++;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (newAnchorCellKey && newFocusCellKey) {
|
|
330
|
+
const newGridSelection = lexical.$createGridSelection();
|
|
331
|
+
newGridSelection.set(gridNode.getKey(), newAnchorCellKey, newFocusCellKey);
|
|
332
|
+
lexical.$setSelection(newGridSelection);
|
|
333
|
+
editor.dispatchCommand(lexical.SELECTION_CHANGE_COMMAND);
|
|
334
|
+
}
|
|
184
335
|
}
|
|
185
336
|
|
|
186
337
|
function $generateNodes(nodeRange) {
|
package/LexicalClipboard.prod.js
CHANGED
|
@@ -4,11 +4,16 @@
|
|
|
4
4
|
* This source code is licensed under the MIT license found in the
|
|
5
5
|
* LICENSE file in the root directory of this source tree.
|
|
6
6
|
*/
|
|
7
|
-
var
|
|
8
|
-
function
|
|
9
|
-
|
|
10
|
-
function
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
7
|
+
var g=require("@lexical/selection"),t=require("@lexical/utils"),x=require("lexical");function y(a){throw Error(`Minified Lexical error #${a}; see codes.json for the full message or `+"use the non-minified dev environment for full errors and additional helpful warnings.");}const z=new Set(["STYLE"]);
|
|
8
|
+
function A(a,c,e){var f=e;if(x.$isRangeSelection(c)||x.$isGridSelection(c)){var b=c.anchor.getNode(),d=c.focus.getNode(),h=e.is(b),k=e.is(d);if(x.$isTextNode(e)&&(h||k)){const [p,m]=c.getCharacterOffsets();h=c.isBackward();k=b.is(d);const q=e.is(h?d:b);b=e.is(h?b:d);k?(f=p>m?m:p,e=e.splitText(f,p>m?p:m),f=0===f?e[0]:e[1]):q?(f=h?m:p,e=e.splitText(f),f=0===f?e[0]:e[1]):b&&(f=e.splitText(h?p:m)[0])}}const {element:l,after:r}=f.exportDOM(a);if(!l)return null;e=x.$isElementNode(f)?f.getChildren():[];
|
|
9
|
+
for(b=0;b<e.length;b++)d=e[b],d.isSelected()&&(d=A(a,c,d))&&l.append(d);return r?r.call(f,l):l}function B(a,c){a=a.getData("text/plain");null!=a&&c.insertRawText(a)}
|
|
10
|
+
function C(a,c,e){if(!e){e=[];let f=null;for(let b=0;b<a.length;b++){const d=a[b];!x.$isElementNode(d)||d.isInline()?(null===f&&(f=x.$createParagraphNode(),e.push(f)),null!==f&&f.append(d)):(e.push(d),f=null)}a=e}if(x.$isRangeSelection(c))c.insertNodes(a);else if(x.$isGridSelection(c)){c=c.anchor.getNode();if(!x.$isGridCellNode(c))throw Error("Expected Grid Cell in Grid Selection");c.append(...a)}}
|
|
11
|
+
function D(a,c,e,f){if(1!==a.length||!x.$isGridNode(a[0]))throw Error("$mergeGridNodesStrategy: Expected Grid insertion.");var b=a[0];a=b.getChildren();e=b.getFirstChildOrThrow().getChildrenSize();var d=b.getChildrenSize(),h=t.$findMatchingParent(c.anchor.getNode(),n=>x.$isGridCellNode(n));c=(b=h&&t.$findMatchingParent(h,n=>x.$isGridRowNode(n)))&&t.$findMatchingParent(b,n=>x.$isGridNode(n));if(!x.$isGridCellNode(h)||!x.$isGridRowNode(b)||!x.$isGridNode(c))throw Error("$mergeGridNodesStrategy: Expected selection to be inside of a Grid.");
|
|
12
|
+
var k=b.getIndexWithinParent(),l=Math.min(c.getChildrenSize()-1,k+d-1);d=h.getIndexWithinParent();h=Math.min(b.getChildrenSize()-1,d+e-1);e=Math.min(d,h);b=Math.min(k,l);d=Math.max(d,h);k=Math.max(k,l);l=c.getChildren();h=0;let r,p;for(let n=b;n<=k;n++){var m=l[n];x.$isGridRowNode(m)||y(77);var q=a[h];x.$isGridRowNode(q)||y(77);m=m.getChildren();q=q.getChildren();let E=0;for(let u=e;u<=d;u++){const v=m[u];x.$isGridCellNode(v)||y(78);const F=q[E];x.$isGridCellNode(F)||y(78);n===b&&u===e?r=v.getKey():
|
|
13
|
+
n===k&&u===d&&(p=v.getKey());const H=v.getChildren();F.getChildren().forEach(w=>{x.$isTextNode(w)&&x.$createParagraphNode().append(w);v.append(w)});H.forEach(w=>w.remove());E++}h++}r&&p&&(a=x.$createGridSelection(),a.set(c.getKey(),r,p),x.$setSelection(a),f.dispatchCommand(x.SELECTION_CHANGE_COMMAND))}function G(a){const {range:c,nodeMap:e}=a;a=new Map(e);const f=[];for(let d=0;d<c.length;d++){var b=a.get(c[d]);void 0!==b&&(b=x.$createNodeFromParse(b,a),f.push(b))}return f}
|
|
14
|
+
function I(a,c){const {nodeName:e}=a;c=c._htmlConversions.get(e.toLowerCase());let f=null;void 0!==c&&c.forEach(b=>{b=b(a);null!==b&&(null===f||f.priority<b.priority)&&(f=b)});return null!==f?f.conversion:null}
|
|
15
|
+
function J(a,c,e=new Map,f){let b=[];if(z.has(a.nodeName))return b;let d=null;var h=I(a,c);const k=h?h(a):null;h=null;if(null!==k){h=k.after;d=k.node;if(null!==d){for(var [,l]of e)if(d=l(d,f),!d)break;d&&b.push(d)}null!=k.forChild&&e.set(a.nodeName,k.forChild)}a=a.childNodes;f=[];for(l=0;l<a.length;l++)f.push(...J(a[l],c,e,d));null!=h&&(f=h(f));null==d?b=b.concat(f):x.$isElementNode(d)&&d.append(...f);return b}
|
|
16
|
+
function K(a,c){let e=[];a=a.body?Array.from(a.body.childNodes):[];const f=a.length;for(let d=0;d<f;d++){var b=a[d];z.has(b.nodeName)||(b=J(b,c),null!==b&&(e=e.concat(b)))}return e}exports.$getLexicalContent=function(a){var c=x.$getSelection();return null!==c?(a=a._config.namespace,c=g.$cloneContents(c),JSON.stringify({namespace:a,state:c})):null};exports.$insertDataTransferForPlainText=B;
|
|
17
|
+
exports.$insertDataTransferForRichText=function(a,c,e){var f=a.getData("application/x-lexical-editor");const b=x.$isGridSelection(c)||null!==t.$findMatchingParent(c.anchor.getNode(),d=>x.$isGridCellNode(d))&&null!==t.$findMatchingParent(c.focus.getNode(),d=>x.$isGridCellNode(d));if(f){const d=e._config.namespace;try{const h=JSON.parse(f);if(h.namespace===d){const k=G(h.state);if(b&&1===k.length&&x.$isGridNode(k[0])){D(k,c,!1,e);return}C(k,c,!0);return}}catch(h){}}(f=a.getData("text/html"))?(a=(new DOMParser).parseFromString(f,
|
|
18
|
+
"text/html"),a=K(a,e),b&&1===a.length&&x.$isGridNode(a[0])?D(a,c,!1,e):C(a,c,!1)):B(a,c)};
|
|
19
|
+
exports.getHtmlContent=function(a){const c=x.$getSelection();if(null==c)throw Error("Expected valid LexicalSelection");if(x.$isRangeSelection(c)&&c.isCollapsed()||0===c.getNodes().length)return null;var e=g.$cloneContents(c);const f=document.createElement("div");for(let h=0;h<e.range.length;h++){var b=x.$getNodeByKey(e.range[h]);if(b){var d=A(a,c,b);if(d)if(b.isSelected())f.append(d);else for(b=d.firstChild;null!=b;)d=b.nextSibling,f.append(b),b=d}}return f.innerHTML};
|
package/package.json
CHANGED
|
@@ -9,14 +9,14 @@
|
|
|
9
9
|
"paste"
|
|
10
10
|
],
|
|
11
11
|
"license": "MIT",
|
|
12
|
-
"version": "0.2.
|
|
12
|
+
"version": "0.2.5",
|
|
13
13
|
"main": "LexicalClipboard.js",
|
|
14
14
|
"peerDependencies": {
|
|
15
|
-
"lexical": "0.2.
|
|
15
|
+
"lexical": "0.2.5"
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
|
18
|
-
"@lexical/utils": "0.2.
|
|
19
|
-
"@lexical/selection": "0.2.
|
|
18
|
+
"@lexical/utils": "0.2.5",
|
|
19
|
+
"@lexical/selection": "0.2.5"
|
|
20
20
|
},
|
|
21
21
|
"repository": {
|
|
22
22
|
"type": "git",
|