@astryx-vue/lab 0.1.1 → 0.2.1

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/dist/index.js CHANGED
@@ -1,6 +1,7 @@
1
- import { Fragment, Teleport, computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, createSlots, createTextVNode, createVNode, defineComponent, h, mergeModels, mergeProps, nextTick, normalizeClass, normalizeStyle, onScopeDispose, openBlock, ref, renderList, renderSlot, resolveDynamicComponent, toDisplayString, unref, useAttrs, useId, useModel, useTemplateRef, watch, withCtx } from "vue";
1
+ import { Fragment, Teleport, computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, createSlots, createTextVNode, createVNode, defineComponent, getCurrentInstance, guardReactiveProps, h, mergeModels, mergeProps, nextTick, normalizeClass, normalizeProps, normalizeStyle, onScopeDispose, openBlock, ref, renderList, renderSlot, resolveDynamicComponent, toDisplayString, unref, useAttrs, useId, useModel, useSlots, useTemplateRef, watch, withCtx } from "vue";
2
2
  import "../styles/circular-progress.css";
3
- import { AlertDialog, BottomSheet, Button, CheckboxIndicator, ComplexSelector, EmptyState, Field, FieldStatus, Icon, IconButton, Item, List, Text, TextInput, Token, Tooltip, VisuallyHidden, useAnnounce, useTranslator } from "@astryx-vue/core";
3
+ import { AlertDialog, BottomSheet, Button, CheckboxIndicator, ComplexSelector, EmptyState, Field, FieldStatus, Icon, IconButton, Item, List, SyntaxTheme, Text, TextInput, Token, Tooltip, VisuallyHidden, tokenize, useAnnounce, useScrollableArea, useTranslator } from "@astryx-vue/core";
4
+ import "../styles/diff-viewer.css";
4
5
  import "../styles/info-tip.css";
5
6
  import "../styles/list-input.css";
6
7
  import "../styles/log-stream.css";
@@ -31,9 +32,9 @@ function defaultFormatValueLabel(value, max) {
31
32
  }
32
33
  //#endregion
33
34
  //#region src/CircularProgress/CircularProgress.vue
34
- const _hoisted_1$11 = ["data-size", "data-variant"];
35
- const _hoisted_2$10 = { class: "astryx-circular-progress__ring" };
36
- const _hoisted_3$9 = [
35
+ const _hoisted_1$13 = ["data-size", "data-variant"];
36
+ const _hoisted_2$12 = { class: "astryx-circular-progress__ring" };
37
+ const _hoisted_3$11 = [
37
38
  "aria-valuenow",
38
39
  "aria-valuemin",
39
40
  "aria-valuemax",
@@ -42,21 +43,21 @@ const _hoisted_3$9 = [
42
43
  "height",
43
44
  "viewBox"
44
45
  ];
45
- const _hoisted_4$8 = [
46
+ const _hoisted_4$10 = [
46
47
  "data-variant",
47
48
  "cx",
48
49
  "cy",
49
50
  "r",
50
51
  "stroke-width"
51
52
  ];
52
- const _hoisted_5$7 = [
53
+ const _hoisted_5$9 = [
53
54
  "data-variant",
54
55
  "cx",
55
56
  "cy",
56
57
  "r",
57
58
  "stroke-width"
58
59
  ];
59
- const _hoisted_6$6 = [
60
+ const _hoisted_6$8 = [
60
61
  "data-variant",
61
62
  "cx",
62
63
  "cy",
@@ -65,7 +66,7 @@ const _hoisted_6$6 = [
65
66
  "stroke-dasharray",
66
67
  "stroke-dashoffset"
67
68
  ];
68
- const _hoisted_7$4 = {
69
+ const _hoisted_7$6 = {
69
70
  key: 0,
70
71
  class: "astryx-circular-progress__center"
71
72
  };
@@ -137,7 +138,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
137
138
  "astryx-circular-progress__visually-hidden": !showLabel.value,
138
139
  "astryx-circular-progress__label--disabled": showLabel.value && __props.isDisabled
139
140
  }])
140
- }, toDisplayString(__props.label), 3), createElementVNode("div", _hoisted_2$10, [(openBlock(), createElementBlock("svg", {
141
+ }, toDisplayString(__props.label), 3), createElementVNode("div", _hoisted_2$12, [(openBlock(), createElementBlock("svg", {
141
142
  class: normalizeClass(["astryx-circular-progress__svg", { "astryx-circular-progress__svg--indeterminate": __props.isIndeterminate }]),
142
143
  role: "progressbar",
143
144
  "aria-labelledby": labelId,
@@ -155,7 +156,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
155
156
  cy: geometry.value.center,
156
157
  r: geometry.value.radius,
157
158
  "stroke-width": geometry.value.strokeWidth
158
- }, null, 8, _hoisted_4$8), __props.isIndeterminate ? (openBlock(), createElementBlock("circle", {
159
+ }, null, 8, _hoisted_4$10), __props.isIndeterminate ? (openBlock(), createElementBlock("circle", {
159
160
  key: 0,
160
161
  class: "astryx-circular-progress__fill astryx-circular-progress__fill--indeterminate",
161
162
  "data-variant": fillVariant.value,
@@ -163,7 +164,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
163
164
  cy: geometry.value.center,
164
165
  r: geometry.value.radius,
165
166
  "stroke-width": geometry.value.strokeWidth
166
- }, null, 8, _hoisted_5$7)) : (openBlock(), createElementBlock("circle", {
167
+ }, null, 8, _hoisted_5$9)) : (openBlock(), createElementBlock("circle", {
167
168
  key: 1,
168
169
  class: "astryx-circular-progress__fill",
169
170
  "data-variant": fillVariant.value,
@@ -173,17 +174,1531 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
173
174
  "stroke-width": geometry.value.strokeWidth,
174
175
  "stroke-dasharray": geometry.value.circumference,
175
176
  "stroke-dashoffset": dashoffset.value
176
- }, null, 8, _hoisted_6$6))], 10, _hoisted_3$9)), _ctx.$slots.default || hasCenterContent.value ? (openBlock(), createElementBlock("div", _hoisted_7$4, [renderSlot(_ctx.$slots, "default", {}, () => [hasCenterContent.value ? (openBlock(), createElementBlock("span", {
177
+ }, null, 8, _hoisted_6$8))], 10, _hoisted_3$11)), _ctx.$slots.default || hasCenterContent.value ? (openBlock(), createElementBlock("div", _hoisted_7$6, [renderSlot(_ctx.$slots, "default", {}, () => [hasCenterContent.value ? (openBlock(), createElementBlock("span", {
177
178
  key: 0,
178
179
  class: normalizeClass(["astryx-circular-progress__value", { "astryx-circular-progress__value--disabled": __props.isDisabled }])
179
- }, toDisplayString(valueText.value), 3)) : createCommentVNode("v-if", true)])])) : createCommentVNode("v-if", true)])], 16, _hoisted_1$11);
180
+ }, toDisplayString(valueText.value), 3)) : createCommentVNode("v-if", true)])])) : createCommentVNode("v-if", true)])], 16, _hoisted_1$13);
180
181
  };
181
182
  }
182
183
  });
183
184
  //#endregion
