@atlaskit/editor-plugin-show-diff 3.2.1 → 3.2.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 +13 -0
- package/afm-jira/tsconfig.json +3 -0
- package/afm-products/tsconfig.json +3 -0
- package/dist/cjs/pm-plugins/attributeDecorations.js +26 -2
- package/dist/cjs/pm-plugins/calculateDiffDecorations.js +6 -5
- package/dist/es2019/pm-plugins/attributeDecorations.js +25 -1
- package/dist/es2019/pm-plugins/calculateDiffDecorations.js +8 -7
- package/dist/esm/pm-plugins/attributeDecorations.js +25 -1
- package/dist/esm/pm-plugins/calculateDiffDecorations.js +8 -7
- package/dist/types/pm-plugins/attributeDecorations.d.ts +9 -0
- package/dist/types-ts4.5/pm-plugins/attributeDecorations.d.ts +9 -0
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# @atlaskit/editor-plugin-show-diff
|
|
2
2
|
|
|
3
|
+
## 3.2.3
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Updated dependencies
|
|
8
|
+
|
|
9
|
+
## 3.2.2
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- [`1c0d87f570c52`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/1c0d87f570c52) -
|
|
14
|
+
[ux] Update attributes to ignore attr steps that do not affect the document
|
|
15
|
+
|
|
3
16
|
## 3.2.1
|
|
4
17
|
|
|
5
18
|
### Patch Changes
|
package/afm-jira/tsconfig.json
CHANGED
|
@@ -4,7 +4,7 @@ var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefau
|
|
|
4
4
|
Object.defineProperty(exports, "__esModule", {
|
|
5
5
|
value: true
|
|
6
6
|
});
|
|
7
|
-
exports.getAttrChangeRanges = void 0;
|
|
7
|
+
exports.stepIsValidAttrChange = exports.getAttrChangeRanges = void 0;
|
|
8
8
|
var _toConsumableArray2 = _interopRequireDefault(require("@babel/runtime/helpers/toConsumableArray"));
|
|
9
9
|
var _steps = require("@atlaskit/adf-schema/steps");
|
|
10
10
|
var _transform = require("@atlaskit/editor-prosemirror/transform");
|
|
@@ -16,7 +16,7 @@ var filterUndefined = function filterUndefined(x) {
|
|
|
16
16
|
var allowedAttrs = ['id', 'collection', 'url'];
|
|
17
17
|
var getAttrChangeRanges = exports.getAttrChangeRanges = function getAttrChangeRanges(doc, steps) {
|
|
18
18
|
return steps.map(function (step) {
|
|
19
|
-
if (step instanceof _transform.AttrStep && allowedAttrs.includes(step.attr) || step instanceof _steps.SetAttrsStep && (0, _toConsumableArray2.default)(Object.keys(step.attrs)).some(function (v) {
|
|
19
|
+
if (step instanceof _transform.AttrStep && allowedAttrs.includes(step.attr) || step instanceof _steps.SetAttrsStep && step.attrs && (0, _toConsumableArray2.default)(Object.keys(step.attrs)).some(function (v) {
|
|
20
20
|
return allowedAttrs.includes(v);
|
|
21
21
|
})) {
|
|
22
22
|
var $pos = doc.resolve(step.pos);
|
|
@@ -30,4 +30,28 @@ var getAttrChangeRanges = exports.getAttrChangeRanges = function getAttrChangeRa
|
|
|
30
30
|
}
|
|
31
31
|
return undefined;
|
|
32
32
|
}).filter(filterUndefined);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Check if the step was a valid attr change and affected the doc
|
|
37
|
+
*
|
|
38
|
+
* @param step Attr step to test
|
|
39
|
+
* @param beforeDoc Doc before the step
|
|
40
|
+
* @param afterDoc Doc after the step
|
|
41
|
+
* @returns Boolean if the change should show a decoration
|
|
42
|
+
*/
|
|
43
|
+
var stepIsValidAttrChange = exports.stepIsValidAttrChange = function stepIsValidAttrChange(step, beforeDoc, afterDoc) {
|
|
44
|
+
try {
|
|
45
|
+
if (step instanceof _transform.AttrStep || step instanceof _steps.SetAttrsStep) {
|
|
46
|
+
var attrStepAfter = afterDoc.nodeAt(step.pos);
|
|
47
|
+
var attrStepBefore = beforeDoc.nodeAt(step.pos);
|
|
48
|
+
// The change affected the document
|
|
49
|
+
if (attrStepAfter && attrStepBefore && !attrStepAfter.eq(attrStepBefore)) {
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return false;
|
|
54
|
+
} catch (e) {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
33
57
|
};
|
|
@@ -13,7 +13,6 @@ var _memoizeOne = _interopRequireDefault(require("memoize-one"));
|
|
|
13
13
|
var _prosemirrorChangeset = require("prosemirror-changeset");
|
|
14
14
|
var _steps = require("@atlaskit/adf-schema/steps");
|
|
15
15
|
var _document = require("@atlaskit/editor-common/utils/document");
|
|
16
|
-
var _transform = require("@atlaskit/editor-prosemirror/transform");
|
|
17
16
|
var _view = require("@atlaskit/editor-prosemirror/view");
|
|
18
17
|
var _attributeDecorations = require("./attributeDecorations");
|
|
19
18
|
var _decorations = require("./decorations");
|
|
@@ -76,7 +75,7 @@ function simplifySteps(steps) {
|
|
|
76
75
|
return steps
|
|
77
76
|
// Remove steps that don't affect document structure or content
|
|
78
77
|
.filter(function (step) {
|
|
79
|
-
return !(step instanceof _steps.AnalyticsStep
|
|
78
|
+
return !(step instanceof _steps.AnalyticsStep);
|
|
80
79
|
})
|
|
81
80
|
// Merge consecutive steps where possible
|
|
82
81
|
.reduce(function (acc, step) {
|
|
@@ -105,7 +104,7 @@ var calculateDiffDecorationsInner = function calculateDiffDecorationsInner(_ref)
|
|
|
105
104
|
}
|
|
106
105
|
var tr = state.tr;
|
|
107
106
|
var steppedDoc = originalDoc;
|
|
108
|
-
var
|
|
107
|
+
var attrSteps = [];
|
|
109
108
|
var changeset = _prosemirrorChangeset.ChangeSet.create(originalDoc);
|
|
110
109
|
var _iterator = _createForOfIteratorHelper(steps),
|
|
111
110
|
_step;
|
|
@@ -114,8 +113,10 @@ var calculateDiffDecorationsInner = function calculateDiffDecorationsInner(_ref)
|
|
|
114
113
|
var step = _step.value;
|
|
115
114
|
var result = step.apply(steppedDoc);
|
|
116
115
|
if (result.failed === null && result.doc) {
|
|
116
|
+
if ((0, _attributeDecorations.stepIsValidAttrChange)(step, steppedDoc, result.doc)) {
|
|
117
|
+
attrSteps.push(step);
|
|
118
|
+
}
|
|
117
119
|
steppedDoc = result.doc;
|
|
118
|
-
stepMaps.push(step.getMap());
|
|
119
120
|
changeset = changeset.addSteps(steppedDoc, [step.getMap()], step);
|
|
120
121
|
}
|
|
121
122
|
}
|
|
@@ -155,7 +156,7 @@ var calculateDiffDecorationsInner = function calculateDiffDecorationsInner(_ref)
|
|
|
155
156
|
(0, _markDecorations.getMarkChangeRanges)(steps).forEach(function (change) {
|
|
156
157
|
decorations.push((0, _decorations.createInlineChangedDecoration)(change, colourScheme));
|
|
157
158
|
});
|
|
158
|
-
(0, _attributeDecorations.getAttrChangeRanges)(tr.doc,
|
|
159
|
+
(0, _attributeDecorations.getAttrChangeRanges)(tr.doc, attrSteps).forEach(function (change) {
|
|
159
160
|
decorations.push.apply(decorations, (0, _toConsumableArray2.default)(calculateNodesForBlockDecoration(tr.doc, change.fromB, change.toB, colourScheme)));
|
|
160
161
|
});
|
|
161
162
|
return _view.DecorationSet.empty.add(tr.doc, decorations);
|
|
@@ -6,7 +6,7 @@ const filterUndefined = x => !!x;
|
|
|
6
6
|
const allowedAttrs = ['id', 'collection', 'url'];
|
|
7
7
|
export const getAttrChangeRanges = (doc, steps) => {
|
|
8
8
|
return steps.map(step => {
|
|
9
|
-
if (step instanceof AttrStep && allowedAttrs.includes(step.attr) || step instanceof SetAttrsStep && [...Object.keys(step.attrs)].some(v => allowedAttrs.includes(v))) {
|
|
9
|
+
if (step instanceof AttrStep && allowedAttrs.includes(step.attr) || step instanceof SetAttrsStep && step.attrs && [...Object.keys(step.attrs)].some(v => allowedAttrs.includes(v))) {
|
|
10
10
|
const $pos = doc.resolve(step.pos);
|
|
11
11
|
if ($pos.parent.type === doc.type.schema.nodes.mediaSingle) {
|
|
12
12
|
const startPos = $pos.pos + $pos.parentOffset;
|
|
@@ -18,4 +18,28 @@ export const getAttrChangeRanges = (doc, steps) => {
|
|
|
18
18
|
}
|
|
19
19
|
return undefined;
|
|
20
20
|
}).filter(filterUndefined);
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Check if the step was a valid attr change and affected the doc
|
|
25
|
+
*
|
|
26
|
+
* @param step Attr step to test
|
|
27
|
+
* @param beforeDoc Doc before the step
|
|
28
|
+
* @param afterDoc Doc after the step
|
|
29
|
+
* @returns Boolean if the change should show a decoration
|
|
30
|
+
*/
|
|
31
|
+
export const stepIsValidAttrChange = (step, beforeDoc, afterDoc) => {
|
|
32
|
+
try {
|
|
33
|
+
if (step instanceof AttrStep || step instanceof SetAttrsStep) {
|
|
34
|
+
const attrStepAfter = afterDoc.nodeAt(step.pos);
|
|
35
|
+
const attrStepBefore = beforeDoc.nodeAt(step.pos);
|
|
36
|
+
// The change affected the document
|
|
37
|
+
if (attrStepAfter && attrStepBefore && !attrStepAfter.eq(attrStepBefore)) {
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return false;
|
|
42
|
+
} catch (e) {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
21
45
|
};
|
|
@@ -2,11 +2,10 @@
|
|
|
2
2
|
import isEqual from 'lodash/isEqual';
|
|
3
3
|
import memoizeOne from 'memoize-one';
|
|
4
4
|
import { ChangeSet, simplifyChanges } from 'prosemirror-changeset';
|
|
5
|
-
import { AnalyticsStep
|
|
5
|
+
import { AnalyticsStep } from '@atlaskit/adf-schema/steps';
|
|
6
6
|
import { areNodesEqualIgnoreAttrs } from '@atlaskit/editor-common/utils/document';
|
|
7
|
-
import { AttrStep } from '@atlaskit/editor-prosemirror/transform';
|
|
8
7
|
import { DecorationSet } from '@atlaskit/editor-prosemirror/view';
|
|
9
|
-
import { getAttrChangeRanges } from './attributeDecorations';
|
|
8
|
+
import { getAttrChangeRanges, stepIsValidAttrChange } from './attributeDecorations';
|
|
10
9
|
import { createInlineChangedDecoration, createDeletedContentDecoration, createBlockChangedDecoration } from './decorations';
|
|
11
10
|
import { getMarkChangeRanges } from './markDecorations';
|
|
12
11
|
const calculateNodesForBlockDecoration = (doc, from, to, colourScheme) => {
|
|
@@ -65,7 +64,7 @@ function optimizeChanges(changes) {
|
|
|
65
64
|
function simplifySteps(steps) {
|
|
66
65
|
return steps
|
|
67
66
|
// Remove steps that don't affect document structure or content
|
|
68
|
-
.filter(step => !(step instanceof AnalyticsStep
|
|
67
|
+
.filter(step => !(step instanceof AnalyticsStep))
|
|
69
68
|
// Merge consecutive steps where possible
|
|
70
69
|
.reduce((acc, step) => {
|
|
71
70
|
var _lastStep$merge;
|
|
@@ -98,13 +97,15 @@ const calculateDiffDecorationsInner = ({
|
|
|
98
97
|
tr
|
|
99
98
|
} = state;
|
|
100
99
|
let steppedDoc = originalDoc;
|
|
101
|
-
const
|
|
100
|
+
const attrSteps = [];
|
|
102
101
|
let changeset = ChangeSet.create(originalDoc);
|
|
103
102
|
for (const step of steps) {
|
|
104
103
|
const result = step.apply(steppedDoc);
|
|
105
104
|
if (result.failed === null && result.doc) {
|
|
105
|
+
if (stepIsValidAttrChange(step, steppedDoc, result.doc)) {
|
|
106
|
+
attrSteps.push(step);
|
|
107
|
+
}
|
|
106
108
|
steppedDoc = result.doc;
|
|
107
|
-
stepMaps.push(step.getMap());
|
|
108
109
|
changeset = changeset.addSteps(steppedDoc, [step.getMap()], step);
|
|
109
110
|
}
|
|
110
111
|
}
|
|
@@ -139,7 +140,7 @@ const calculateDiffDecorationsInner = ({
|
|
|
139
140
|
getMarkChangeRanges(steps).forEach(change => {
|
|
140
141
|
decorations.push(createInlineChangedDecoration(change, colourScheme));
|
|
141
142
|
});
|
|
142
|
-
getAttrChangeRanges(tr.doc,
|
|
143
|
+
getAttrChangeRanges(tr.doc, attrSteps).forEach(change => {
|
|
143
144
|
decorations.push(...calculateNodesForBlockDecoration(tr.doc, change.fromB, change.toB, colourScheme));
|
|
144
145
|
});
|
|
145
146
|
return DecorationSet.empty.add(tr.doc, decorations);
|
|
@@ -9,7 +9,7 @@ var filterUndefined = function filterUndefined(x) {
|
|
|
9
9
|
var allowedAttrs = ['id', 'collection', 'url'];
|
|
10
10
|
export var getAttrChangeRanges = function getAttrChangeRanges(doc, steps) {
|
|
11
11
|
return steps.map(function (step) {
|
|
12
|
-
if (step instanceof AttrStep && allowedAttrs.includes(step.attr) || step instanceof SetAttrsStep && _toConsumableArray(Object.keys(step.attrs)).some(function (v) {
|
|
12
|
+
if (step instanceof AttrStep && allowedAttrs.includes(step.attr) || step instanceof SetAttrsStep && step.attrs && _toConsumableArray(Object.keys(step.attrs)).some(function (v) {
|
|
13
13
|
return allowedAttrs.includes(v);
|
|
14
14
|
})) {
|
|
15
15
|
var $pos = doc.resolve(step.pos);
|
|
@@ -23,4 +23,28 @@ export var getAttrChangeRanges = function getAttrChangeRanges(doc, steps) {
|
|
|
23
23
|
}
|
|
24
24
|
return undefined;
|
|
25
25
|
}).filter(filterUndefined);
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Check if the step was a valid attr change and affected the doc
|
|
30
|
+
*
|
|
31
|
+
* @param step Attr step to test
|
|
32
|
+
* @param beforeDoc Doc before the step
|
|
33
|
+
* @param afterDoc Doc after the step
|
|
34
|
+
* @returns Boolean if the change should show a decoration
|
|
35
|
+
*/
|
|
36
|
+
export var stepIsValidAttrChange = function stepIsValidAttrChange(step, beforeDoc, afterDoc) {
|
|
37
|
+
try {
|
|
38
|
+
if (step instanceof AttrStep || step instanceof SetAttrsStep) {
|
|
39
|
+
var attrStepAfter = afterDoc.nodeAt(step.pos);
|
|
40
|
+
var attrStepBefore = beforeDoc.nodeAt(step.pos);
|
|
41
|
+
// The change affected the document
|
|
42
|
+
if (attrStepAfter && attrStepBefore && !attrStepAfter.eq(attrStepBefore)) {
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return false;
|
|
47
|
+
} catch (e) {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
26
50
|
};
|
|
@@ -10,11 +10,10 @@ function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t =
|
|
|
10
10
|
import isEqual from 'lodash/isEqual';
|
|
11
11
|
import memoizeOne from 'memoize-one';
|
|
12
12
|
import { ChangeSet, simplifyChanges } from 'prosemirror-changeset';
|
|
13
|
-
import { AnalyticsStep
|
|
13
|
+
import { AnalyticsStep } from '@atlaskit/adf-schema/steps';
|
|
14
14
|
import { areNodesEqualIgnoreAttrs } from '@atlaskit/editor-common/utils/document';
|
|
15
|
-
import { AttrStep } from '@atlaskit/editor-prosemirror/transform';
|
|
16
15
|
import { DecorationSet } from '@atlaskit/editor-prosemirror/view';
|
|
17
|
-
import { getAttrChangeRanges } from './attributeDecorations';
|
|
16
|
+
import { getAttrChangeRanges, stepIsValidAttrChange } from './attributeDecorations';
|
|
18
17
|
import { createInlineChangedDecoration, createDeletedContentDecoration, createBlockChangedDecoration } from './decorations';
|
|
19
18
|
import { getMarkChangeRanges } from './markDecorations';
|
|
20
19
|
var calculateNodesForBlockDecoration = function calculateNodesForBlockDecoration(doc, from, to, colourScheme) {
|
|
@@ -70,7 +69,7 @@ function simplifySteps(steps) {
|
|
|
70
69
|
return steps
|
|
71
70
|
// Remove steps that don't affect document structure or content
|
|
72
71
|
.filter(function (step) {
|
|
73
|
-
return !(step instanceof AnalyticsStep
|
|
72
|
+
return !(step instanceof AnalyticsStep);
|
|
74
73
|
})
|
|
75
74
|
// Merge consecutive steps where possible
|
|
76
75
|
.reduce(function (acc, step) {
|
|
@@ -99,7 +98,7 @@ var calculateDiffDecorationsInner = function calculateDiffDecorationsInner(_ref)
|
|
|
99
98
|
}
|
|
100
99
|
var tr = state.tr;
|
|
101
100
|
var steppedDoc = originalDoc;
|
|
102
|
-
var
|
|
101
|
+
var attrSteps = [];
|
|
103
102
|
var changeset = ChangeSet.create(originalDoc);
|
|
104
103
|
var _iterator = _createForOfIteratorHelper(steps),
|
|
105
104
|
_step;
|
|
@@ -108,8 +107,10 @@ var calculateDiffDecorationsInner = function calculateDiffDecorationsInner(_ref)
|
|
|
108
107
|
var step = _step.value;
|
|
109
108
|
var result = step.apply(steppedDoc);
|
|
110
109
|
if (result.failed === null && result.doc) {
|
|
110
|
+
if (stepIsValidAttrChange(step, steppedDoc, result.doc)) {
|
|
111
|
+
attrSteps.push(step);
|
|
112
|
+
}
|
|
111
113
|
steppedDoc = result.doc;
|
|
112
|
-
stepMaps.push(step.getMap());
|
|
113
114
|
changeset = changeset.addSteps(steppedDoc, [step.getMap()], step);
|
|
114
115
|
}
|
|
115
116
|
}
|
|
@@ -149,7 +150,7 @@ var calculateDiffDecorationsInner = function calculateDiffDecorationsInner(_ref)
|
|
|
149
150
|
getMarkChangeRanges(steps).forEach(function (change) {
|
|
150
151
|
decorations.push(createInlineChangedDecoration(change, colourScheme));
|
|
151
152
|
});
|
|
152
|
-
getAttrChangeRanges(tr.doc,
|
|
153
|
+
getAttrChangeRanges(tr.doc, attrSteps).forEach(function (change) {
|
|
153
154
|
decorations.push.apply(decorations, _toConsumableArray(calculateNodesForBlockDecoration(tr.doc, change.fromB, change.toB, colourScheme)));
|
|
154
155
|
});
|
|
155
156
|
return DecorationSet.empty.add(tr.doc, decorations);
|
|
@@ -5,4 +5,13 @@ type StepRange = {
|
|
|
5
5
|
toB: number;
|
|
6
6
|
};
|
|
7
7
|
export declare const getAttrChangeRanges: (doc: PMNode, steps: ProseMirrorStep[]) => StepRange[];
|
|
8
|
+
/**
|
|
9
|
+
* Check if the step was a valid attr change and affected the doc
|
|
10
|
+
*
|
|
11
|
+
* @param step Attr step to test
|
|
12
|
+
* @param beforeDoc Doc before the step
|
|
13
|
+
* @param afterDoc Doc after the step
|
|
14
|
+
* @returns Boolean if the change should show a decoration
|
|
15
|
+
*/
|
|
16
|
+
export declare const stepIsValidAttrChange: (step: ProseMirrorStep, beforeDoc: PMNode, afterDoc: PMNode) => boolean;
|
|
8
17
|
export {};
|
|
@@ -5,4 +5,13 @@ type StepRange = {
|
|
|
5
5
|
toB: number;
|
|
6
6
|
};
|
|
7
7
|
export declare const getAttrChangeRanges: (doc: PMNode, steps: ProseMirrorStep[]) => StepRange[];
|
|
8
|
+
/**
|
|
9
|
+
* Check if the step was a valid attr change and affected the doc
|
|
10
|
+
*
|
|
11
|
+
* @param step Attr step to test
|
|
12
|
+
* @param beforeDoc Doc before the step
|
|
13
|
+
* @param afterDoc Doc after the step
|
|
14
|
+
* @returns Boolean if the change should show a decoration
|
|
15
|
+
*/
|
|
16
|
+
export declare const stepIsValidAttrChange: (step: ProseMirrorStep, beforeDoc: PMNode, afterDoc: PMNode) => boolean;
|
|
8
17
|
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atlaskit/editor-plugin-show-diff",
|
|
3
|
-
"version": "3.2.
|
|
3
|
+
"version": "3.2.3",
|
|
4
4
|
"description": "ShowDiff plugin for @atlaskit/editor-core",
|
|
5
5
|
"author": "Atlassian Pty Ltd",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -32,14 +32,14 @@
|
|
|
32
32
|
"@atlaskit/editor-prosemirror": "7.0.0",
|
|
33
33
|
"@atlaskit/editor-tables": "^2.9.0",
|
|
34
34
|
"@atlaskit/platform-feature-flags": "^1.1.0",
|
|
35
|
-
"@atlaskit/tokens": "^
|
|
35
|
+
"@atlaskit/tokens": "^8.0.0",
|
|
36
36
|
"@babel/runtime": "^7.0.0",
|
|
37
37
|
"lodash": "^4.17.21",
|
|
38
38
|
"memoize-one": "^6.0.0",
|
|
39
39
|
"prosemirror-changeset": "^2.2.1"
|
|
40
40
|
},
|
|
41
41
|
"peerDependencies": {
|
|
42
|
-
"@atlaskit/editor-common": "^110.
|
|
42
|
+
"@atlaskit/editor-common": "^110.24.0",
|
|
43
43
|
"react": "^18.2.0"
|
|
44
44
|
},
|
|
45
45
|
"techstack": {
|