@lowdefy/blocks-diff 0.0.0-experimental-20260420110710

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.
@@ -0,0 +1,231 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */ import diff from 'microdiff';
16
+ import { type } from '@lowdefy/helpers';
17
+ import breadcrumbLabel from './breadcrumbLabel.js';
18
+ import { CHANGE_TYPES, GROUP_ROOT } from './constants.js';
19
+ import { getValueAtPath, matchesPath, pathLabel, pathToString, resolveFormatter } from './pathUtils.js';
20
+ const MICRODIFF_TYPE_MAP = {
21
+ CREATE: CHANGE_TYPES.CREATE,
22
+ REMOVE: CHANGE_TYPES.REMOVE,
23
+ CHANGE: CHANGE_TYPES.CHANGE
24
+ };
25
+ // microdiff only accepts objects or arrays. Anything else (null, primitive)
26
+ // becomes an empty object so "before was nothing" still produces a clean diff.
27
+ function toInput(value) {
28
+ if (type.isArray(value)) return value;
29
+ if (type.isObject(value)) return value;
30
+ return {};
31
+ }
32
+ function enrichChange(entry, { labels, format }) {
33
+ const pathStr = pathToString(entry.path);
34
+ const mappedType = MICRODIFF_TYPE_MAP[entry.type] ?? CHANGE_TYPES.CHANGE;
35
+ let formatter = resolveFormatter(pathStr, format);
36
+ if (type.isNone(formatter) && mappedType === CHANGE_TYPES.CHANGE && (type.isArray(entry.value) || type.isObject(entry.value))) {
37
+ formatter = {
38
+ type: 'json'
39
+ };
40
+ }
41
+ return {
42
+ type: mappedType,
43
+ path: entry.path.slice(),
44
+ pathStr,
45
+ displayPath: pathToString(entry.path, {
46
+ display: true
47
+ }),
48
+ label: pathLabel(entry.path, labels),
49
+ oldValue: entry.type === 'CREATE' ? undefined : entry.oldValue,
50
+ newValue: entry.type === 'REMOVE' ? undefined : entry.value,
51
+ formatter,
52
+ depth: entry.path.length,
53
+ breadcrumb: breadcrumbLabel(entry.path, labels)
54
+ };
55
+ }
56
+ function collectLeafPaths(value, basePath, collected, visited) {
57
+ if (type.isArray(value) || type.isObject(value)) {
58
+ if (visited.has(value)) return;
59
+ visited.add(value);
60
+ const keys = type.isArray(value) ? value.map((_, i)=>i) : Object.keys(value);
61
+ if (keys.length === 0) {
62
+ collected.push(basePath.slice());
63
+ return;
64
+ }
65
+ keys.forEach((key)=>{
66
+ collectLeafPaths(value[key], basePath.concat(key), collected, visited);
67
+ });
68
+ return;
69
+ }
70
+ collected.push(basePath.slice());
71
+ }
72
+ function synthesiseUnchanged({ before, after, existingPathSet, labels, format }) {
73
+ const leafPaths = [];
74
+ collectLeafPaths(after, [], leafPaths, new WeakSet());
75
+ collectLeafPaths(before, [], leafPaths, new WeakSet());
76
+ const seen = new Set();
77
+ const results = [];
78
+ leafPaths.forEach((path)=>{
79
+ const pathStr = pathToString(path);
80
+ if (!pathStr || seen.has(pathStr) || existingPathSet.has(pathStr)) return;
81
+ seen.add(pathStr);
82
+ const beforeVal = getValueAtPath(before, path);
83
+ const afterVal = getValueAtPath(after, path);
84
+ if (beforeVal === afterVal || JSON.stringify(beforeVal) === JSON.stringify(afterVal)) {
85
+ results.push({
86
+ type: CHANGE_TYPES.UNCHANGED,
87
+ path: path.slice(),
88
+ pathStr,
89
+ displayPath: pathToString(path, {
90
+ display: true
91
+ }),
92
+ label: pathLabel(path, labels),
93
+ oldValue: beforeVal,
94
+ newValue: afterVal,
95
+ formatter: resolveFormatter(pathStr, format),
96
+ depth: path.length,
97
+ breadcrumb: breadcrumbLabel(path, labels)
98
+ });
99
+ }
100
+ });
101
+ return results;
102
+ }
103
+ function applyFilters(changes, { hide, show }) {
104
+ let filtered = changes;
105
+ if (type.isArray(show) && show.length > 0) {
106
+ filtered = filtered.filter((change)=>matchesPath(change.pathStr, show));
107
+ }
108
+ if (type.isArray(hide) && hide.length > 0) {
109
+ filtered = filtered.filter((change)=>!matchesPath(change.pathStr, hide));
110
+ }
111
+ return filtered;
112
+ }
113
+ function summariseGroup(changes) {
114
+ const summary = {
115
+ added: 0,
116
+ removed: 0,
117
+ changed: 0,
118
+ unchanged: 0
119
+ };
120
+ let hasArrayIndices = false;
121
+ changes.forEach((change)=>{
122
+ if (change.type === CHANGE_TYPES.CREATE) summary.added += 1;
123
+ else if (change.type === CHANGE_TYPES.REMOVE) summary.removed += 1;
124
+ else if (change.type === CHANGE_TYPES.CHANGE) summary.changed += 1;
125
+ else if (change.type === CHANGE_TYPES.UNCHANGED) summary.unchanged += 1;
126
+ if (change.path.length > 1 && /^\d+$/.test(String(change.path[1]))) {
127
+ hasArrayIndices = true;
128
+ }
129
+ });
130
+ return {
131
+ ...summary,
132
+ hasArrayIndices
133
+ };
134
+ }
135
+ function groupChanges(changes, { groupByRoot }) {
136
+ if (!groupByRoot) {
137
+ return [
138
+ {
139
+ key: GROUP_ROOT,
140
+ label: '',
141
+ changes,
142
+ summary: summariseGroup(changes)
143
+ }
144
+ ];
145
+ }
146
+ const byKey = new Map();
147
+ changes.forEach((change)=>{
148
+ const key = change.path.length <= 1 ? GROUP_ROOT : String(change.path[0]);
149
+ if (!byKey.has(key)) byKey.set(key, []);
150
+ byKey.get(key).push(change);
151
+ });
152
+ return Array.from(byKey.entries()).map(([key, groupChangesList])=>({
153
+ key,
154
+ label: key === GROUP_ROOT ? '' : key,
155
+ changes: groupChangesList,
156
+ summary: summariseGroup(groupChangesList)
157
+ }));
158
+ }
159
+ function collapseDeep(raw, { maxDepth, before, after }) {
160
+ if (!type.isInt(maxDepth) || maxDepth < 1) return raw;
161
+ const passthrough = [];
162
+ const bucketOrder = [];
163
+ const buckets = new Map();
164
+ raw.forEach((entry)=>{
165
+ if (!type.isArray(entry.path) || entry.path.length <= maxDepth) {
166
+ passthrough.push(entry);
167
+ return;
168
+ }
169
+ const parent = entry.path.slice(0, maxDepth);
170
+ const key = parent.join('.');
171
+ if (!buckets.has(key)) {
172
+ buckets.set(key, parent);
173
+ bucketOrder.push(key);
174
+ }
175
+ });
176
+ const collapsed = [];
177
+ bucketOrder.forEach((key)=>{
178
+ const parent = buckets.get(key);
179
+ const oldValue = getValueAtPath(before, parent);
180
+ const newValue = getValueAtPath(after, parent);
181
+ if (JSON.stringify(oldValue) === JSON.stringify(newValue)) return;
182
+ collapsed.push({
183
+ type: 'CHANGE',
184
+ path: parent,
185
+ oldValue,
186
+ value: newValue
187
+ });
188
+ });
189
+ return passthrough.concat(collapsed);
190
+ }
191
+ function buildDiffModel({ before, after, options = {} }) {
192
+ const { labels, hide, show, format, showUnchanged = false, groupByRoot = true, maxDepth = 4 } = options;
193
+ const safeBefore = toInput(before);
194
+ const safeAfter = toInput(after);
195
+ const rawChanges = diff(safeBefore, safeAfter);
196
+ const collapsedRaw = collapseDeep(rawChanges, {
197
+ maxDepth,
198
+ before: safeBefore,
199
+ after: safeAfter
200
+ });
201
+ const enriched = collapsedRaw.map((entry)=>enrichChange(entry, {
202
+ labels,
203
+ format
204
+ }));
205
+ const changePathSet = new Set(enriched.map((change)=>change.pathStr));
206
+ let allChanges = enriched;
207
+ if (showUnchanged) {
208
+ const unchanged = synthesiseUnchanged({
209
+ before: safeBefore,
210
+ after: safeAfter,
211
+ existingPathSet: changePathSet,
212
+ labels,
213
+ format
214
+ });
215
+ allChanges = allChanges.concat(unchanged);
216
+ }
217
+ const filtered = applyFilters(allChanges, {
218
+ hide,
219
+ show
220
+ });
221
+ const groups = groupChanges(filtered, {
222
+ groupByRoot
223
+ });
224
+ const hasMeaningfulChanges = filtered.some((change)=>change.type !== CHANGE_TYPES.UNCHANGED);
225
+ return {
226
+ groups,
227
+ empty: !hasMeaningfulChanges && filtered.length === 0,
228
+ total: filtered.length
229
+ };
230
+ }
231
+ export default buildDiffModel;
@@ -0,0 +1,36 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */ export const CHANGE_TYPES = {
16
+ CREATE: 'CREATE',
17
+ REMOVE: 'REMOVE',
18
+ CHANGE: 'CHANGE',
19
+ UNCHANGED: 'UNCHANGED'
20
+ };
21
+ // Sentinel for the synthetic group that holds top-level primitive changes when
22
+ // groupByRoot is enabled, and every change when groupByRoot is false. Leading
23
+ // underscores keep it from colliding with user keys in typical datasets.
24
+ export const GROUP_ROOT = '__root__';
25
+ export const DEFAULT_CHANGE_TYPE_LABELS = {
26
+ added: 'Added',
27
+ removed: 'Removed',
28
+ changed: 'Changed',
29
+ unchanged: 'Unchanged'
30
+ };
31
+ export const CHANGE_TYPE_TAG_COLORS = {
32
+ [CHANGE_TYPES.CREATE]: 'success',
33
+ [CHANGE_TYPES.REMOVE]: 'error',
34
+ [CHANGE_TYPES.CHANGE]: 'warning',
35
+ [CHANGE_TYPES.UNCHANGED]: 'default'
36
+ };
@@ -0,0 +1,111 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */ import React from 'react';
16
+ import { Tag, Typography } from 'antd';
17
+ import dayjs from 'dayjs';
18
+ import { type } from '@lowdefy/helpers';
19
+ const { Text } = Typography;
20
+ function renderEmpty() {
21
+ return /*#__PURE__*/ React.createElement(Text, {
22
+ type: "secondary"
23
+ }, "—");
24
+ }
25
+ function renderDate(value, { pattern = 'YYYY-MM-DD' } = {}) {
26
+ const parsed = dayjs(value);
27
+ if (!parsed.isValid()) return /*#__PURE__*/ React.createElement(Text, null, String(value));
28
+ return /*#__PURE__*/ React.createElement(Text, null, parsed.format(pattern));
29
+ }
30
+ function renderDateTime(value, { pattern = 'YYYY-MM-DD HH:mm' } = {}) {
31
+ const parsed = dayjs(value);
32
+ if (!parsed.isValid()) return /*#__PURE__*/ React.createElement(Text, null, String(value));
33
+ return /*#__PURE__*/ React.createElement(Text, null, parsed.format(pattern));
34
+ }
35
+ function renderBoolean(value, { yes = 'Yes', no = 'No' } = {}) {
36
+ const truthy = Boolean(value);
37
+ return /*#__PURE__*/ React.createElement(Tag, {
38
+ color: truthy ? 'success' : 'default'
39
+ }, truthy ? yes : no);
40
+ }
41
+ function renderCurrency(value, { locale, currency = 'USD' } = {}) {
42
+ const number = Number(value);
43
+ if (!Number.isFinite(number)) return /*#__PURE__*/ React.createElement(Text, null, String(value));
44
+ try {
45
+ const formatted = new Intl.NumberFormat(locale, {
46
+ style: 'currency',
47
+ currency
48
+ }).format(number);
49
+ return /*#__PURE__*/ React.createElement(Text, null, formatted);
50
+ } catch (_err) {
51
+ return /*#__PURE__*/ React.createElement(Text, null, String(value));
52
+ }
53
+ }
54
+ function renderJson(value) {
55
+ return /*#__PURE__*/ React.createElement("pre", {
56
+ style: {
57
+ margin: 0,
58
+ padding: '8px 10px',
59
+ fontSize: '0.85em',
60
+ background: 'var(--ant-color-fill-tertiary, rgba(0,0,0,0.04))',
61
+ border: '1px solid var(--ant-color-border-secondary, rgba(0,0,0,0.06))',
62
+ borderRadius: 'var(--ant-border-radius-sm, 4px)',
63
+ whiteSpace: 'pre-wrap',
64
+ wordBreak: 'break-word'
65
+ }
66
+ }, JSON.stringify(value, null, 2));
67
+ }
68
+ function renderCode(value) {
69
+ return /*#__PURE__*/ React.createElement(Typography.Text, {
70
+ code: true
71
+ }, String(value));
72
+ }
73
+ function renderEnum(value, { map = {} } = {}) {
74
+ const key = String(value);
75
+ const entry = map[key];
76
+ if (type.isNone(entry)) return /*#__PURE__*/ React.createElement(Tag, null, key);
77
+ if (type.isString(entry)) return /*#__PURE__*/ React.createElement(Tag, null, entry);
78
+ if (type.isObject(entry)) {
79
+ return /*#__PURE__*/ React.createElement(Tag, {
80
+ color: entry.color
81
+ }, entry.label ?? key);
82
+ }
83
+ return /*#__PURE__*/ React.createElement(Tag, null, key);
84
+ }
85
+ function renderFallback(value) {
86
+ if (type.isPrimitive(value)) return /*#__PURE__*/ React.createElement(Text, null, String(value));
87
+ return renderJson(value);
88
+ }
89
+ function formatValue(value, formatter) {
90
+ if (type.isNone(value)) return renderEmpty();
91
+ const formatType = type.isObject(formatter) ? formatter.type : undefined;
92
+ switch(formatType){
93
+ case 'date':
94
+ return renderDate(value, formatter);
95
+ case 'datetime':
96
+ return renderDateTime(value, formatter);
97
+ case 'boolean':
98
+ return renderBoolean(value, formatter);
99
+ case 'currency':
100
+ return renderCurrency(value, formatter);
101
+ case 'json':
102
+ return renderJson(value);
103
+ case 'code':
104
+ return renderCode(value);
105
+ case 'enum':
106
+ return renderEnum(value, formatter);
107
+ default:
108
+ return renderFallback(value);
109
+ }
110
+ }
111
+ export default formatValue;
@@ -0,0 +1,99 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */ import pluralize from 'pluralize';
16
+ import { type } from '@lowdefy/helpers';
17
+ export function isIndex(segment) {
18
+ return type.isInt(segment) || type.isString(segment) && /^\d+$/.test(segment);
19
+ }
20
+ function joinInternal(path) {
21
+ return path.map((segment)=>String(segment)).join('.');
22
+ }
23
+ function joinDisplay(path) {
24
+ return path.reduce((acc, segment, index)=>{
25
+ if (isIndex(segment)) return `${acc}[${segment}]`;
26
+ return index === 0 ? String(segment) : `${acc}.${segment}`;
27
+ }, '');
28
+ }
29
+ export function pathToString(path, { display = false } = {}) {
30
+ if (!type.isArray(path)) return '';
31
+ return display ? joinDisplay(path) : joinInternal(path);
32
+ }
33
+ export function matchesPath(pathStr, patterns) {
34
+ if (!type.isArray(patterns) || patterns.length === 0) return false;
35
+ return patterns.some((pattern)=>{
36
+ if (!type.isString(pattern) || pattern === '') return false;
37
+ if (pattern === pathStr) return true;
38
+ if (pattern.endsWith('.*')) {
39
+ const prefix = pattern.slice(0, -2);
40
+ return pathStr === prefix || pathStr.startsWith(`${prefix}.`);
41
+ }
42
+ if (pattern.startsWith('*.')) {
43
+ const suffix = pattern.slice(2);
44
+ return pathStr === suffix || pathStr.endsWith(`.${suffix}`);
45
+ }
46
+ return false;
47
+ });
48
+ }
49
+ export function humaniseSegment(segment) {
50
+ const str = String(segment);
51
+ const spaced = str.replace(/([a-z0-9])([A-Z])/g, '$1 $2').replace(/[_-]+/g, ' ').trim();
52
+ if (spaced.length === 0) return str;
53
+ return spaced.charAt(0).toUpperCase() + spaced.slice(1);
54
+ }
55
+ export function pathLabel(path, labelsMap) {
56
+ if (!type.isArray(path) || path.length === 0) return '';
57
+ const pathStr = pathToString(path);
58
+ if (type.isObject(labelsMap) && type.isString(labelsMap[pathStr])) {
59
+ return labelsMap[pathStr];
60
+ }
61
+ const leaf = path[path.length - 1];
62
+ if (isIndex(leaf)) {
63
+ if (path.length > 1) {
64
+ const parent = pathToString(path.slice(0, -1));
65
+ const parentLabel = type.isObject(labelsMap) && type.isString(labelsMap[parent]) ? labelsMap[parent] : humaniseSegment(path[path.length - 2]);
66
+ return `${parentLabel} [${leaf}]`;
67
+ }
68
+ return `[${leaf}]`;
69
+ }
70
+ return humaniseSegment(leaf);
71
+ }
72
+ export function resolveFormatter(pathStr, formatMap) {
73
+ if (!type.isObject(formatMap)) return undefined;
74
+ if (type.isObject(formatMap[pathStr])) return formatMap[pathStr];
75
+ const keys = Object.keys(formatMap);
76
+ for (const key of keys){
77
+ if (key.endsWith('.*')) {
78
+ const prefix = key.slice(0, -2);
79
+ if (pathStr === prefix || pathStr.startsWith(`${prefix}.`)) return formatMap[key];
80
+ } else if (key.startsWith('*.')) {
81
+ const suffix = key.slice(2);
82
+ if (pathStr === suffix || pathStr.endsWith(`.${suffix}`)) return formatMap[key];
83
+ }
84
+ }
85
+ return undefined;
86
+ }
87
+ export function getValueAtPath(obj, path) {
88
+ let current = obj;
89
+ for (const segment of path){
90
+ if (type.isNone(current)) return undefined;
91
+ current = current[segment];
92
+ }
93
+ return current;
94
+ }
95
+ export function singularise(label) {
96
+ if (!type.isString(label) || label.length === 0) return label;
97
+ return pluralize.singular(label);
98
+ }
99
+ export { default as breadcrumbLabel } from './breadcrumbLabel.js';
@@ -0,0 +1,87 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */ import React from 'react';
16
+ import { diffLines } from 'diff';
17
+ import serializeYaml from '../serializeYaml.js';
18
+ const ADDED_STYLE = {
19
+ background: 'var(--ant-color-success-bg, rgba(82,196,26,0.1))',
20
+ color: 'var(--ant-color-success-text, var(--ant-color-text))',
21
+ display: 'block'
22
+ };
23
+ const REMOVED_STYLE = {
24
+ background: 'var(--ant-color-error-bg, rgba(255,77,79,0.1))',
25
+ color: 'var(--ant-color-error-text, var(--ant-color-text))',
26
+ display: 'block'
27
+ };
28
+ const CONTEXT_STYLE = {
29
+ color: 'var(--ant-color-text-secondary, rgba(0,0,0,0.65))',
30
+ display: 'block'
31
+ };
32
+ const PRE_STYLE = {
33
+ fontFamily: 'var(--ant-font-family-code, monospace)',
34
+ fontSize: 'var(--ant-font-size-sm, 12px)',
35
+ padding: '12px',
36
+ background: 'var(--ant-color-fill-tertiary, rgba(0,0,0,0.04))',
37
+ borderRadius: 'var(--ant-border-radius-sm, 4px)',
38
+ whiteSpace: 'pre',
39
+ overflowX: 'auto',
40
+ margin: 0
41
+ };
42
+ function splitHunkLines(value) {
43
+ const lines = value.split('\n');
44
+ if (lines.length > 0 && lines[lines.length - 1] === '') lines.pop();
45
+ return lines;
46
+ }
47
+ function GitDiffRenderer({ before, after, hide, show, classNames = {}, styles = {} }) {
48
+ const beforeYaml = serializeYaml(before, {
49
+ hide,
50
+ show
51
+ });
52
+ const afterYaml = serializeYaml(after, {
53
+ hide,
54
+ show
55
+ });
56
+ const hunks = diffLines(beforeYaml, afterYaml);
57
+ const nodes = [];
58
+ hunks.forEach((hunk, hunkIndex)=>{
59
+ const lines = splitHunkLines(hunk.value);
60
+ let prefix;
61
+ let style;
62
+ if (hunk.added) {
63
+ prefix = '+ ';
64
+ style = ADDED_STYLE;
65
+ } else if (hunk.removed) {
66
+ prefix = '- ';
67
+ style = REMOVED_STYLE;
68
+ } else {
69
+ prefix = ' ';
70
+ style = CONTEXT_STYLE;
71
+ }
72
+ lines.forEach((line, lineIndex)=>{
73
+ nodes.push(/*#__PURE__*/ React.createElement("span", {
74
+ key: `${hunkIndex}-${lineIndex}`,
75
+ style: style
76
+ }, `${prefix}${line}`));
77
+ });
78
+ });
79
+ return /*#__PURE__*/ React.createElement("pre", {
80
+ className: classNames.group,
81
+ style: {
82
+ ...PRE_STYLE,
83
+ ...styles.group
84
+ }
85
+ }, nodes);
86
+ }
87
+ export default GitDiffRenderer;