185
+ //#region src/DiffViewer/createDiffFile.ts
186
+ function splitLines(value) {
187
+ if (value === "") return [];
188
+ const lines = value.split("\n").map((line) => line.replace(/\r$/, ""));
189
+ if (lines[lines.length - 1] === "") lines.pop();
190
+ return lines;
191
+ }
192
+ /**
193
+ * Most changed lines the minimal search explores before giving up. The trace
194
+ * it keeps grows with the square of this, so the cap bounds memory to a few
195
+ * megabytes; past it the changed middle is shown as one replaced block.
196
+ */
197
+ const MAX_EDIT_DISTANCE = 2e3;
198
+ /**
199
+ * Line edit script from `a` to `b`. Shared leading and trailing lines are
200
+ * context without a search; the middle uses Myers' O(ND) shortest edit script,
201
+ * or, when it needs more than MAX_EDIT_DISTANCE edits, removes the old middle
202
+ * and adds the new one.
203
+ */
204
+ function diffLines(a, b) {
205
+ let head = 0;
206
+ while (head < a.length && head < b.length && a[head] === b[head]) head++;
207
+ let tailA = a.length;
208
+ let tailB = b.length;
209
+ while (tailA > head && tailB > head && a[tailA - 1] === b[tailB - 1]) {
210
+ tailA--;
211
+ tailB--;
212
+ }
213
+ const ops = [];
214
+ for (let i = 0; i < head; i++) ops.push({
215
+ kind: "context",
216
+ a: i,
217
+ b: i
218
+ });
219
+ const middle = myers(a, b, head, tailA, tailB);
220
+ if (middle != null) for (const op of middle) ops.push(op);
221
+ else {
222
+ for (let i = head; i < tailA; i++) ops.push({
223
+ kind: "removed",
224
+ a: i
225
+ });
226
+ for (let j = head; j < tailB; j++) ops.push({
227
+ kind: "added",
228
+ b: j
229
+ });
230
+ }
231
+ for (let i = 0; tailA + i < a.length; i++) ops.push({
232
+ kind: "context",
233
+ a: tailA + i,
234
+ b: tailB + i
235
+ });
236
+ return ops;
237
+ }
238
+ /**
239
+ * Myers' shortest edit script over `a[start, endA)` and `b[start, endB)`,
240
+ * with absolute indexes in the ops; null when it needs more than
241
+ * MAX_EDIT_DISTANCE edits.
242
+ */
243
+ function myers(a, b, start, endA, endB) {
244
+ const n = endA - start;
245
+ const m = endB - start;
246
+ const max = n + m;
247
+ const offset = max + 1;
248
+ const v = new Int32Array(2 * max + 3);
249
+ const trace = [];
250
+ let done = false;
251
+ for (let d = 0; d <= max && !done; d++) {
252
+ if (d > MAX_EDIT_DISTANCE) return null;
253
+ trace.push(v.slice(offset - d, offset + d + 1));
254
+ for (let k = -d; k <= d; k += 2) {
255
+ let x = k === -d || k !== d && v[offset + k - 1] < v[offset + k + 1] ? v[offset + k + 1] : v[offset + k - 1] + 1;
256
+ let y = x - k;
257
+ while (x < n && y < m && a[start + x] === b[start + y]) {
258
+ x++;
259
+ y++;
260
+ }
261
+ v[offset + k] = x;
262
+ if (x >= n && y >= m) {
263
+ done = true;
264
+ break;
265
+ }
266
+ }
267
+ }
268
+ const ops = [];
269
+ let x = n;
270
+ let y = m;
271
+ for (let d = trace.length - 1; d >= 0; d--) {
272
+ const vd = trace[d];
273
+ const at = (k) => vd[k + d] ?? 0;
274
+ const k = x - y;
275
+ const prevK = k === -d || k !== d && at(k - 1) < at(k + 1) ? k + 1 : k - 1;
276
+ const prevX = at(prevK);
277
+ const prevY = prevX - prevK;
278
+ while (x > prevX && y > prevY) {
279
+ x--;
280
+ y--;
281
+ ops.push({
282
+ kind: "context",
283
+ a: start + x,
284
+ b: start + y
285
+ });
286
+ }
287
+ if (d > 0) {
288
+ if (x === prevX) ops.push({
289
+ kind: "added",
290
+ b: start + prevY
291
+ });
292
+ else ops.push({
293
+ kind: "removed",
294
+ a: start + prevX
295
+ });
296
+ }
297
+ x = prevX;
298
+ y = prevY;
299
+ }
300
+ ops.reverse();
301
+ return ops;
302
+ }
303
+ function formatRange(start, count) {
304
+ return count === 1 ? `${start}` : `${start},${count}`;
305
+ }
306
+ /**
307
+ * Compute a {@link DiffViewerFile} from two versions of a text, for callers
308
+ * that hold file contents rather than a patch.
309
+ *
310
+ * @example
311
+ * ```
312
+ * const file = createDiffFile({oldValue: before, newValue: after, path: 'src/app.ts'});
313
+ * <DiffViewer file={file} />
314
+ * ```
315
+ */
316
+ function createDiffFile({ oldValue, newValue, path, oldPath: oldPathOption, newPath: newPathOption, contextLines = 3 }) {
317
+ const oldPath = oldPathOption === void 0 ? path ?? null : oldPathOption;
318
+ const newPath = newPathOption === void 0 ? path ?? null : newPathOption;
319
+ const context = Number.isNaN(contextLines) ? 3 : Math.max(0, Math.floor(contextLines));
320
+ let status = "modified";
321
+ if (oldPathOption === null) status = "added";
322
+ else if (newPathOption === null) status = "deleted";
323
+ else if (oldPath != null && newPath != null && oldPath !== newPath) status = "renamed";
324
+ const a = splitLines(oldValue);
325
+ const b = splitLines(newValue);
326
+ const ops = diffLines(a, b);
327
+ const changed = [];
328
+ ops.forEach((op, i) => {
329
+ if (op.kind !== "context") changed.push(i);
330
+ });
331
+ const hunks = [];
332
+ let g = 0;
333
+ while (g < changed.length) {
334
+ let last = changed[g];
335
+ let h = g + 1;
336
+ while (h < changed.length && changed[h] - last - 1 <= context * 2) {
337
+ last = changed[h];
338
+ h++;
339
+ }
340
+ const start = Math.max(0, changed[g] - context);
341
+ const end = Math.min(ops.length - 1, last + context);
342
+ const lines = [];
343
+ let oldCount = 0;
344
+ let newCount = 0;
345
+ for (let i = start; i <= end; i++) {
346
+ const op = ops[i];
347
+ if (op.kind === "context") {
348
+ lines.push({
349
+ kind: "context",
350
+ content: a[op.a],
351
+ oldLineNumber: op.a + 1,
352
+ newLineNumber: op.b + 1
353
+ });
354
+ oldCount++;
355
+ newCount++;
356
+ } else if (op.kind === "removed") {
357
+ lines.push({
358
+ kind: "removed",
359
+ content: a[op.a],
360
+ oldLineNumber: op.a + 1,
361
+ newLineNumber: null
362
+ });
363
+ oldCount++;
364
+ } else {
365
+ lines.push({
366
+ kind: "added",
367
+ content: b[op.b],
368
+ oldLineNumber: null,
369
+ newLineNumber: op.b + 1
370
+ });
371
+ newCount++;
372
+ }
373
+ }
374
+ let oldBefore = 0;
375
+ let newBefore = 0;
376
+ for (let i = 0; i < start; i++) {
377
+ const op = ops[i];
378
+ if (op.kind !== "added") oldBefore++;
379
+ if (op.kind !== "removed") newBefore++;
380
+ }
381
+ const oldStart = oldCount === 0 ? oldBefore : oldBefore + 1;
382
+ const newStart = newCount === 0 ? newBefore : newBefore + 1;
383
+ hunks.push({
384
+ header: `@@ -${formatRange(oldStart, oldCount)} +${formatRange(newStart, newCount)} @@`,
385
+ oldStart,
386
+ oldLines: oldCount,
387
+ newStart,
388
+ newLines: newCount,
389
+ lines
390
+ });
391
+ g = h;
392
+ }
393
+ return {
394
+ oldPath: status === "added" ? null : oldPath,
395
+ newPath: status === "deleted" ? null : newPath,
396
+ status,
397
+ isBinary: false,
398
+ hunks
399
+ };
400
+ }
401
+ //#endregion
402
+ //#region src/DiffViewer/RenderVNode.ts
403
+ var RenderVNode_default = defineComponent({
404
+ name: "AstryxDiffViewerRenderVNode",
405
+ props: { content: { required: true } },
406
+ setup(props) {
407
+ return () => props.content;
408
+ }
409
+ });
410
+ //#endregion
411
+ //#region src/DiffViewer/DiffViewerTable.vue
412
+ const _hoisted_1$12 = {
413
+ key: 0,
414
+ scope: "col",
415
+ class: "astryx-diff-viewer__column-header"
416
+ };
417
+ const _hoisted_2$11 = {
418
+ key: 1,
419
+ scope: "col",
420
+ class: "astryx-diff-viewer__column-header"
421
+ };
422
+ const _hoisted_3$10 = {
423
+ scope: "col",
424
+ class: "astryx-diff-viewer__column-header"
425
+ };
426
+ const _hoisted_4$9 = {
427
+ scope: "col",
428
+ class: "astryx-diff-viewer__column-header"
429
+ };
430
+ const _hoisted_5$8 = { key: 0 };
431
+ const _hoisted_6$7 = ["colspan"];
432
+ const _hoisted_7$5 = { class: "astryx-diff-viewer__ltr" };
433
+ const _hoisted_8$4 = ["aria-label", "onClick"];
434
+ const _hoisted_9$4 = ["aria-label", "onClick"];
435
+ const _hoisted_10$4 = ["data-diff"];
436
+ const _hoisted_11$2 = { class: "astryx-diff-viewer__ltr" };
437
+ const _hoisted_12$1 = ["data-kind"];
438
+ const _hoisted_13 = { key: 0 };
439
+ const _hoisted_14 = ["colspan"];
440
+ const _hoisted_15 = { class: "astryx-diff-viewer__annotation-content" };
441
+ const _hoisted_16 = {
442
+ key: 0,
443
+ class: "astryx-diff-viewer__gutter-col"
444
+ };
445
+ const _hoisted_17 = {
446
+ key: 1,
447
+ class: "astryx-diff-viewer__gutter-col"
448
+ };
449
+ const _hoisted_18 = {
450
+ key: 0,
451
+ scope: "col",
452
+ class: "astryx-diff-viewer__column-header"
453
+ };
454
+ const _hoisted_19 = {
455
+ scope: "col",
456
+ class: "astryx-diff-viewer__column-header"
457
+ };
458
+ const _hoisted_20 = {
459
+ scope: "col",
460
+ class: "astryx-diff-viewer__column-header"
461
+ };
462
+ const _hoisted_21 = {
463
+ key: 1,
464
+ scope: "col",
465
+ class: "astryx-diff-viewer__column-header"
466
+ };
467
+ const _hoisted_22 = {
468
+ scope: "col",
469
+ class: "astryx-diff-viewer__column-header"
470
+ };
471
+ const _hoisted_23 = {
472
+ scope: "col",
473
+ class: "astryx-diff-viewer__column-header"
474
+ };
475
+ const _hoisted_24 = { key: 0 };
476
+ const _hoisted_25 = ["colspan"];
477
+ const _hoisted_26 = { class: "astryx-diff-viewer__ltr" };
478
+ const _hoisted_27 = ["aria-label", "onClick"];
479
+ const _hoisted_28 = ["data-diff"];
480
+ const _hoisted_29 = { class: "astryx-diff-viewer__ltr" };
481
+ const _hoisted_30 = ["data-kind"];
482
+ const _hoisted_31 = ["aria-label", "onClick"];
483
+ const _hoisted_32 = ["data-diff"];
484
+ const _hoisted_33 = { class: "astryx-diff-viewer__ltr" };
485
+ const _hoisted_34 = ["data-kind"];
486
+ const _hoisted_35 = { key: 0 };
487
+ const _hoisted_36 = ["colspan"];
488
+ const _hoisted_37 = {
489
+ key: 0,
490
+ class: "astryx-diff-viewer__annotation-content"
491
+ };
492
+ const _hoisted_38 = ["colspan"];
493
+ const _hoisted_39 = {
494
+ key: 0,
495
+ class: "astryx-diff-viewer__annotation-content"
496
+ };
497
+ const _sfc_main$13 = /*@__PURE__*/ defineComponent({
498
+ name: "AstryxDiffViewerTable",
499
+ __name: "DiffViewerTable",
500
+ props: {
501
+ viewMode: {},
502
+ unifiedRows: {},
503
+ splitRows: {},
504
+ hasLineNumbers: { type: Boolean },
505
+ isWrapped: { type: Boolean },
506
+ hasLineNumberClick: { type: Boolean },
507
+ digits: {}
508
+ },
509
+ emits: ["lineNumberClick"],
510
+ setup(__props, { emit: __emit }) {
511
+ const props = __props;
512
+ const emit = __emit;
513
+ const unifiedColumns = computed(() => (props.hasLineNumbers ? 2 : 0) + 2);
514
+ const splitSideColumns = computed(() => (props.hasLineNumbers ? 1 : 0) + 2);
515
+ const tableStyle = computed(() => ({ "--astryx-diff-viewer-gutter-width": `${props.digits}ch` }));
516
+ function lineNumber(cell, side) {
517
+ if (cell == null) return null;
518
+ return side === "old" ? cell.line.oldLineNumber : cell.line.newLineNumber;
519
+ }
520
+ /** Only the gutter of the line's own anchor side is a button. */
521
+ function lineAction(cell, side) {
522
+ if (cell == null || cell.ref.side !== side || lineNumber(cell, side) == null) return null;
523
+ return cell.ref;
524
+ }
525
+ function emitLineNumberClick(cell, side, event) {
526
+ const action = lineAction(cell, side);
527
+ if (action != null) emit("lineNumberClick", action, event);
528
+ }
529
+ function lineNumberButtonLabel(side, number) {
530
+ return `${side === "old" ? "Original" : "Changed"} line ${number ?? ""}`;
531
+ }
532
+ function diffKind(cell) {
533
+ const kind = cell?.line.kind;
534
+ return kind !== "context" ? kind : void 0;
535
+ }
536
+ function cellClass(cell) {
537
+ return {
538
+ "is-added": cell?.line.kind === "added",
539
+ "is-removed": cell?.line.kind === "removed",
540
+ "is-filler": cell == null
541
+ };
542
+ }
543
+ function oldNote(row) {
544
+ if (row.type !== "pair" || row.old == null || row.old.line.kind === "context") return null;
545
+ return row.old.note ?? null;
546
+ }
547
+ function newNote(row) {
548
+ return row.type === "pair" ? row.new?.note ?? null : null;
549
+ }
550
+ return (_ctx, _cache) => {
551
+ return __props.viewMode === "unified" ? (openBlock(), createElementBlock("table", {
552
+ key: 0,
553
+ class: normalizeClass(["astryx-diff-viewer__table", { "astryx-diff-viewer__table--wrapped": __props.isWrapped }]),
554
+ style: normalizeStyle(tableStyle.value)
555
+ }, [
556
+ createElementVNode("colgroup", null, [
557
+ __props.hasLineNumbers ? (openBlock(), createElementBlock(Fragment, { key: 0 }, [_cache[0] || (_cache[0] = createElementVNode("col", { class: "astryx-diff-viewer__gutter-col" }, null, -1)), _cache[1] || (_cache[1] = createElementVNode("col", { class: "astryx-diff-viewer__gutter-col" }, null, -1))], 64)) : createCommentVNode("v-if", true),
558
+ _cache[2] || (_cache[2] = createElementVNode("col", { class: "astryx-diff-viewer__marker-col" }, null, -1)),
559
+ _cache[3] || (_cache[3] = createElementVNode("col", null, null, -1))
560
+ ]),
561
+ createElementVNode("thead", null, [createElementVNode("tr", null, [
562
+ __props.hasLineNumbers ? (openBlock(), createElementBlock("th", _hoisted_1$12, [createVNode(unref(VisuallyHidden), null, {
563
+ default: withCtx(() => [..._cache[4] || (_cache[4] = [createTextVNode("Original line number", -1)])]),
564
+ _: 1
565
+ })])) : createCommentVNode("v-if", true),
566
+ __props.hasLineNumbers ? (openBlock(), createElementBlock("th", _hoisted_2$11, [createVNode(unref(VisuallyHidden), null, {
567
+ default: withCtx(() => [..._cache[5] || (_cache[5] = [createTextVNode("Changed line number", -1)])]),
568
+ _: 1
569
+ })])) : createCommentVNode("v-if", true),
570
+ createElementVNode("th", _hoisted_3$10, [createVNode(unref(VisuallyHidden), null, {
571
+ default: withCtx(() => [..._cache[6] || (_cache[6] = [createTextVNode("Change", -1)])]),
572
+ _: 1
573
+ })]),
574
+ createElementVNode("th", _hoisted_4$9, [createVNode(unref(VisuallyHidden), null, {
575
+ default: withCtx(() => [..._cache[7] || (_cache[7] = [createTextVNode("Code", -1)])]),
576
+ _: 1
577
+ })])
578
+ ])]),
579
+ createElementVNode("tbody", null, [(openBlock(true), createElementBlock(Fragment, null, renderList(__props.unifiedRows, (row) => {
580
+ return openBlock(), createElementBlock(Fragment, { key: row.key }, [row.type === "hunk" ? (openBlock(), createElementBlock("tr", _hoisted_5$8, [createElementVNode("td", {
581
+ colspan: unifiedColumns.value,
582
+ class: "astryx-diff-viewer__hunk"
583
+ }, [createElementVNode("span", _hoisted_7$5, toDisplayString(row.header), 1)], 8, _hoisted_6$7)])) : (openBlock(), createElementBlock(Fragment, { key: 1 }, [createElementVNode("tr", null, [
584
+ __props.hasLineNumbers ? (openBlock(), createElementBlock("td", {
585
+ key: 0,
586
+ class: normalizeClass(["astryx-diff-viewer__gutter", cellClass(row.cell)])
587
+ }, [__props.hasLineNumberClick && lineAction(row.cell, "old") != null ? (openBlock(), createElementBlock("button", {
588
+ key: 0,
589
+ type: "button",
590
+ class: "astryx-diff-viewer__gutter-button",
591
+ "data-diff-line-button": "",
592
+ "aria-label": lineNumberButtonLabel("old", lineNumber(row.cell, "old")),
593
+ onClick: ($event) => emitLineNumberClick(row.cell, "old", $event)
594
+ }, toDisplayString(lineNumber(row.cell, "old")), 9, _hoisted_8$4)) : (openBlock(), createElementBlock(Fragment, { key: 1 }, [createTextVNode(toDisplayString(lineNumber(row.cell, "old")), 1)], 64))], 2)) : createCommentVNode("v-if", true),
595
+ __props.hasLineNumbers ? (openBlock(), createElementBlock("td", {
596
+ key: 1,
597
+ class: normalizeClass(["astryx-diff-viewer__gutter", cellClass(row.cell)])
598
+ }, [__props.hasLineNumberClick && lineAction(row.cell, "new") != null ? (openBlock(), createElementBlock("button", {
599
+ key: 0,
600
+ type: "button",
601
+ class: "astryx-diff-viewer__gutter-button",
602
+ "data-diff-line-button": "",
603
+ "aria-label": lineNumberButtonLabel("new", lineNumber(row.cell, "new")),
604
+ onClick: ($event) => emitLineNumberClick(row.cell, "new", $event)
605
+ }, toDisplayString(lineNumber(row.cell, "new")), 9, _hoisted_9$4)) : (openBlock(), createElementBlock(Fragment, { key: 1 }, [createTextVNode(toDisplayString(lineNumber(row.cell, "new")), 1)], 64))], 2)) : createCommentVNode("v-if", true),
606
+ createElementVNode("td", { class: normalizeClass(["astryx-diff-viewer__marker", cellClass(row.cell)]) }, [row.cell.line.kind === "added" ? (openBlock(), createElementBlock(Fragment, { key: 0 }, [_cache[9] || (_cache[9] = createElementVNode("span", { "aria-hidden": "true" }, "+", -1)), createVNode(unref(VisuallyHidden), null, {
607
+ default: withCtx(() => [..._cache[8] || (_cache[8] = [createTextVNode("Added", -1)])]),
608
+ _: 1
609
+ })], 64)) : row.cell.line.kind === "removed" ? (openBlock(), createElementBlock(Fragment, { key: 1 }, [_cache[11] || (_cache[11] = createElementVNode("span", { "aria-hidden": "true" }, "-", -1)), createVNode(unref(VisuallyHidden), null, {
610
+ default: withCtx(() => [..._cache[10] || (_cache[10] = [createTextVNode("Removed", -1)])]),
611
+ _: 1
612
+ })], 64)) : createCommentVNode("v-if", true)], 2),
613
+ createElementVNode("td", {
614
+ class: normalizeClass(["astryx-diff-viewer__code", [cellClass(row.cell), { "astryx-diff-viewer__code--wrapped": __props.isWrapped }]]),
615
+ "data-diff": diffKind(row.cell)
616
+ }, [createElementVNode("span", _hoisted_11$2, [(openBlock(true), createElementBlock(Fragment, null, renderList(row.cell.groups, (group, groupIndex) => {
617
+ return openBlock(), createElementBlock(Fragment, { key: groupIndex }, [group.changed ? (openBlock(), createElementBlock("span", {
618
+ key: 0,
619
+ class: "astryx-diff-viewer__change",
620
+ "data-diff-change": "",
621
+ "data-kind": row.cell.line.kind
622
+ }, [(openBlock(true), createElementBlock(Fragment, null, renderList(group.parts, (part, partIndex) => {
623
+ return openBlock(), createElementBlock(Fragment, { key: partIndex }, [part.type != null ? (openBlock(), createElementBlock("span", {
624
+ key: 0,
625
+ class: normalizeClass(`astryx-token-${part.type}`)
626
+ }, toDisplayString(part.text), 3)) : (openBlock(), createElementBlock(Fragment, { key: 1 }, [createTextVNode(toDisplayString(part.text), 1)], 64))], 64);
627
+ }), 128))], 8, _hoisted_12$1)) : (openBlock(true), createElementBlock(Fragment, { key: 1 }, renderList(group.parts, (part, partIndex) => {
628
+ return openBlock(), createElementBlock(Fragment, { key: partIndex }, [part.type != null ? (openBlock(), createElementBlock("span", {
629
+ key: 0,
630
+ class: normalizeClass(`astryx-token-${part.type}`)
631
+ }, toDisplayString(part.text), 3)) : (openBlock(), createElementBlock(Fragment, { key: 1 }, [createTextVNode(toDisplayString(part.text), 1)], 64))], 64);
632
+ }), 128))], 64);
633
+ }), 128))])], 10, _hoisted_10$4)
634
+ ]), row.cell.note != null ? (openBlock(), createElementBlock("tr", _hoisted_13, [createElementVNode("td", {
635
+ colspan: unifiedColumns.value,
636
+ class: "astryx-diff-viewer__annotation"
637
+ }, [createElementVNode("div", _hoisted_15, [createVNode(unref(RenderVNode_default), { content: row.cell.note }, null, 8, ["content"])])], 8, _hoisted_14)])) : createCommentVNode("v-if", true)], 64))], 64);
638
+ }), 128))])
639
+ ], 6)) : (openBlock(), createElementBlock("table", {
640
+ key: 1,
641
+ class: normalizeClass(["astryx-diff-viewer__table", { "astryx-diff-viewer__table--wrapped": __props.isWrapped }]),
642
+ style: normalizeStyle(tableStyle.value)
643
+ }, [
644
+ createElementVNode("colgroup", null, [
645
+ __props.hasLineNumbers ? (openBlock(), createElementBlock("col", _hoisted_16)) : createCommentVNode("v-if", true),
646
+ _cache[12] || (_cache[12] = createElementVNode("col", { class: "astryx-diff-viewer__marker-col" }, null, -1)),
647
+ _cache[13] || (_cache[13] = createElementVNode("col", null, null, -1)),
648
+ __props.hasLineNumbers ? (openBlock(), createElementBlock("col", _hoisted_17)) : createCommentVNode("v-if", true),
649
+ _cache[14] || (_cache[14] = createElementVNode("col", { class: "astryx-diff-viewer__marker-col" }, null, -1)),
650
+ _cache[15] || (_cache[15] = createElementVNode("col", null, null, -1))
651
+ ]),
652
+ createElementVNode("thead", null, [createElementVNode("tr", null, [
653
+ __props.hasLineNumbers ? (openBlock(), createElementBlock("th", _hoisted_18, [createVNode(unref(VisuallyHidden), null, {
654
+ default: withCtx(() => [..._cache[16] || (_cache[16] = [createTextVNode("Original line number", -1)])]),
655
+ _: 1
656
+ })])) : createCommentVNode("v-if", true),
657
+ createElementVNode("th", _hoisted_19, [createVNode(unref(VisuallyHidden), null, {
658
+ default: withCtx(() => [..._cache[17] || (_cache[17] = [createTextVNode("Change", -1)])]),
659
+ _: 1
660
+ })]),
661
+ createElementVNode("th", _hoisted_20, [createVNode(unref(VisuallyHidden), null, {
662
+ default: withCtx(() => [..._cache[18] || (_cache[18] = [createTextVNode("Original code", -1)])]),
663
+ _: 1
664
+ })]),
665
+ __props.hasLineNumbers ? (openBlock(), createElementBlock("th", _hoisted_21, [createVNode(unref(VisuallyHidden), null, {
666
+ default: withCtx(() => [..._cache[19] || (_cache[19] = [createTextVNode("Changed line number", -1)])]),
667
+ _: 1
668
+ })])) : createCommentVNode("v-if", true),
669
+ createElementVNode("th", _hoisted_22, [createVNode(unref(VisuallyHidden), null, {
670
+ default: withCtx(() => [..._cache[20] || (_cache[20] = [createTextVNode("Change", -1)])]),
671
+ _: 1
672
+ })]),
673
+ createElementVNode("th", _hoisted_23, [createVNode(unref(VisuallyHidden), null, {
674
+ default: withCtx(() => [..._cache[21] || (_cache[21] = [createTextVNode("Changed code", -1)])]),
675
+ _: 1
676
+ })])
677
+ ])]),
678
+ createElementVNode("tbody", null, [(openBlock(true), createElementBlock(Fragment, null, renderList(__props.splitRows, (row) => {
679
+ return openBlock(), createElementBlock(Fragment, { key: row.key }, [row.type === "hunk" ? (openBlock(), createElementBlock("tr", _hoisted_24, [createElementVNode("td", {
680
+ colspan: splitSideColumns.value * 2,
681
+ class: "astryx-diff-viewer__hunk"
682
+ }, [createElementVNode("span", _hoisted_26, toDisplayString(row.header), 1)], 8, _hoisted_25)])) : (openBlock(), createElementBlock(Fragment, { key: 1 }, [createElementVNode("tr", null, [
683
+ __props.hasLineNumbers ? (openBlock(), createElementBlock("td", {
684
+ key: 0,
685
+ class: normalizeClass(["astryx-diff-viewer__gutter", cellClass(row.old)])
686
+ }, [__props.hasLineNumberClick && lineAction(row.old, "old") != null ? (openBlock(), createElementBlock("button", {
687
+ key: 0,
688
+ type: "button",
689
+ class: "astryx-diff-viewer__gutter-button",
690
+ "data-diff-line-button": "",
691
+ "aria-label": lineNumberButtonLabel("old", lineNumber(row.old, "old")),
692
+ onClick: ($event) => emitLineNumberClick(row.old, "old", $event)
693
+ }, toDisplayString(lineNumber(row.old, "old")), 9, _hoisted_27)) : (openBlock(), createElementBlock(Fragment, { key: 1 }, [createTextVNode(toDisplayString(lineNumber(row.old, "old")), 1)], 64))], 2)) : createCommentVNode("v-if", true),
694
+ createElementVNode("td", { class: normalizeClass(["astryx-diff-viewer__marker", cellClass(row.old)]) }, [row.old?.line.kind === "added" ? (openBlock(), createElementBlock(Fragment, { key: 0 }, [_cache[23] || (_cache[23] = createElementVNode("span", { "aria-hidden": "true" }, "+", -1)), createVNode(unref(VisuallyHidden), null, {
695
+ default: withCtx(() => [..._cache[22] || (_cache[22] = [createTextVNode("Added", -1)])]),
696
+ _: 1
697
+ })], 64)) : row.old?.line.kind === "removed" ? (openBlock(), createElementBlock(Fragment, { key: 1 }, [_cache[25] || (_cache[25] = createElementVNode("span", { "aria-hidden": "true" }, "-", -1)), createVNode(unref(VisuallyHidden), null, {
698
+ default: withCtx(() => [..._cache[24] || (_cache[24] = [createTextVNode("Removed", -1)])]),
699
+ _: 1
700
+ })], 64)) : createCommentVNode("v-if", true)], 2),
701
+ createElementVNode("td", {
702
+ class: normalizeClass(["astryx-diff-viewer__code astryx-diff-viewer__code--old", [cellClass(row.old), { "astryx-diff-viewer__code--wrapped": __props.isWrapped }]]),
703
+ "data-diff": diffKind(row.old)
704
+ }, [createElementVNode("span", _hoisted_29, [(openBlock(true), createElementBlock(Fragment, null, renderList(row.old?.groups ?? [], (group, groupIndex) => {
705
+ return openBlock(), createElementBlock(Fragment, { key: groupIndex }, [group.changed ? (openBlock(), createElementBlock("span", {
706
+ key: 0,
707
+ class: "astryx-diff-viewer__change",
708
+ "data-diff-change": "",
709
+ "data-kind": row.old?.line.kind
710
+ }, [(openBlock(true), createElementBlock(Fragment, null, renderList(group.parts, (part, partIndex) => {
711
+ return openBlock(), createElementBlock(Fragment, { key: partIndex }, [part.type != null ? (openBlock(), createElementBlock("span", {
712
+ key: 0,
713
+ class: normalizeClass(`astryx-token-${part.type}`)
714
+ }, toDisplayString(part.text), 3)) : (openBlock(), createElementBlock(Fragment, { key: 1 }, [createTextVNode(toDisplayString(part.text), 1)], 64))], 64);
715
+ }), 128))], 8, _hoisted_30)) : (openBlock(true), createElementBlock(Fragment, { key: 1 }, renderList(group.parts, (part, partIndex) => {
716
+ return openBlock(), createElementBlock(Fragment, { key: partIndex }, [part.type != null ? (openBlock(), createElementBlock("span", {
717
+ key: 0,
718
+ class: normalizeClass(`astryx-token-${part.type}`)
719
+ }, toDisplayString(part.text), 3)) : (openBlock(), createElementBlock(Fragment, { key: 1 }, [createTextVNode(toDisplayString(part.text), 1)], 64))], 64);
720
+ }), 128))], 64);
721
+ }), 128))])], 10, _hoisted_28),
722
+ __props.hasLineNumbers ? (openBlock(), createElementBlock("td", {
723
+ key: 1,
724
+ class: normalizeClass(["astryx-diff-viewer__gutter", cellClass(row.new)])
725
+ }, [__props.hasLineNumberClick && lineAction(row.new, "new") != null ? (openBlock(), createElementBlock("button", {
726
+ key: 0,
727
+ type: "button",
728
+ class: "astryx-diff-viewer__gutter-button",
729
+ "data-diff-line-button": "",
730
+ "aria-label": lineNumberButtonLabel("new", lineNumber(row.new, "new")),
731
+ onClick: ($event) => emitLineNumberClick(row.new, "new", $event)
732
+ }, toDisplayString(lineNumber(row.new, "new")), 9, _hoisted_31)) : (openBlock(), createElementBlock(Fragment, { key: 1 }, [createTextVNode(toDisplayString(lineNumber(row.new, "new")), 1)], 64))], 2)) : createCommentVNode("v-if", true),
733
+ createElementVNode("td", { class: normalizeClass(["astryx-diff-viewer__marker", cellClass(row.new)]) }, [row.new?.line.kind === "added" ? (openBlock(), createElementBlock(Fragment, { key: 0 }, [_cache[27] || (_cache[27] = createElementVNode("span", { "aria-hidden": "true" }, "+", -1)), createVNode(unref(VisuallyHidden), null, {
734
+ default: withCtx(() => [..._cache[26] || (_cache[26] = [createTextVNode("Added", -1)])]),
735
+ _: 1
736
+ })], 64)) : row.new?.line.kind === "removed" ? (openBlock(), createElementBlock(Fragment, { key: 1 }, [_cache[29] || (_cache[29] = createElementVNode("span", { "aria-hidden": "true" }, "-", -1)), createVNode(unref(VisuallyHidden), null, {
737
+ default: withCtx(() => [..._cache[28] || (_cache[28] = [createTextVNode("Removed", -1)])]),
738
+ _: 1
739
+ })], 64)) : createCommentVNode("v-if", true)], 2),
740
+ createElementVNode("td", {
741
+ class: normalizeClass(["astryx-diff-viewer__code", [cellClass(row.new), { "astryx-diff-viewer__code--wrapped": __props.isWrapped }]]),
742
+ "data-diff": diffKind(row.new)
743
+ }, [createElementVNode("span", _hoisted_33, [(openBlock(true), createElementBlock(Fragment, null, renderList(row.new?.groups ?? [], (group, groupIndex) => {
744
+ return openBlock(), createElementBlock(Fragment, { key: groupIndex }, [group.changed ? (openBlock(), createElementBlock("span", {
745
+ key: 0,
746
+ class: "astryx-diff-viewer__change",
747
+ "data-diff-change": "",
748
+ "data-kind": row.new?.line.kind
749
+ }, [(openBlock(true), createElementBlock(Fragment, null, renderList(group.parts, (part, partIndex) => {
750
+ return openBlock(), createElementBlock(Fragment, { key: partIndex }, [part.type != null ? (openBlock(), createElementBlock("span", {
751
+ key: 0,
752
+ class: normalizeClass(`astryx-token-${part.type}`)
753
+ }, toDisplayString(part.text), 3)) : (openBlock(), createElementBlock(Fragment, { key: 1 }, [createTextVNode(toDisplayString(part.text), 1)], 64))], 64);
754
+ }), 128))], 8, _hoisted_34)) : (openBlock(true), createElementBlock(Fragment, { key: 1 }, renderList(group.parts, (part, partIndex) => {
755
+ return openBlock(), createElementBlock(Fragment, { key: partIndex }, [part.type != null ? (openBlock(), createElementBlock("span", {
756
+ key: 0,
757
+ class: normalizeClass(`astryx-token-${part.type}`)
758
+ }, toDisplayString(part.text), 3)) : (openBlock(), createElementBlock(Fragment, { key: 1 }, [createTextVNode(toDisplayString(part.text), 1)], 64))], 64);
759
+ }), 128))], 64);
760
+ }), 128))])], 10, _hoisted_32)
761
+ ]), oldNote(row) != null || newNote(row) != null ? (openBlock(), createElementBlock("tr", _hoisted_35, [createElementVNode("td", {
762
+ colspan: splitSideColumns.value,
763
+ class: "astryx-diff-viewer__annotation astryx-diff-viewer__code--old"
764
+ }, [oldNote(row) != null ? (openBlock(), createElementBlock("div", _hoisted_37, [createVNode(unref(RenderVNode_default), { content: oldNote(row) }, null, 8, ["content"])])) : createCommentVNode("v-if", true)], 8, _hoisted_36), createElementVNode("td", {
765
+ colspan: splitSideColumns.value,
766
+ class: "astryx-diff-viewer__annotation"
767
+ }, [newNote(row) != null ? (openBlock(), createElementBlock("div", _hoisted_39, [createVNode(unref(RenderVNode_default), { content: newNote(row) }, null, 8, ["content"])])) : createCommentVNode("v-if", true)], 8, _hoisted_38)])) : createCommentVNode("v-if", true)], 64))], 64);
768
+ }), 128))])
769
+ ], 6));
770
+ };
771
+ }
772
+ });
773
+ //#endregion
774
+ //#region src/DiffViewer/language.ts
775
+ /**
776
+ * @file language.ts
777
+ * @input A file path from a diff
778
+ * @output inferLanguage(path): the tokenizer language for that extension
779
+ * @position Lab helper used by DiffViewer when `language` is not given
780
+ */
781
+ const EXTENSION_LANGUAGES = {
782
+ ts: "ts",
783
+ mts: "ts",
784
+ cts: "ts",
785
+ tsx: "tsx",
786
+ js: "js",
787
+ mjs: "js",
788
+ cjs: "js",
789
+ jsx: "jsx",
790
+ json: "json",
791
+ html: "html",
792
+ htm: "html",
793
+ xml: "xml",
794
+ svg: "svg",
795
+ css: "css",
796
+ scss: "scss",
797
+ less: "less",
798
+ py: "python",
799
+ sh: "bash",
800
+ bash: "bash",
801
+ zsh: "zsh",
802
+ php: "php",
803
+ hack: "hack",
804
+ yaml: "yaml",
805
+ yml: "yaml",
806
+ md: "markdown",
807
+ markdown: "markdown"
808
+ };
809
+ /** `src/app.ts` → `ts`; unknown or missing extensions render as plaintext. */
810
+ function inferLanguage(path) {
811
+ if (path == null) return "plaintext";
812
+ const base = path.slice(path.lastIndexOf("/") + 1);
813
+ const dot = base.lastIndexOf(".");
814
+ if (dot <= 0) return "plaintext";
815
+ return EXTENSION_LANGUAGES[base.slice(dot + 1).toLowerCase()] ?? "plaintext";
816
+ }
817
+ //#endregion
818
+ //#region src/DiffViewer/inlineDiff.ts
819
+ /**
820
+ * Upper bound on the comparison table (tokens × tokens). Beyond it a pair is
821
+ * reported as having no inline ranges, so a very long minified line cannot
822
+ * stall rendering; its row tint still marks it as changed.
823
+ */
824
+ const MAX_CELLS = 25e4;
825
+ const EMPTY = {
826
+ old: [],
827
+ new: []
828
+ };
829
+ const WORD_PATTERN = /\w+|\s+|[^\w\s]/gu;
830
+ function split(line, granularity) {
831
+ if (granularity === "char") {
832
+ let offset = 0;
833
+ return Array.from(line, (text) => {
834
+ const start = offset;
835
+ offset += text.length;
836
+ return {
837
+ text,
838
+ start,
839
+ end: offset
840
+ };
841
+ });
842
+ }
843
+ const pieces = [];
844
+ for (const match of line.matchAll(WORD_PATTERN)) {
845
+ const start = match.index ?? 0;
846
+ pieces.push({
847
+ text: match[0],
848
+ start,
849
+ end: start + match[0].length
850
+ });
851
+ }
852
+ return pieces;
853
+ }
854
+ function isBlank(text) {
855
+ return text.trim() === "";
856
+ }
857
+ /**
858
+ * Merge the changed pieces into ranges: adjacent pieces join, and in word
859
+ * mode so do two changes separated only by whitespace, so `a b` → `c d`
860
+ * reads as one change rather than two with a gap. Ranges of only whitespace
861
+ * (re-indentation) are dropped: a highlighted blank says little.
862
+ */
863
+ function toRanges(pieces, changed, granularity) {
864
+ const ranges = [];
865
+ let i = 0;
866
+ while (i < pieces.length) {
867
+ if (!changed[i]) {
868
+ i++;
869
+ continue;
870
+ }
871
+ const start = pieces[i].start;
872
+ let end = pieces[i].end;
873
+ i++;
874
+ while (i < pieces.length) {
875
+ if (changed[i]) {
876
+ end = pieces[i].end;
877
+ i++;
878
+ continue;
879
+ }
880
+ if (granularity === "word" && isBlank(pieces[i].text) && i + 1 < pieces.length && changed[i + 1]) {
881
+ end = pieces[i + 1].end;
882
+ i += 2;
883
+ continue;
884
+ }
885
+ break;
886
+ }
887
+ if (pieces.some((p) => p.start >= start && p.end <= end && !isBlank(p.text))) ranges.push([start, end]);
888
+ }
889
+ return ranges;
890
+ }
891
+ /**
892
+ * Below this share of the shorter line's non-whitespace characters in
893
+ * common, two paired lines are treated as unrelated: highlighting most of
894
+ * both would say no more than the row tint.
895
+ */
896
+ const MIN_SHARED_RATIO = .5;
897
+ function contentLength(pieces) {
898
+ let length = 0;
899
+ for (const piece of pieces) if (!isBlank(piece.text)) length += piece.text.length;
900
+ return length;
901
+ }
902
+ /**
903
+ * The changed ranges of a removed line and the added line that replaced it.
904
+ * Returns no ranges when the lines are mostly different (less than half of
905
+ * the shorter line's non-whitespace text in common: the whole line changed,
906
+ * which the row tint already says) or when they are too long to compare.
907
+ */
908
+ function diffInlineRanges(oldLine, newLine, granularity) {
909
+ if (oldLine === newLine) return EMPTY;
910
+ const a = split(oldLine, granularity);
911
+ const b = split(newLine, granularity);
912
+ const n = a.length;
913
+ const m = b.length;
914
+ if (n === 0 || m === 0 || n * m > MAX_CELLS) return EMPTY;
915
+ const width = m + 1;
916
+ const lengths = new Uint32Array((n + 1) * width);
917
+ for (let i = n - 1; i >= 0; i--) for (let j = m - 1; j >= 0; j--) lengths[i * width + j] = a[i].text === b[j].text ? lengths[(i + 1) * width + j + 1] + 1 : Math.max(lengths[(i + 1) * width + j], lengths[i * width + j + 1]);
918
+ const oldChanged = Array.from({ length: n }).fill(true);
919
+ const newChanged = Array.from({ length: m }).fill(true);
920
+ let shared = 0;
921
+ let i = 0;
922
+ let j = 0;
923
+ while (i < n && j < m) if (a[i].text === b[j].text) {
924
+ oldChanged[i] = false;
925
+ newChanged[j] = false;
926
+ if (!isBlank(a[i].text)) shared += a[i].text.length;
927
+ i++;
928
+ j++;
929
+ } else if (lengths[(i + 1) * width + j] >= lengths[i * width + j + 1]) i++;
930
+ else j++;
931
+ const shorter = Math.min(contentLength(a), contentLength(b));
932
+ if (shared === 0 || shared < shorter * MIN_SHARED_RATIO) return EMPTY;
933
+ return {
934
+ old: toRanges(a, oldChanged, granularity),
935
+ new: toRanges(b, newChanged, granularity)
936
+ };
937
+ }
938
+ //#endregion
939
+ //#region src/DiffViewer/lineParts.ts
940
+ /**
941
+ * Cut a line at every syntax-token and inline-highlight boundary, so each
942
+ * piece carries one token type and one changed/unchanged state.
943
+ */
944
+ function buildPieces(text, tokens, emphasis) {
945
+ const segments = [];
946
+ let cursor = 0;
947
+ for (const token of tokens) {
948
+ const start = Math.max(token.start, cursor);
949
+ const end = Math.min(token.end, text.length);
950
+ if (start > cursor) segments.push({
951
+ start: cursor,
952
+ end: start,
953
+ type: null
954
+ });
955
+ if (end > start) segments.push({
956
+ start,
957
+ end,
958
+ type: token.type
959
+ });
960
+ cursor = Math.max(cursor, end);
961
+ }
962
+ if (cursor < text.length) segments.push({
963
+ start: cursor,
964
+ end: text.length,
965
+ type: null
966
+ });
967
+ const cuts = /* @__PURE__ */ new Set();
968
+ for (const [start, end] of emphasis) {
969
+ cuts.add(start);
970
+ cuts.add(end);
971
+ }
972
+ const isChanged = (offset) => emphasis.some(([start, end]) => offset >= start && offset < end);
973
+ const pieces = [];
974
+ for (const segment of segments) {
975
+ let start = segment.start;
976
+ for (let offset = segment.start + 1; offset < segment.end; offset++) if (cuts.has(offset)) {
977
+ pieces.push({
978
+ start,
979
+ end: offset,
980
+ type: segment.type,
981
+ changed: isChanged(start)
982
+ });
983
+ start = offset;
984
+ }
985
+ pieces.push({
986
+ start,
987
+ end: segment.end,
988
+ type: segment.type,
989
+ changed: isChanged(start)
990
+ });
991
+ }
992
+ return pieces;
993
+ }
994
+ /**
995
+ * Group a line into runs that DiffViewer can render without extra wrappers:
996
+ * unchanged runs keep their token spans, and adjacent changed pieces share one
997
+ * `data-diff-change` element.
998
+ */
999
+ function buildLineGroups(text, tokens, emphasis) {
1000
+ if (text === "") return [{
1001
+ changed: false,
1002
+ parts: [{
1003
+ text: "​",
1004
+ type: null,
1005
+ changed: false
1006
+ }]
1007
+ }];
1008
+ const pieces = buildPieces(text, tokens, emphasis);
1009
+ const groups = [];
1010
+ for (const piece of pieces) {
1011
+ const part = {
1012
+ text: text.slice(piece.start, piece.end),
1013
+ type: piece.type,
1014
+ changed: piece.changed
1015
+ };
1016
+ const last = groups[groups.length - 1];
1017
+ if (last != null && last.changed === piece.changed) {
1018
+ last.parts.push(part);
1019
+ continue;
1020
+ }
1021
+ groups.push({
1022
+ changed: piece.changed,
1023
+ parts: [part]
1024
+ });
1025
+ }
1026
+ return groups;
1027
+ }
1028
+ //#endregion
1029
+ //#region src/DiffViewer/rows.ts
1030
+ function lineRef(line, hunkIndex) {
1031
+ if (line.kind === "removed") return line.oldLineNumber == null ? null : {
1032
+ side: "old",
1033
+ lineNumber: line.oldLineNumber,
1034
+ line,
1035
+ hunkIndex
1036
+ };
1037
+ return line.newLineNumber == null ? null : {
1038
+ side: "new",
1039
+ lineNumber: line.newLineNumber,
1040
+ line,
1041
+ hunkIndex
1042
+ };
1043
+ }
1044
+ /**
1045
+ * Pair each run of removed lines with the run of added lines after it, line by
1046
+ * line (the same pairing split view shows), and mark what changed inside each
1047
+ * pair.
1048
+ */
1049
+ function addInlineEmphasis(cells, granularity) {
1050
+ let i = 0;
1051
+ while (i < cells.length) {
1052
+ if (cells[i].line.kind !== "removed") {
1053
+ i++;
1054
+ continue;
1055
+ }
1056
+ const removed = [];
1057
+ const added = [];
1058
+ while (i < cells.length && cells[i].line.kind === "removed") removed.push(cells[i++]);
1059
+ while (i < cells.length && cells[i].line.kind === "added") added.push(cells[i++]);
1060
+ const pairs = Math.min(removed.length, added.length);
1061
+ for (let r = 0; r < pairs; r++) {
1062
+ const ranges = diffInlineRanges(removed[r].line.content, added[r].line.content, granularity);
1063
+ removed[r].emphasis = ranges.old;
1064
+ added[r].emphasis = ranges.new;
1065
+ }
1066
+ }
1067
+ }
1068
+ /**
1069
+ * Tokenize each hunk's old and new side as continuous text, so constructs that
1070
+ * span lines (block comments, template strings) keep their paint within the
1071
+ * hunk, then hand each line the tokens of the side it renders from.
1072
+ */
1073
+ function buildCells(file, language, inlineHighlight) {
1074
+ return file.hunks.map((hunk, hunkIndex) => {
1075
+ const oldLines = hunk.lines.filter((line) => line.kind !== "added");
1076
+ const newLines = hunk.lines.filter((line) => line.kind !== "removed");
1077
+ const highlight = language !== "plaintext";
1078
+ const oldTokens = highlight ? tokenize(oldLines.map((line) => line.content).join("\n"), language) : [];
1079
+ const newTokens = highlight ? tokenize(newLines.map((line) => line.content).join("\n"), language) : [];
1080
+ let oldIndex = 0;
1081
+ let newIndex = 0;
1082
+ const cells = [];
1083
+ for (const line of hunk.lines) {
1084
+ let tokens;
1085
+ if (line.kind === "removed") tokens = oldTokens[oldIndex++] ?? [];
1086
+ else if (line.kind === "added") tokens = newTokens[newIndex++] ?? [];
1087
+ else {
1088
+ oldIndex++;
1089
+ tokens = newTokens[newIndex++] ?? [];
1090
+ }
1091
+ const ref = lineRef(line, hunkIndex);
1092
+ if (ref != null) cells.push({
1093
+ line,
1094
+ ref,
1095
+ tokens,
1096
+ emphasis: [],
1097
+ groups: []
1098
+ });
1099
+ }
1100
+ if (inlineHighlight !== "none") addInlineEmphasis(cells, inlineHighlight);
1101
+ for (const cell of cells) cell.groups = buildLineGroups(cell.line.content, cell.tokens, cell.emphasis);
1102
+ return cells;
1103
+ });
1104
+ }
1105
+ function buildUnifiedRows(file, cells) {
1106
+ const rows = [];
1107
+ file.hunks.forEach((hunk, h) => {
1108
+ rows.push({
1109
+ type: "hunk",
1110
+ key: `h${h}`,
1111
+ header: hunk.header
1112
+ });
1113
+ cells[h].forEach((cell, i) => {
1114
+ rows.push({
1115
+ type: "line",
1116
+ key: `h${h}l${i}`,
1117
+ cell
1118
+ });
1119
+ });
1120
+ });
1121
+ return rows;
1122
+ }
1123
+ /**
1124
+ * Pair each run of removed lines with the run of added lines that follows it,
1125
+ * row by row; context lines appear on both sides of one row.
1126
+ */
1127
+ function buildSplitRows(file, cells) {
1128
+ const rows = [];
1129
+ file.hunks.forEach((hunk, h) => {
1130
+ rows.push({
1131
+ type: "hunk",
1132
+ key: `h${h}`,
1133
+ header: hunk.header
1134
+ });
1135
+ const hunkCells = cells[h];
1136
+ let i = 0;
1137
+ while (i < hunkCells.length) {
1138
+ const cell = hunkCells[i];
1139
+ if (cell.line.kind === "context") {
1140
+ rows.push({
1141
+ type: "pair",
1142
+ key: `h${h}l${i}`,
1143
+ old: cell,
1144
+ new: cell
1145
+ });
1146
+ i++;
1147
+ continue;
1148
+ }
1149
+ const removed = [];
1150
+ const added = [];
1151
+ while (i < hunkCells.length && hunkCells[i].line.kind === "removed") removed.push(hunkCells[i++]);
1152
+ while (i < hunkCells.length && hunkCells[i].line.kind === "added") added.push(hunkCells[i++]);
1153
+ const count = Math.max(removed.length, added.length);
1154
+ for (let r = 0; r < count; r++) rows.push({
1155
+ type: "pair",
1156
+ key: `h${h}l${i}r${r}`,
1157
+ old: removed[r] ?? null,
1158
+ new: added[r] ?? null
1159
+ });
1160
+ }
1161
+ });
1162
+ return rows;
1163
+ }
1164
+ //#endregion
1165
+ //#region src/DiffViewer/DiffViewer.vue
1166
+ const _hoisted_1$11 = ["data-view-mode"];
1167
+ const _hoisted_2$10 = ["aria-expanded"];
1168
+ const _hoisted_3$9 = { class: "astryx-diff-viewer__path" };
1169
+ const _hoisted_4$8 = {
1170
+ key: 1,
1171
+ class: "astryx-diff-viewer__path"
1172
+ };
1173
+ const _hoisted_5$7 = {
1174
+ key: 3,
1175
+ class: "astryx-diff-viewer__stats"
1176
+ };
1177
+ const _hoisted_6$6 = {
1178
+ "aria-hidden": "true",
1179
+ class: "astryx-diff-viewer__stat-added"
1180
+ };
1181
+ const _hoisted_7$4 = {
1182
+ "aria-hidden": "true",
1183
+ class: "astryx-diff-viewer__stat-removed"
1184
+ };
1185
+ const _hoisted_8$3 = {
1186
+ key: 4,
1187
+ class: "astryx-diff-viewer__header-end"
1188
+ };
1189
+ const _hoisted_9$3 = ["hidden"];
1190
+ const _hoisted_10$3 = {
1191
+ key: 0,
1192
+ class: "astryx-diff-viewer__message"
1193
+ };
1194
+ const _hoisted_11$1 = {
1195
+ key: 1,
1196
+ class: "astryx-diff-viewer__message"
1197
+ };
1198
+ const _hoisted_12 = ["aria-label"];
1199
+ const _sfc_main$1 = /*@__PURE__*/ defineComponent({
1200
+ name: "AstryxDiffViewer",
1201
+ inheritAttrs: false,
1202
+ __name: "DiffViewer",
1203
+ props: {
1204
+ file: {},
1205
+ viewMode: { default: "unified" },
1206
+ language: { default: void 0 },
1207
+ hasLineNumbers: {
1208
+ type: Boolean,
1209
+ default: true
1210
+ },
1211
+ isWrapped: {
1212
+ type: Boolean,
1213
+ default: false
1214
+ },
1215
+ inlineHighlight: { default: "word" },
1216
+ isCollapsible: {
1217
+ type: Boolean,
1218
+ default: true
1219
+ },
1220
+ isCollapsed: {
1221
+ type: Boolean,
1222
+ default: void 0
1223
+ },
1224
+ defaultIsCollapsed: {
1225
+ type: Boolean,
1226
+ default: false
1227
+ },
1228
+ maxHeight: { default: void 0 },
1229
+ syntaxTheme: { default: void 0 },
1230
+ label: { default: "Diff" },
1231
+ renderLineAnnotation: {
1232
+ type: Function,
1233
+ default: void 0
1234
+ }
1235
+ },
1236
+ emits: [
1237
+ "collapsedChange",
1238
+ "update:isCollapsed",
1239
+ "lineNumberClick"
1240
+ ],
1241
+ setup(__props, { emit: __emit }) {
1242
+ const props = __props;
1243
+ const emit = __emit;
1244
+ const slots = useSlots();
1245
+ const attrs = useAttrs();
1246
+ const instance = getCurrentInstance();
1247
+ const bodyId = `astryx-diff-viewer-body-${useId()}`;
1248
+ const rootAttrs = computed(() => {
1249
+ const { class: _class, style: _style, ...rest } = attrs;
1250
+ return rest;
1251
+ });
1252
+ const rootClass = computed(() => attrs.class);
1253
+ const rootStyle = computed(() => attrs.style);
1254
+ const path = computed(() => props.file.newPath ?? props.file.oldPath);
1255
+ const displayPath = computed(() => {
1256
+ if (props.file.status === "renamed" && props.file.oldPath != null && props.file.newPath != null) return `${props.file.oldPath} → ${props.file.newPath}`;
1257
+ return path.value;
1258
+ });
1259
+ const language = computed(() => props.language ?? inferLanguage(path.value));
1260
+ const scrollLabel = computed(() => displayPath.value ?? props.label);
1261
+ const cells = computed(() => {
1262
+ const built = buildCells(props.file, language.value, props.inlineHighlight);
1263
+ const annotate = props.renderLineAnnotation;
1264
+ if (annotate == null) return built;
1265
+ return built.map((hunk) => hunk.map((cell) => {
1266
+ const note = annotate(cell.ref);
1267
+ return {
1268
+ ...cell,
1269
+ note: typeof note === "boolean" ? null : note
1270
+ };
1271
+ }));
1272
+ });
1273
+ const unifiedRows = computed(() => props.viewMode === "unified" ? buildUnifiedRows(props.file, cells.value) : []);
1274
+ const splitRows = computed(() => props.viewMode === "split" ? buildSplitRows(props.file, cells.value) : []);
1275
+ const stats = computed(() => {
1276
+ let added = 0;
1277
+ let removed = 0;
1278
+ let maxLineNumber = 1;
1279
+ for (const hunk of props.file.hunks) for (const line of hunk.lines) {
1280
+ if (line.kind === "added") added++;
1281
+ else if (line.kind === "removed") removed++;
1282
+ maxLineNumber = Math.max(maxLineNumber, line.oldLineNumber ?? 0, line.newLineNumber ?? 0);
1283
+ }
1284
+ return {
1285
+ added,
1286
+ removed,
1287
+ digits: String(maxLineNumber).length
1288
+ };
1289
+ });
1290
+ const addedText = computed(() => `+${stats.value.added}`);
1291
+ const removedText = computed(() => `−${stats.value.removed}`);
1292
+ const statsText = computed(() => {
1293
+ const lines = (count, word) => `${count} ${word}${count === 1 ? "" : "s"}`;
1294
+ return `${lines(stats.value.added, "line")} added, ${lines(stats.value.removed, "line")} removed`;
1295
+ });
1296
+ const uncontrolledCollapsed = ref(props.defaultIsCollapsed);
1297
+ const canCollapse = computed(() => props.isCollapsible && displayPath.value != null);
1298
+ const isCollapsed = computed(() => canCollapse.value && (props.isCollapsed ?? uncontrolledCollapsed.value));
1299
+ function toggleCollapsed() {
1300
+ const next = !isCollapsed.value;
1301
+ if (props.isCollapsed === void 0) uncontrolledCollapsed.value = next;
1302
+ emit("update:isCollapsed", next);
1303
+ emit("collapsedChange", next);
1304
+ }
1305
+ const STATUS_COLORS = {
1306
+ added: "green",
1307
+ deleted: "red",
1308
+ renamed: "blue"
1309
+ };
1310
+ const statusColor = computed(() => STATUS_COLORS[props.file.status]);
1311
+ const statusLabel = computed(() => {
1312
+ switch (props.file.status) {
1313
+ case "added": return "Added";
1314
+ case "deleted": return "Deleted";
1315
+ case "renamed": return "Renamed";
1316
+ default: return null;
1317
+ }
1318
+ });
1319
+ const hasHeader = computed(() => displayPath.value != null || slots.headerEnd != null);
1320
+ const scrollStyle = computed(() => props.maxHeight == null ? void 0 : { maxHeight: typeof props.maxHeight === "number" ? `${props.maxHeight}px` : props.maxHeight });
1321
+ const scroll = useScrollableArea({
1322
+ axis: "both",
1323
+ keyboardAccess: {
1324
+ owner: "viewport",
1325
+ role: "group",
1326
+ label: props.label
1327
+ }
1328
+ });
1329
+ const hasLineNumberClick = computed(() => instance?.vnode.props?.onLineNumberClick != null);
1330
+ function relayLineNumberClick(ref, event) {
1331
+ emit("lineNumberClick", ref, event);
1332
+ }
1333
+ return (_ctx, _cache) => {
1334
+ return openBlock(), createElementBlock("div", mergeProps(rootAttrs.value, {
1335
+ class: ["astryx-diff-viewer", rootClass.value],
1336
+ style: rootStyle.value,
1337
+ "data-view-mode": __props.viewMode
1338
+ }), [(openBlock(), createBlock(resolveDynamicComponent(__props.syntaxTheme != null ? unref(SyntaxTheme) : "div"), normalizeProps(guardReactiveProps(__props.syntaxTheme != null ? { theme: __props.syntaxTheme } : {})), {
1339
+ default: withCtx(() => [hasHeader.value ? (openBlock(), createElementBlock("div", {
1340
+ key: 0,
1341
+ class: normalizeClass(["astryx-diff-viewer__header", { "astryx-diff-viewer__header--divided": !isCollapsed.value }])
1342
+ }, [
1343
+ canCollapse.value ? (openBlock(), createElementBlock("button", {
1344
+ key: 0,
1345
+ type: "button",
1346
+ class: "astryx-diff-viewer__toggle",
1347
+ "aria-expanded": !isCollapsed.value,
1348
+ "aria-controls": bodyId,
1349
+ onClick: toggleCollapsed
1350
+ }, [createElementVNode("span", { class: normalizeClass(["astryx-diff-viewer__chevron", { "astryx-diff-viewer__chevron--expanded": !isCollapsed.value }]) }, [createVNode(unref(Icon), {
1351
+ icon: "chevronRight",
1352
+ size: "xsm",
1353
+ color: "inherit"
1354
+ })], 2), createElementVNode("span", _hoisted_3$9, toDisplayString(displayPath.value), 1)], 8, _hoisted_2$10)) : displayPath.value != null ? (openBlock(), createElementBlock("span", _hoisted_4$8, toDisplayString(displayPath.value), 1)) : createCommentVNode("v-if", true),
1355
+ statusColor.value != null && statusLabel.value != null ? (openBlock(), createBlock(unref(Token), {
1356
+ key: 2,
1357
+ size: "sm",
1358
+ color: statusColor.value,
1359
+ label: statusLabel.value
1360
+ }, null, 8, ["color", "label"])) : createCommentVNode("v-if", true),
1361
+ stats.value.added > 0 || stats.value.removed > 0 ? (openBlock(), createElementBlock("span", _hoisted_5$7, [
1362
+ createElementVNode("span", _hoisted_6$6, toDisplayString(addedText.value), 1),
1363
+ createElementVNode("span", _hoisted_7$4, toDisplayString(removedText.value), 1),
1364
+ createVNode(unref(VisuallyHidden), null, {
1365
+ default: withCtx(() => [createTextVNode(toDisplayString(statsText.value), 1)]),
1366
+ _: 1
1367
+ })
1368
+ ])) : createCommentVNode("v-if", true),
1369
+ _ctx.$slots.headerEnd ? (openBlock(), createElementBlock("div", _hoisted_8$3, [renderSlot(_ctx.$slots, "headerEnd")])) : createCommentVNode("v-if", true)
1370
+ ], 2)) : createCommentVNode("v-if", true), createElementVNode("div", {
1371
+ id: bodyId,
1372
+ hidden: isCollapsed.value
1373
+ }, [!isCollapsed.value ? (openBlock(), createElementBlock(Fragment, { key: 0 }, [__props.file.isBinary ? (openBlock(), createElementBlock("div", _hoisted_10$3, " Binary file not shown ")) : __props.file.hunks.length === 0 ? (openBlock(), createElementBlock("div", _hoisted_11$1, " No content changes ")) : (openBlock(), createElementBlock("div", mergeProps({
1374
+ key: 2,
1375
+ ref: unref(scroll).viewport
1376
+ }, unref(scroll).viewportAttrs.value, {
1377
+ class: "astryx-diff-viewer__viewport",
1378
+ "aria-label": scrollLabel.value,
1379
+ style: [unref(scroll).viewportStyle.value, scrollStyle.value]
1380
+ }), [createElementVNode("div", {
1381
+ ref: unref(scroll).content,
1382
+ class: "astryx-diff-viewer__content"
1383
+ }, [createVNode(_sfc_main$13, {
1384
+ "view-mode": __props.viewMode,
1385
+ "unified-rows": unifiedRows.value,
1386
+ "split-rows": splitRows.value,
1387
+ "has-line-numbers": __props.hasLineNumbers,
1388
+ "is-wrapped": __props.isWrapped,
1389
+ "has-line-number-click": hasLineNumberClick.value,
1390
+ digits: stats.value.digits,
1391
+ onLineNumberClick: relayLineNumberClick
1392
+ }, null, 8, [
1393
+ "view-mode",
1394
+ "unified-rows",
1395
+ "split-rows",
1396
+ "has-line-numbers",
1397
+ "is-wrapped",
1398
+ "has-line-number-click",
1399
+ "digits"
1400
+ ])], 512)], 16, _hoisted_12))], 64)) : createCommentVNode("v-if", true)], 8, _hoisted_9$3)]),
1401
+ _: 3
1402
+ }, 16))], 16, _hoisted_1$11);
1403
+ };
1404
+ }
1405
+ });
1406
+ //#endregion
1407
+ //#region src/DiffViewer/parseUnifiedDiff.ts
1408
+ const HUNK_HEADER = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/;
1409
+ const GIT_HEADER_PREFIX = "diff --git ";
1410
+ const BINARY_PREFIX = "Binary files ";
1411
+ const BINARY_SUFFIX = " differ";
1412
+ const BINARY_SEPARATOR = " and ";
1413
+ /**
1414
+ * The two paths of a `Binary files X and Y differ` line, or null when the
1415
+ * line is not one. Plain string matching, not a regex, so any line (however
1416
+ * long, whatever characters it holds) is read in one pass. Like the greedy
1417
+ * pattern it replaces, the separator is the last ` and `.
1418
+ */
1419
+ function parseBinaryLine(line) {
1420
+ if (!line.startsWith(BINARY_PREFIX) || !line.endsWith(BINARY_SUFFIX)) return null;
1421
+ const middle = line.slice(13, line.length - 7);
1422
+ const split = middle.lastIndexOf(BINARY_SEPARATOR);
1423
+ if (split <= 0 || split + 5 >= middle.length) return null;
1424
+ return {
1425
+ oldPath: middle.slice(0, split),
1426
+ newPath: middle.slice(split + 5)
1427
+ };
1428
+ }
1429
+ const C_ESCAPES = {
1430
+ "a": 7,
1431
+ "b": 8,
1432
+ "t": 9,
1433
+ "n": 10,
1434
+ "v": 11,
1435
+ "f": 12,
1436
+ "r": 13,
1437
+ "\"": 34,
1438
+ "\\": 92
1439
+ };
1440
+ const utf8 = new TextDecoder();
1441
+ const utf8Encoder = new TextEncoder();
1442
+ /**
1443
+ * Read a git C-style quoted path starting at `start` (the opening quote).
1444
+ * Git quotes a path that holds non-ASCII bytes or special characters and
1445
+ * writes those bytes as octal escapes (`"caf\303\251.md"`), so the escapes
1446
+ * are collected as bytes and decoded as UTF-8. Returns the path and the index
1447
+ * just past the closing quote, or null when the quote never closes.
1448
+ */
1449
+ function readQuoted(text, start) {
1450
+ const bytes = [];
1451
+ let i = start + 1;
1452
+ while (i < text.length) {
1453
+ const ch = text[i];
1454
+ if (ch === "\"") return {
1455
+ value: utf8.decode(new Uint8Array(bytes)),
1456
+ end: i + 1
1457
+ };
1458
+ if (ch === "\\" && i + 1 < text.length) {
1459
+ const next = text[i + 1];
1460
+ const octal = /^[0-7]{3}/.exec(text.slice(i + 1, i + 4));
1461
+ if (octal != null) {
1462
+ bytes.push(Number.parseInt(octal[0], 8));
1463
+ i += 4;
1464
+ continue;
1465
+ }
1466
+ if (next in C_ESCAPES) {
1467
+ bytes.push(C_ESCAPES[next]);
1468
+ i += 2;
1469
+ continue;
1470
+ }
1471
+ }
1472
+ const codePoint = text.codePointAt(i) ?? 0;
1473
+ const char = String.fromCodePoint(codePoint);
1474
+ bytes.push(...utf8Encoder.encode(char));
1475
+ i += char.length;
1476
+ }
1477
+ return null;
1478
+ }
1479
+ /** The path written in `raw`, unquoted and decoded when git quoted it. */
1480
+ function unquotePath(raw) {
1481
+ if (raw.startsWith("\"")) {
1482
+ const quoted = readQuoted(raw, 0);
1483
+ if (quoted != null) return quoted.value;
1484
+ }
1485
+ return raw;
1486
+ }
1487
+ function stripPrefix(path, prefix) {
1488
+ return path.startsWith(prefix) ? path.slice(prefix.length) : path;
1489
+ }
1490
+ /**
1491
+ * The two paths of a `diff --git` line. Git quotes a path that holds special
1492
+ * or non-ASCII characters; quoted paths are read whole and decoded. An
1493
+ * unquoted path may hold spaces, so a space can look like the separator: when
1494
+ * both sides name the same path (every change except a rename) the separator
1495
+ * is the middle space. A rename writes `rename from` / `rename to` lines,
1496
+ * which replace these paths, so the first-space split is only a fallback.
1497
+ */
1498
+ function parseGitHeader(line) {
1499
+ if (!line.startsWith(GIT_HEADER_PREFIX)) return null;
1500
+ const rest = line.slice(11);
1501
+ const paths = (left, right) => ({
1502
+ oldPath: stripPrefix(left, "a/"),
1503
+ newPath: stripPrefix(right, "b/")
1504
+ });
1505
+ if (rest.startsWith("\"")) {
1506
+ const left = readQuoted(rest, 0);
1507
+ if (left != null && rest[left.end] === " ") return paths(left.value, unquotePath(rest.slice(left.end + 1)));
1508
+ }
1509
+ if (rest.endsWith("\"")) for (let split = rest.lastIndexOf(" \""); split > 0; split = rest.lastIndexOf(" \"", split - 1)) {
1510
+ const right = readQuoted(rest, split + 1);
1511
+ if (right != null && right.end === rest.length) return paths(rest.slice(0, split), right.value);
1512
+ }
1513
+ if (rest.length % 2 === 1) {
1514
+ const middle = (rest.length - 1) / 2;
1515
+ const left = rest.slice(0, middle);
1516
+ const right = rest.slice(middle + 1);
1517
+ if (rest[middle] === " " && stripPrefix(left, "a/") === stripPrefix(right, "b/")) return paths(left, right);
1518
+ }
1519
+ const space = rest.indexOf(" ");
1520
+ return space <= 0 ? null : paths(rest.slice(0, space), rest.slice(space + 1));
1521
+ }
1522
+ function emptyFile() {
1523
+ return {
1524
+ oldPath: null,
1525
+ newPath: null,
1526
+ status: null,
1527
+ isBinary: false,
1528
+ hunks: []
1529
+ };
1530
+ }
1531
+ /**
1532
+ * `a/src/x.ts` → `src/x.ts`; `/dev/null` → null; drops a trailing
1533
+ * tab+timestamp and decodes a quoted path.
1534
+ */
1535
+ function cleanPath(raw, prefix) {
1536
+ let path = unquotePath(raw.split(" ")[0].trim());
1537
+ if (path === "/dev/null") return null;
1538
+ if (path.startsWith(prefix)) path = path.slice(prefix.length);
1539
+ return path;
1540
+ }
1541
+ function finalize(file) {
1542
+ let status = file.status;
1543
+ if (status == null) {
1544
+ if (file.oldPath == null && file.newPath != null) status = "added";
1545
+ else if (file.newPath == null && file.oldPath != null) status = "deleted";
1546
+ else status = "modified";
1547
+ }
1548
+ return {
1549
+ oldPath: status === "added" ? null : file.oldPath,
1550
+ newPath: status === "deleted" ? null : file.newPath,
1551
+ status,
1552
+ isBinary: file.isBinary,
1553
+ hunks: file.hunks
1554
+ };
1555
+ }
1556
+ function hasContent(file) {
1557
+ return file.hunks.length > 0 || file.isBinary || file.oldPath != null || file.newPath != null || file.status != null;
1558
+ }
1559
+ /**
1560
+ * Parse a unified diff into one {@link DiffViewerFile} per file.
1561
+ *
1562
+ * Accepts `git diff` output (with `diff --git`, mode, rename, copy, and
1563
+ * binary lines), classic `diff -u` output (`---`/`+++` pairs), several files in one
1564
+ * patch, and a bare `@@` hunk with no file header (returned as one file with
1565
+ * null paths). Input it cannot read yields `[]`; it never throws.
1566
+ *
1567
+ * @example
1568
+ * ```
1569
+ * const files = parseUnifiedDiff(patch);
1570
+ * files.map((file) => <DiffViewer key={file.newPath ?? file.oldPath} file={file} />);
1571
+ * ```
1572
+ */
1573
+ function parseUnifiedDiff(patch) {
1574
+ const lines = patch.split("\n").map((line) => line.replace(/\r$/, ""));
1575
+ const files = [];
1576
+ let file = null;
1577
+ let hunk = null;
1578
+ let oldRemaining = 0;
1579
+ let newRemaining = 0;
1580
+ let oldLine = 0;
1581
+ let newLine = 0;
1582
+ const flush = () => {
1583
+ if (file != null && hasContent(file)) files.push(finalize(file));
1584
+ file = null;
1585
+ hunk = null;
1586
+ };
1587
+ for (let i = 0; i < lines.length; i++) {
1588
+ const line = lines[i];
1589
+ if (hunk != null && (oldRemaining > 0 || newRemaining > 0)) {
1590
+ const glyph = line[0];
1591
+ if (glyph === "\\") continue;
1592
+ if (glyph === "+") {
1593
+ hunk.lines.push({
1594
+ kind: "added",
1595
+ content: line.slice(1),
1596
+ oldLineNumber: null,
1597
+ newLineNumber: newLine++
1598
+ });
1599
+ newRemaining--;
1600
+ continue;
1601
+ }
1602
+ if (glyph === "-") {
1603
+ hunk.lines.push({
1604
+ kind: "removed",
1605
+ content: line.slice(1),
1606
+ oldLineNumber: oldLine++,
1607
+ newLineNumber: null
1608
+ });
1609
+ oldRemaining--;
1610
+ continue;
1611
+ }
1612
+ if (glyph === " " || line === "") {
1613
+ if (line === "" && i === lines.length - 1) continue;
1614
+ hunk.lines.push({
1615
+ kind: "context",
1616
+ content: line.slice(1),
1617
+ oldLineNumber: oldLine++,
1618
+ newLineNumber: newLine++
1619
+ });
1620
+ oldRemaining--;
1621
+ newRemaining--;
1622
+ continue;
1623
+ }
1624
+ hunk = null;
1625
+ }
1626
+ if (hunk != null && line.startsWith("\\")) continue;
1627
+ const gitPaths = parseGitHeader(line);
1628
+ if (gitPaths != null) {
1629
+ flush();
1630
+ file = emptyFile();
1631
+ file.oldPath = gitPaths.oldPath;
1632
+ file.newPath = gitPaths.newPath;
1633
+ continue;
1634
+ }
1635
+ if (line.startsWith("--- ") && (lines[i + 1]?.startsWith("+++ ") ?? false)) {
1636
+ if (file == null || file.hunks.length > 0 || file.isBinary) {
1637
+ flush();
1638
+ file = emptyFile();
1639
+ }
1640
+ const current = file;
1641
+ current.oldPath = cleanPath(line.slice(4), "a/");
1642
+ current.newPath = cleanPath(lines[i + 1].slice(4), "b/");
1643
+ if (current.oldPath == null && current.status == null) current.status = "added";
1644
+ else if (current.newPath == null && current.status == null) current.status = "deleted";
1645
+ i++;
1646
+ continue;
1647
+ }
1648
+ const header = HUNK_HEADER.exec(line);
1649
+ if (header) {
1650
+ file ??= emptyFile();
1651
+ const oldStart = Number(header[1]);
1652
+ const oldLines = header[2] == null ? 1 : Number(header[2]);
1653
+ const newStart = Number(header[3]);
1654
+ const newLines = header[4] == null ? 1 : Number(header[4]);
1655
+ const next = {
1656
+ header: line,
1657
+ oldStart,
1658
+ oldLines,
1659
+ newStart,
1660
+ newLines,
1661
+ lines: []
1662
+ };
1663
+ file.hunks.push(next);
1664
+ hunk = next;
1665
+ oldRemaining = oldLines;
1666
+ newRemaining = newLines;
1667
+ oldLine = oldLines === 0 ? oldStart + 1 : oldStart;
1668
+ newLine = newLines === 0 ? newStart + 1 : newStart;
1669
+ continue;
1670
+ }
1671
+ const binary = parseBinaryLine(line);
1672
+ if (binary != null && (file == null || file.hunks.length > 0 || file.isBinary)) {
1673
+ flush();
1674
+ const own = emptyFile();
1675
+ own.oldPath = cleanPath(binary.oldPath, "a/");
1676
+ own.newPath = cleanPath(binary.newPath, "b/");
1677
+ own.isBinary = true;
1678
+ file = own;
1679
+ continue;
1680
+ }
1681
+ if (file == null) continue;
1682
+ const current = file;
1683
+ if (line.startsWith("new file mode")) current.status = "added";
1684
+ else if (line.startsWith("deleted file mode")) current.status = "deleted";
1685
+ else if (line.startsWith("rename from ")) {
1686
+ current.oldPath = unquotePath(line.slice(12));
1687
+ current.status = "renamed";
1688
+ } else if (line.startsWith("rename to ")) {
1689
+ current.newPath = unquotePath(line.slice(10));
1690
+ current.status = "renamed";
1691
+ } else if (line.startsWith("copy from ")) current.oldPath = unquotePath(line.slice(10));
1692
+ else if (line.startsWith("copy to ")) current.newPath = unquotePath(line.slice(8));
1693
+ else if (line.startsWith("Binary files ") || line === "GIT binary patch") current.isBinary = true;
1694
+ }
1695
+ flush();
1696
+ return files;
1697
+ }
1698
+ //#endregion
184
1699
  //#region src/InfoTip/InfoTip.vue
