@lexical/utils 0.9.0 → 0.9.2
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/LexicalUtils.dev.js +127 -0
- package/index.d.ts +113 -0
- package/package.json +5 -5
package/LexicalUtils.dev.js
CHANGED
|
@@ -10,6 +10,15 @@ var selection = require('@lexical/selection');
|
|
|
10
10
|
var lexical = require('lexical');
|
|
11
11
|
|
|
12
12
|
/** @module @lexical/utils */
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Takes an HTML element and adds the classNames passed within an array,
|
|
16
|
+
* ignoring any non-string types. A space can be used to add multiple classes
|
|
17
|
+
* eg. addClassNamesToElement(element, ['element-inner active', true, null])
|
|
18
|
+
* will add both 'element-inner' and 'active' as classes to that element.
|
|
19
|
+
* @param element - The element in which the classes are added
|
|
20
|
+
* @param classNames - An array defining the class names to add to the element
|
|
21
|
+
*/
|
|
13
22
|
function addClassNamesToElement(element, ...classNames) {
|
|
14
23
|
classNames.forEach(className => {
|
|
15
24
|
if (typeof className === 'string') {
|
|
@@ -18,6 +27,15 @@ function addClassNamesToElement(element, ...classNames) {
|
|
|
18
27
|
}
|
|
19
28
|
});
|
|
20
29
|
}
|
|
30
|
+
/**
|
|
31
|
+
* Takes an HTML element and removes the classNames passed within an array,
|
|
32
|
+
* ignoring any non-string types. A space can be used to remove multiple classes
|
|
33
|
+
* eg. removeClassNamesFromElement(element, ['active small', true, null])
|
|
34
|
+
* will remove both the 'active' and 'small' classes from that element.
|
|
35
|
+
* @param element - The element in which the classes are removed
|
|
36
|
+
* @param classNames - An array defining the class names to remove from the element
|
|
37
|
+
*/
|
|
38
|
+
|
|
21
39
|
function removeClassNamesFromElement(element, ...classNames) {
|
|
22
40
|
classNames.forEach(className => {
|
|
23
41
|
if (typeof className === 'string') {
|
|
@@ -25,6 +43,15 @@ function removeClassNamesFromElement(element, ...classNames) {
|
|
|
25
43
|
}
|
|
26
44
|
});
|
|
27
45
|
}
|
|
46
|
+
/**
|
|
47
|
+
* Returns true if the file type matches the types passed within the acceptableMimeTypes array, false otherwise.
|
|
48
|
+
* The types passed must be strings and are CASE-SENSITIVE.
|
|
49
|
+
* eg. if file is of type 'text' and acceptableMimeTypes = ['TEXT', 'IMAGE'] the function will return false.
|
|
50
|
+
* @param file - The file you want to type check.
|
|
51
|
+
* @param acceptableMimeTypes - An array of strings of types which the file is checked against.
|
|
52
|
+
* @returns true if the file is an acceptable mime type, false otherwise.
|
|
53
|
+
*/
|
|
54
|
+
|
|
28
55
|
function isMimeType(file, acceptableMimeTypes) {
|
|
29
56
|
for (const acceptableType of acceptableMimeTypes) {
|
|
30
57
|
if (file.type.startsWith(acceptableType)) {
|
|
@@ -86,6 +113,17 @@ function mediaFileReader(files, acceptableMimeTypes) {
|
|
|
86
113
|
handleNextFile();
|
|
87
114
|
});
|
|
88
115
|
}
|
|
116
|
+
/**
|
|
117
|
+
* "Depth-First Search" starts at the root/top node of a tree and goes as far as it can down a branch end
|
|
118
|
+
* before backtracking and finding a new path. Consider solving a maze by hugging either wall, moving down a
|
|
119
|
+
* branch until you hit a dead-end (leaf) and backtracking to find the nearest branching path and repeat.
|
|
120
|
+
* It will then return all the nodes found in the search in an array of objects.
|
|
121
|
+
* @param startingNode - The node to start the search, if ommitted, it will start at the root node.
|
|
122
|
+
* @param endingNode - The node to end the search, if ommitted, it will find all descendants of the startingNode.
|
|
123
|
+
* @returns An array of objects of all the nodes found by the search, including their depth into the tree.
|
|
124
|
+
* {depth: number, node: LexicalNode} It will always return at least 1 node (the ending node) so long as it exists
|
|
125
|
+
*/
|
|
126
|
+
|
|
89
127
|
function $dfs(startingNode, endingNode) {
|
|
90
128
|
const nodes = [];
|
|
91
129
|
const start = (startingNode || lexical.$getRoot()).getLatest();
|
|
@@ -139,6 +177,14 @@ function $getDepth(node) {
|
|
|
139
177
|
|
|
140
178
|
return depth;
|
|
141
179
|
}
|
|
180
|
+
/**
|
|
181
|
+
* Takes a node and traverses up its ancestors (toward the root node)
|
|
182
|
+
* in order to find a specific type of node.
|
|
183
|
+
* @param node - the node to begin searching.
|
|
184
|
+
* @param klass - an instance of the type of node to look for.
|
|
185
|
+
* @returns the node of type klass that was passed, or null if none exist.
|
|
186
|
+
*/
|
|
187
|
+
|
|
142
188
|
|
|
143
189
|
function $getNearestNodeOfType(node, klass) {
|
|
144
190
|
let parent = node;
|
|
@@ -153,6 +199,12 @@ function $getNearestNodeOfType(node, klass) {
|
|
|
153
199
|
|
|
154
200
|
return null;
|
|
155
201
|
}
|
|
202
|
+
/**
|
|
203
|
+
*Returns the element node of the nearest ancestor, otherwise throws an error.
|
|
204
|
+
* @param startNode - The starting node of the search
|
|
205
|
+
* @returns The ancestor node found
|
|
206
|
+
*/
|
|
207
|
+
|
|
156
208
|
function $getNearestBlockElementAncestorOrThrow(startNode) {
|
|
157
209
|
const blockNode = $findMatchingParent(startNode, node => lexical.$isElementNode(node) && !node.isInline());
|
|
158
210
|
|
|
@@ -164,6 +216,15 @@ function $getNearestBlockElementAncestorOrThrow(startNode) {
|
|
|
164
216
|
|
|
165
217
|
return blockNode;
|
|
166
218
|
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Starts with a node and moves up the tree (toward the root node) to find a matching node based on
|
|
222
|
+
* the search parameters of the findFn. (Consider JavaScripts' .find() function where a testing function must be
|
|
223
|
+
* passed as an argument. eg. if( (node) => node.__type === 'div') ) return true; otherwise return false
|
|
224
|
+
* @param startingNode - The node where the search starts.
|
|
225
|
+
* @param findFn - A testing function that returns true if the current node satisfies the testing parameters.
|
|
226
|
+
* @returns A parent node that matches the findFn parameters, or null if one wasn't found.
|
|
227
|
+
*/
|
|
167
228
|
function $findMatchingParent(startingNode, findFn) {
|
|
168
229
|
let curr = startingNode;
|
|
169
230
|
|
|
@@ -177,11 +238,44 @@ function $findMatchingParent(startingNode, findFn) {
|
|
|
177
238
|
|
|
178
239
|
return null;
|
|
179
240
|
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Returns a function that will execute all functions passed when called. It is generally used
|
|
244
|
+
* to register multiple lexical listeners and then tear them down with a single function call, such
|
|
245
|
+
* as React's useEffect hook.
|
|
246
|
+
* @example
|
|
247
|
+
* ```ts
|
|
248
|
+
* useEffect(() => {
|
|
249
|
+
* return mergeRegister(
|
|
250
|
+
* editor.registerCommand(...registerCommand1 logic),
|
|
251
|
+
* editor.registerCommand(...registerCommand2 logic),
|
|
252
|
+
* editor.registerCommand(...registerCommand3 logic)
|
|
253
|
+
* )
|
|
254
|
+
* }, [editor])
|
|
255
|
+
* ```
|
|
256
|
+
* In this case, useEffect is returning the function returned by mergeRegister as a cleanup
|
|
257
|
+
* function to be executed after either the useEffect runs again (due to one of its dependencies
|
|
258
|
+
* updating) or the compenent it resides in unmounts.
|
|
259
|
+
* Note the functions don't neccesarily need to be in an array as all arguements
|
|
260
|
+
* are considered to be the func argument and spread from there.
|
|
261
|
+
* @param func - An array of functions meant to be executed by the returned function.
|
|
262
|
+
* @returns the function which executes all the passed register command functions.
|
|
263
|
+
*/
|
|
180
264
|
function mergeRegister(...func) {
|
|
181
265
|
return () => {
|
|
182
266
|
func.forEach(f => f());
|
|
183
267
|
};
|
|
184
268
|
}
|
|
269
|
+
/**
|
|
270
|
+
* Attempts to resolve nested element nodes of the same type into a single node of that type.
|
|
271
|
+
* It is generally used for marks/commenting
|
|
272
|
+
* @param editor - The lexical editor
|
|
273
|
+
* @param targetNode - The target for the nested element to be extracted from.
|
|
274
|
+
* @param cloneNode - See {@link $createMarkNode}
|
|
275
|
+
* @param handleOverlap - Handles any overlap between the node to extract and the targetNode
|
|
276
|
+
* @returns The lexical editor
|
|
277
|
+
*/
|
|
278
|
+
|
|
185
279
|
function registerNestedElementResolver(editor, targetNode, cloneNode, handleOverlap) {
|
|
186
280
|
const $isTargetNode = node => {
|
|
187
281
|
return node instanceof targetNode;
|
|
@@ -251,6 +345,13 @@ function registerNestedElementResolver(editor, targetNode, cloneNode, handleOver
|
|
|
251
345
|
|
|
252
346
|
return editor.registerNodeTransform(targetNode, elementNodeTransform);
|
|
253
347
|
}
|
|
348
|
+
/**
|
|
349
|
+
* Clones the editor and marks it as dirty to be reconciled. If there was a selection,
|
|
350
|
+
* it would be set back to its previous state, or null otherwise.
|
|
351
|
+
* @param editor - The lexical editor
|
|
352
|
+
* @param editorState - The editor's state
|
|
353
|
+
*/
|
|
354
|
+
|
|
254
355
|
function $restoreEditorState(editor, editorState) {
|
|
255
356
|
const FULL_RECONCILE = 2;
|
|
256
357
|
const nodeMap = new Map();
|
|
@@ -274,6 +375,15 @@ function $restoreEditorState(editor, editorState) {
|
|
|
274
375
|
const selection$1 = editorState._selection;
|
|
275
376
|
lexical.$setSelection(selection$1 === null ? null : selection$1.clone());
|
|
276
377
|
}
|
|
378
|
+
/**
|
|
379
|
+
* If the selected insertion area is the root/shadow root node (see {@link lexical!$isRootOrShadowRoot}),
|
|
380
|
+
* the node will be appended there, otherwise, it will be inserted before the insertion area.
|
|
381
|
+
* If there is no selection where the node is to be inserted, it will be appended after any current nodes
|
|
382
|
+
* within the tree, as a child of the root node. A paragraph node will then be added after the inserted node and selected.
|
|
383
|
+
* @param node - The node to be inserted
|
|
384
|
+
* @returns The node after its insertion
|
|
385
|
+
*/
|
|
386
|
+
|
|
277
387
|
function $insertNodeToNearestRoot(node) {
|
|
278
388
|
const selection = lexical.$getSelection();
|
|
279
389
|
|
|
@@ -331,15 +441,32 @@ function $insertNodeToNearestRoot(node) {
|
|
|
331
441
|
|
|
332
442
|
return node.getLatest();
|
|
333
443
|
}
|
|
444
|
+
/**
|
|
445
|
+
* Wraps the node into another node created from a createElementNode function, eg. $createParagraphNode
|
|
446
|
+
* @param node - Node to be wrapped.
|
|
447
|
+
* @param createElementNode - Creates a new lexcial element to wrap the to-be-wrapped node and returns it.
|
|
448
|
+
* @returns A new lexcial element with the previous node appended within (as a child, including its children).
|
|
449
|
+
*/
|
|
450
|
+
|
|
334
451
|
function $wrapNodeInElement(node, createElementNode) {
|
|
335
452
|
const elementNode = createElementNode();
|
|
336
453
|
node.replace(elementNode);
|
|
337
454
|
elementNode.append(node);
|
|
338
455
|
return elementNode;
|
|
339
456
|
}
|
|
457
|
+
/**
|
|
458
|
+
* @param x - The element being tested
|
|
459
|
+
* @returns Returns true if x is an HTML anchor tag, false otherwise
|
|
460
|
+
*/
|
|
461
|
+
|
|
340
462
|
function isHTMLAnchorElement(x) {
|
|
341
463
|
return isHTMLElement(x) && x.tagName === 'A';
|
|
342
464
|
}
|
|
465
|
+
/**
|
|
466
|
+
* @param x - The element being testing
|
|
467
|
+
* @returns Returns true if x is an HTML element, false otherwise.
|
|
468
|
+
*/
|
|
469
|
+
|
|
343
470
|
function isHTMLElement(x) {
|
|
344
471
|
// @ts-ignore-next-line - strict check on nodeType here should filter out non-Element EventTarget implementors
|
|
345
472
|
return x.nodeType === 1;
|
package/index.d.ts
CHANGED
|
@@ -12,8 +12,32 @@ export declare type DFSNode = Readonly<{
|
|
|
12
12
|
depth: number;
|
|
13
13
|
node: LexicalNode;
|
|
14
14
|
}>;
|
|
15
|
+
/**
|
|
16
|
+
* Takes an HTML element and adds the classNames passed within an array,
|
|
17
|
+
* ignoring any non-string types. A space can be used to add multiple classes
|
|
18
|
+
* eg. addClassNamesToElement(element, ['element-inner active', true, null])
|
|
19
|
+
* will add both 'element-inner' and 'active' as classes to that element.
|
|
20
|
+
* @param element - The element in which the classes are added
|
|
21
|
+
* @param classNames - An array defining the class names to add to the element
|
|
22
|
+
*/
|
|
15
23
|
export declare function addClassNamesToElement(element: HTMLElement, ...classNames: Array<typeof undefined | boolean | null | string>): void;
|
|
24
|
+
/**
|
|
25
|
+
* Takes an HTML element and removes the classNames passed within an array,
|
|
26
|
+
* ignoring any non-string types. A space can be used to remove multiple classes
|
|
27
|
+
* eg. removeClassNamesFromElement(element, ['active small', true, null])
|
|
28
|
+
* will remove both the 'active' and 'small' classes from that element.
|
|
29
|
+
* @param element - The element in which the classes are removed
|
|
30
|
+
* @param classNames - An array defining the class names to remove from the element
|
|
31
|
+
*/
|
|
16
32
|
export declare function removeClassNamesFromElement(element: HTMLElement, ...classNames: Array<typeof undefined | boolean | null | string>): void;
|
|
33
|
+
/**
|
|
34
|
+
* Returns true if the file type matches the types passed within the acceptableMimeTypes array, false otherwise.
|
|
35
|
+
* The types passed must be strings and are CASE-SENSITIVE.
|
|
36
|
+
* eg. if file is of type 'text' and acceptableMimeTypes = ['TEXT', 'IMAGE'] the function will return false.
|
|
37
|
+
* @param file - The file you want to type check.
|
|
38
|
+
* @param acceptableMimeTypes - An array of strings of types which the file is checked against.
|
|
39
|
+
* @returns true if the file is an acceptable mime type, false otherwise.
|
|
40
|
+
*/
|
|
17
41
|
export declare function isMimeType(file: File, acceptableMimeTypes: Array<string>): boolean;
|
|
18
42
|
/**
|
|
19
43
|
* Lexical File Reader with:
|
|
@@ -30,17 +54,106 @@ export declare function mediaFileReader(files: Array<File>, acceptableMimeTypes:
|
|
|
30
54
|
file: File;
|
|
31
55
|
result: string;
|
|
32
56
|
}>>;
|
|
57
|
+
/**
|
|
58
|
+
* "Depth-First Search" starts at the root/top node of a tree and goes as far as it can down a branch end
|
|
59
|
+
* before backtracking and finding a new path. Consider solving a maze by hugging either wall, moving down a
|
|
60
|
+
* branch until you hit a dead-end (leaf) and backtracking to find the nearest branching path and repeat.
|
|
61
|
+
* It will then return all the nodes found in the search in an array of objects.
|
|
62
|
+
* @param startingNode - The node to start the search, if ommitted, it will start at the root node.
|
|
63
|
+
* @param endingNode - The node to end the search, if ommitted, it will find all descendants of the startingNode.
|
|
64
|
+
* @returns An array of objects of all the nodes found by the search, including their depth into the tree.
|
|
65
|
+
* {depth: number, node: LexicalNode} It will always return at least 1 node (the ending node) so long as it exists
|
|
66
|
+
*/
|
|
33
67
|
export declare function $dfs(startingNode?: LexicalNode, endingNode?: LexicalNode): Array<DFSNode>;
|
|
68
|
+
/**
|
|
69
|
+
* Takes a node and traverses up its ancestors (toward the root node)
|
|
70
|
+
* in order to find a specific type of node.
|
|
71
|
+
* @param node - the node to begin searching.
|
|
72
|
+
* @param klass - an instance of the type of node to look for.
|
|
73
|
+
* @returns the node of type klass that was passed, or null if none exist.
|
|
74
|
+
*/
|
|
34
75
|
export declare function $getNearestNodeOfType<T extends ElementNode>(node: LexicalNode, klass: Klass<T>): T | null;
|
|
76
|
+
/**
|
|
77
|
+
*Returns the element node of the nearest ancestor, otherwise throws an error.
|
|
78
|
+
* @param startNode - The starting node of the search
|
|
79
|
+
* @returns The ancestor node found
|
|
80
|
+
*/
|
|
35
81
|
export declare function $getNearestBlockElementAncestorOrThrow(startNode: LexicalNode): ElementNode;
|
|
36
82
|
export declare type DOMNodeToLexicalConversion = (element: Node) => LexicalNode;
|
|
37
83
|
export declare type DOMNodeToLexicalConversionMap = Record<string, DOMNodeToLexicalConversion>;
|
|
84
|
+
/**
|
|
85
|
+
* Starts with a node and moves up the tree (toward the root node) to find a matching node based on
|
|
86
|
+
* the search parameters of the findFn. (Consider JavaScripts' .find() function where a testing function must be
|
|
87
|
+
* passed as an argument. eg. if( (node) => node.__type === 'div') ) return true; otherwise return false
|
|
88
|
+
* @param startingNode - The node where the search starts.
|
|
89
|
+
* @param findFn - A testing function that returns true if the current node satisfies the testing parameters.
|
|
90
|
+
* @returns A parent node that matches the findFn parameters, or null if one wasn't found.
|
|
91
|
+
*/
|
|
38
92
|
export declare function $findMatchingParent(startingNode: LexicalNode, findFn: (node: LexicalNode) => boolean): LexicalNode | null;
|
|
39
93
|
declare type Func = () => void;
|
|
94
|
+
/**
|
|
95
|
+
* Returns a function that will execute all functions passed when called. It is generally used
|
|
96
|
+
* to register multiple lexical listeners and then tear them down with a single function call, such
|
|
97
|
+
* as React's useEffect hook.
|
|
98
|
+
* @example
|
|
99
|
+
* ```ts
|
|
100
|
+
* useEffect(() => {
|
|
101
|
+
* return mergeRegister(
|
|
102
|
+
* editor.registerCommand(...registerCommand1 logic),
|
|
103
|
+
* editor.registerCommand(...registerCommand2 logic),
|
|
104
|
+
* editor.registerCommand(...registerCommand3 logic)
|
|
105
|
+
* )
|
|
106
|
+
* }, [editor])
|
|
107
|
+
* ```
|
|
108
|
+
* In this case, useEffect is returning the function returned by mergeRegister as a cleanup
|
|
109
|
+
* function to be executed after either the useEffect runs again (due to one of its dependencies
|
|
110
|
+
* updating) or the compenent it resides in unmounts.
|
|
111
|
+
* Note the functions don't neccesarily need to be in an array as all arguements
|
|
112
|
+
* are considered to be the func argument and spread from there.
|
|
113
|
+
* @param func - An array of functions meant to be executed by the returned function.
|
|
114
|
+
* @returns the function which executes all the passed register command functions.
|
|
115
|
+
*/
|
|
40
116
|
export declare function mergeRegister(...func: Array<Func>): () => void;
|
|
117
|
+
/**
|
|
118
|
+
* Attempts to resolve nested element nodes of the same type into a single node of that type.
|
|
119
|
+
* It is generally used for marks/commenting
|
|
120
|
+
* @param editor - The lexical editor
|
|
121
|
+
* @param targetNode - The target for the nested element to be extracted from.
|
|
122
|
+
* @param cloneNode - See {@link $createMarkNode}
|
|
123
|
+
* @param handleOverlap - Handles any overlap between the node to extract and the targetNode
|
|
124
|
+
* @returns The lexical editor
|
|
125
|
+
*/
|
|
41
126
|
export declare function registerNestedElementResolver<N extends ElementNode>(editor: LexicalEditor, targetNode: Klass<N>, cloneNode: (from: N) => N, handleOverlap: (from: N, to: N) => void): () => void;
|
|
127
|
+
/**
|
|
128
|
+
* Clones the editor and marks it as dirty to be reconciled. If there was a selection,
|
|
129
|
+
* it would be set back to its previous state, or null otherwise.
|
|
130
|
+
* @param editor - The lexical editor
|
|
131
|
+
* @param editorState - The editor's state
|
|
132
|
+
*/
|
|
42
133
|
export declare function $restoreEditorState(editor: LexicalEditor, editorState: EditorState): void;
|
|
134
|
+
/**
|
|
135
|
+
* If the selected insertion area is the root/shadow root node (see {@link lexical!$isRootOrShadowRoot}),
|
|
136
|
+
* the node will be appended there, otherwise, it will be inserted before the insertion area.
|
|
137
|
+
* If there is no selection where the node is to be inserted, it will be appended after any current nodes
|
|
138
|
+
* within the tree, as a child of the root node. A paragraph node will then be added after the inserted node and selected.
|
|
139
|
+
* @param node - The node to be inserted
|
|
140
|
+
* @returns The node after its insertion
|
|
141
|
+
*/
|
|
43
142
|
export declare function $insertNodeToNearestRoot<T extends LexicalNode>(node: T): T;
|
|
143
|
+
/**
|
|
144
|
+
* Wraps the node into another node created from a createElementNode function, eg. $createParagraphNode
|
|
145
|
+
* @param node - Node to be wrapped.
|
|
146
|
+
* @param createElementNode - Creates a new lexcial element to wrap the to-be-wrapped node and returns it.
|
|
147
|
+
* @returns A new lexcial element with the previous node appended within (as a child, including its children).
|
|
148
|
+
*/
|
|
44
149
|
export declare function $wrapNodeInElement(node: LexicalNode, createElementNode: () => ElementNode): ElementNode;
|
|
150
|
+
/**
|
|
151
|
+
* @param x - The element being tested
|
|
152
|
+
* @returns Returns true if x is an HTML anchor tag, false otherwise
|
|
153
|
+
*/
|
|
45
154
|
export declare function isHTMLAnchorElement(x: Node): x is HTMLAnchorElement;
|
|
155
|
+
/**
|
|
156
|
+
* @param x - The element being testing
|
|
157
|
+
* @returns Returns true if x is an HTML element, false otherwise.
|
|
158
|
+
*/
|
|
46
159
|
export declare function isHTMLElement(x: Node | EventTarget): x is HTMLElement;
|
package/package.json
CHANGED
|
@@ -8,15 +8,15 @@
|
|
|
8
8
|
"utils"
|
|
9
9
|
],
|
|
10
10
|
"license": "MIT",
|
|
11
|
-
"version": "0.9.
|
|
11
|
+
"version": "0.9.2",
|
|
12
12
|
"main": "LexicalUtils.js",
|
|
13
13
|
"peerDependencies": {
|
|
14
|
-
"lexical": "0.9.
|
|
14
|
+
"lexical": "0.9.2"
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
|
-
"@lexical/list": "0.9.
|
|
18
|
-
"@lexical/table": "0.9.
|
|
19
|
-
"@lexical/selection": "0.9.
|
|
17
|
+
"@lexical/list": "0.9.2",
|
|
18
|
+
"@lexical/table": "0.9.2",
|
|
19
|
+
"@lexical/selection": "0.9.2"
|
|
20
20
|
},
|
|
21
21
|
"repository": {
|
|
22
22
|
"type": "git",
|