185
1700
  const _hoisted_1$10 = ["aria-label"];
186
- const _sfc_main$1 = /*@__PURE__*/ defineComponent({
1701
+ const _sfc_main$2 = /*@__PURE__*/ defineComponent({
187
1702
  name: "AstryxInfoTip",
188
1703
  __name: "InfoTip",
189
1704
  props: {
@@ -310,7 +1825,7 @@ const _hoisted_7$3 = {
310
1825
  key: 0,
311
1826
  class: "astryx-list-input__control-cell astryx-list-input__control-cell--reorder"
312
1827
  };
313
- const _sfc_main$11 = /*@__PURE__*/ defineComponent({
1828
+ const _sfc_main$12 = /*@__PURE__*/ defineComponent({
314
1829
  name: "AstryxListInputRow",
315
1830
  __name: "ListInputRow",
316
1831
  props: {
@@ -756,7 +2271,7 @@ const _hoisted_5$5 = {
756
2271
  class: "astryx-list-input__action-content"
757
2272
  };
758
2273
  const focusableFieldSelector = "[data-list-input-cell] input, [data-list-input-cell] select, [data-list-input-cell] textarea, [data-list-input-cell] button, [data-list-input-cell] [tabindex]:not([tabindex=\"-1\"])";
759
- const _sfc_main$2 = /*@__PURE__*/ defineComponent({
2274
+ const _sfc_main$3 = /*@__PURE__*/ defineComponent({
760
2275
  name: "AstryxListInput",
761
2276
  inheritAttrs: false,
762
2277
  __name: "ListInput",
@@ -1034,7 +2549,7 @@ const _sfc_main$2 = /*@__PURE__*/ defineComponent({
1034
2549
  description: unref(t)("@astryx.listInput.emptyDescription", { itemName: __props.itemName }),
1035
2550
  "is-compact": ""
1036
2551
  }, null, 8, ["title", "description"])])) : createCommentVNode("v-if", true), (openBlock(true), createElementBlock(Fragment, null, renderList(unref(reorder).displayValue.value, (item, index) => {
1037
- return openBlock(), createBlock(_sfc_main$11, {
2552
+ return openBlock(), createBlock(_sfc_main$12, {
1038
2553
  key: itemKeyOf(item),
1039
2554
  item,
1040
2555
  index,
@@ -1149,7 +2664,7 @@ const _hoisted_11 = {
1149
2664
  key: 0,
1150
2665
  class: "astryx-log-stream__detail"
1151
2666
  };
1152
- const _sfc_main$10 = /*@__PURE__*/ defineComponent({
2667
+ const _sfc_main$11 = /*@__PURE__*/ defineComponent({
1153
2668
  name: "AstryxLogStreamRow",
1154
2669
  __name: "LogStreamRow",
1155
2670
  props: {
@@ -1253,7 +2768,7 @@ function useLogFollow(options) {
1253
2768
  //#region src/LogStream/LogStream.vue
1254
2769
  const _hoisted_1$6 = ["data-variant"];
1255
2770
  const _hoisted_2$6 = ["aria-label", "aria-live"];
1256
- const _sfc_main$3 = /*@__PURE__*/ defineComponent({
2771
+ const _sfc_main$4 = /*@__PURE__*/ defineComponent({
1257
2772
  name: "AstryxLogStream",
1258
2773
  inheritAttrs: false,
1259
2774
  __name: "LogStream",
@@ -1319,7 +2834,7 @@ const _sfc_main$3 = /*@__PURE__*/ defineComponent({
1319
2834
  style: normalizeStyle(scrollerStyle.value),
1320
2835
  onScrollPassive: _cache[0] || (_cache[0] = (...args) => unref(handleScroll) && unref(handleScroll)(...args))
1321
2836
  }, [(openBlock(true), createElementBlock(Fragment, null, renderList(__props.entries, (entry) => {
1322
- return renderSlot(_ctx.$slots, "entry", { entry }, () => [createVNode(_sfc_main$10, {
2837
+ return renderSlot(_ctx.$slots, "entry", { entry }, () => [createVNode(_sfc_main$11, {
1323
2838
  entry,
1324
2839
  "has-timestamps": __props.hasTimestamps,
1325
2840
  "has-source": hasSource.value,
@@ -1485,7 +3000,7 @@ const _hoisted_10$1 = {
1485
3000
  class: "astryx-mobile-tokenizer__footer"
1486
3001
  };
1487
3002
  const CREATABLE_ID_PREFIX = "__xds_create__";
1488
- const _sfc_main$4 = /*@__PURE__*/ defineComponent({
3003
+ const _sfc_main$5 = /*@__PURE__*/ defineComponent({
1489
3004
  name: "AstryxMobileTokenizer",
1490
3005
  inheritAttrs: false,
1491
3006
  __name: "MobileTokenizer",
@@ -1799,7 +3314,7 @@ const _hoisted_10 = {
1799
3314
  key: 1,
1800
3315
  class: "astryx-stat__media"
1801
3316
  };
1802
- const _sfc_main$6 = /*@__PURE__*/ defineComponent({
3317
+ const _sfc_main$7 = /*@__PURE__*/ defineComponent({
1803
3318
  name: "AstryxStat",
1804
3319
  inheritAttrs: false,
1805
3320
  __name: "Stat",
@@ -2656,7 +4171,7 @@ const _hoisted_6$1 = {
2656
4171
  key: 1,
2657
4172
  class: "astryx-svg-icon__layer astryx-svg-icon__layer--secondary-stroke"
2658
4173
  };
2659
- const _sfc_main$5 = /*@__PURE__*/ defineComponent({
4174
+ const _sfc_main$6 = /*@__PURE__*/ defineComponent({
2660
4175
  name: "AstryxSvgIcon",
2661
4176
  inheritAttrs: false,
2662
4177
  __name: "SVGIcon",
@@ -2960,7 +4475,7 @@ const _hoisted_5 = {
2960
4475
  class: "astryx-transfer-list-panel__group-heading"
2961
4476
  };
2962
4477
  const _hoisted_6 = ["data-transfer-list-empty"];
2963
- const _sfc_main$9 = /*@__PURE__*/ defineComponent({
4478
+ const _sfc_main$10 = /*@__PURE__*/ defineComponent({
2964
4479
  name: "AstryxTransferListPanel",
2965
4480
  __name: "TransferListPanel",
2966
4481
  props: {
@@ -3432,7 +4947,7 @@ const _hoisted_3$1 = {
3432
4947
  class: "astryx-transfer-list__controls"
3433
4948
  };
3434
4949
  const _hoisted_4 = { class: "astryx-transfer-list-collection" };
3435
- const _sfc_main$7 = /*@__PURE__*/ defineComponent({
4950
+ const _sfc_main$8 = /*@__PURE__*/ defineComponent({
3436
4951
  name: "AstryxTransferList",
3437
4952
  inheritAttrs: false,
3438
4953
  __name: "TransferList",
@@ -3706,7 +5221,7 @@ const _sfc_main$7 = /*@__PURE__*/ defineComponent({
3706
5221
  default: withCtx(() => [createTextVNode(toDisplayString(unref(t)("@astryx.transferList.reorderInstructions")), 1)]),
3707
5222
  _: 1
3708
5223
  }),
3709
- createElementVNode("div", _hoisted_4, [createVNode(_sfc_main$9, {
5224
+ createElementVNode("div", _hoisted_4, [createVNode(_sfc_main$10, {
3710
5225
  side: "selected",
3711
5226
  "heading-id": selectedHeadingId,
3712
5227
  label: selectedLabel.value,
@@ -3742,7 +5257,7 @@ const _sfc_main$7 = /*@__PURE__*/ defineComponent({
3742
5257
  "dragging-value",
3743
5258
  "grabbed-value",
3744
5259
  "drop-target"
3745
- ]), createVNode(_sfc_main$9, {
5260
+ ]), createVNode(_sfc_main$10, {
3746
5261
  side: "available",
3747
5262
  "heading-id": availableHeadingId,
3748
5263
  label: availableLabel.value,
@@ -3797,7 +5312,7 @@ const _hoisted_3 = {
3797
5312
  class: "astryx-transfer-list-selector__footer"
3798
5313
  };
3799
5314
  const DEFAULT_SELECTOR_WIDTH = "min(41rem, calc(100vw - 32px))";
3800
- const _sfc_main$8 = /*@__PURE__*/ defineComponent({
5315
+ const _sfc_main$9 = /*@__PURE__*/ defineComponent({
3801
5316
  name: "AstryxTransferListSelector",
3802
5317
  __name: "TransferListSelector",
3803
5318
  props: {
@@ -3934,7 +5449,7 @@ const _sfc_main$8 = /*@__PURE__*/ defineComponent({
3934
5449
  }, [createElementVNode("fieldset", {
3935
5450
  class: "astryx-transfer-list-selector__fieldset",
3936
5451
  disabled: __props.isDisabled
3937
- }, [createVNode(_sfc_main$7, {
5452
+ }, [createVNode(_sfc_main$8, {
3938
5453
  label: __props.label,
3939
5454
  "is-label-hidden": "",
3940
5455
  options: __props.options,
@@ -4013,4 +5528,4 @@ const _sfc_main$8 = /*@__PURE__*/ defineComponent({
4013
5528
  }
4014
5529
  });
4015
5530
  //#endregion
4016
- export { _sfc_main as CircularProgress, _sfc_main$1 as InfoTip, _sfc_main$2 as ListInput, _sfc_main$3 as LogStream, _sfc_main$4 as MobileTokenizer, _sfc_main$5 as SVGIcon, _sfc_main$6 as Stat, _sfc_main$7 as TransferList, _sfc_main$8 as TransferListSelector, addCurvature, adjustTension, applyPersonality, bellIcon, calendarIcon, checkIcon, eyeIcon, folderIcon, heartIcon, homeIcon, lockIcon, mailIcon, menuIcon, roundCorners, searchIcon, settingsIcon, shieldIcon, starIcon, starterIcons, xIcon, xifBell, xifBellOverride, xifCheck, xifExamples, xifFile, xifHome, xifShield, xifStar };
5531
+ export { _sfc_main as CircularProgress, _sfc_main$1 as DiffViewer, _sfc_main$2 as InfoTip, _sfc_main$3 as ListInput, _sfc_main$4 as LogStream, _sfc_main$5 as MobileTokenizer, _sfc_main$6 as SVGIcon, _sfc_main$7 as Stat, _sfc_main$8 as TransferList, _sfc_main$9 as TransferListSelector, addCurvature, adjustTension, applyPersonality, bellIcon, calendarIcon, checkIcon, createDiffFile, eyeIcon, folderIcon, heartIcon, homeIcon, lockIcon, mailIcon, menuIcon, parseUnifiedDiff, roundCorners, searchIcon, settingsIcon, shieldIcon, starIcon, starterIcons, xIcon, xifBell, xifBellOverride, xifCheck, xifExamples, xifFile, xifHome, xifShield, xifStar };