@lvce-editor/extension-detail-view 7.11.0 → 7.13.0

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.
@@ -1,13 +1,13 @@
1
1
  const emptyObject = {};
2
- const RE_PLACEHOLDER = /\{(PH\d+)\}/g;
3
2
  const i18nString = (key, placeholders = emptyObject) => {
4
3
  if (placeholders === emptyObject) {
5
4
  return key;
6
5
  }
7
- const replacer = (match, rest) => {
8
- return placeholders[rest];
9
- };
10
- return key.replaceAll(RE_PLACEHOLDER, replacer);
6
+ let result = key;
7
+ for (const [placeholder, replacement] of Object.entries(placeholders)) {
8
+ result = result.split(`{${placeholder}}`).join(String(replacement));
9
+ }
10
+ return result;
11
11
  };
12
12
 
13
13
  const ActivationEvents$1 = 'Activation Events';
@@ -26,6 +26,7 @@ const Disable$1 = 'Disable';
26
26
  const Elements = 'Elements';
27
27
  const EmptyCommandsArray = 'The extension contributes an empty commands array.';
28
28
  const Enable$1 = 'Enable';
29
+ const ExtensionNotAvailable = 'The extension "{PH1}" is not available in this version of LVCE Editor.';
29
30
  const Features$2 = 'Features';
30
31
  const FileExtensions = 'File Extensions';
31
32
  const FileMatch = 'File Match';
@@ -37,7 +38,7 @@ const Installation = 'Installation';
37
38
  const InvalidLink = 'Invalid link';
38
39
  const Issues = 'Issues';
39
40
  const JsonValidation$1 = 'Json Validation';
40
- const Label = 'Label';
41
+ const Label$1 = 'Label';
41
42
  const LastReleased = 'Last Released';
42
43
  const LastUpdated = 'Last Updated';
43
44
  const License = 'License';
@@ -67,6 +68,8 @@ const Status = 'Status';
67
68
  const StringMustNotBeEmpty = 'String must not be empty';
68
69
  const Theme$1 = 'Theme';
69
70
  const Uninstall$1 = 'Uninstall';
71
+ const UnableToLoadExtension = 'Unable to load extension';
72
+ const UnableToLoadExtensionWithError = 'The extension details could not be loaded: {PH1}';
70
73
  const UnsupportedFeature = 'Unsupported Feature';
71
74
  const Version = 'Version';
72
75
  const WebViews$1 = 'WebViews';
@@ -114,7 +117,7 @@ const openInNewTab = () => {
114
117
  return i18nString(OpenInNewTab);
115
118
  };
116
119
  const label = () => {
117
- return i18nString(Label);
120
+ return i18nString(Label$1);
118
121
  };
119
122
  const openImageInNewTab$1 = () => {
120
123
  return i18nString(OpenImageInNewTab);
@@ -137,6 +140,11 @@ const setColorTheme$2 = () => {
137
140
  const enable = () => {
138
141
  return i18nString(Enable$1);
139
142
  };
143
+ const extensionNotAvailable = extensionId => {
144
+ return i18nString(ExtensionNotAvailable, {
145
+ PH1: extensionId
146
+ });
147
+ };
140
148
  const theme = () => {
141
149
  return i18nString(Theme$1);
142
150
  };
@@ -188,6 +196,14 @@ const elements = () => {
188
196
  const uninstall = () => {
189
197
  return i18nString(Uninstall$1);
190
198
  };
199
+ const unableToLoadExtension = () => {
200
+ return i18nString(UnableToLoadExtension);
201
+ };
202
+ const unableToLoadExtensionWithError = errorMessage => {
203
+ return i18nString(UnableToLoadExtensionWithError, {
204
+ PH1: errorMessage
205
+ });
206
+ };
191
207
  const scrollToTop = () => {
192
208
  return i18nString(ScrollToTop$1);
193
209
  };
@@ -302,9 +318,14 @@ const Panel = 'panel';
302
318
  const Tab = 'tab';
303
319
  const TabList = 'tablist';
304
320
 
321
+ const Audio = 0;
305
322
  const Button$1 = 1;
323
+ const Col = 2;
324
+ const ColGroup = 3;
306
325
  const Div = 4;
307
326
  const H1 = 5;
327
+ const Input = 6;
328
+ const Kbd = 7;
308
329
  const Span = 8;
309
330
  const Table$1 = 9;
310
331
  const TBody = 10;
@@ -313,20 +334,159 @@ const Text$1 = 12;
313
334
  const Th = 13;
314
335
  const THead = 14;
315
336
  const Tr = 15;
337
+ const I = 16;
316
338
  const Img = 17;
339
+ const Root = 0;
340
+ const Ins = 20;
341
+ const Del = 21;
317
342
  const H2 = 22;
343
+ const H3 = 23;
344
+ const H4 = 24;
345
+ const H5 = 25;
346
+ const H6 = 26;
347
+ const Article = 27;
318
348
  const Aside$1 = 28;
349
+ const Footer = 29;
350
+ const Header = 30;
351
+ const Nav = 40;
352
+ const Section = 41;
353
+ const Search = 42;
319
354
  const Dd = 43;
320
355
  const Dl = 44;
356
+ const Figcaption = 45;
357
+ const Figure = 46;
358
+ const Hr = 47;
321
359
  const Li = 48;
360
+ const Ol = 49;
322
361
  const P = 50;
323
362
  const Pre = 51;
324
363
  const A = 53;
364
+ const Abbr = 54;
365
+ const Br = 55;
366
+ const Cite = 56;
367
+ const Data = 57;
368
+ const Time = 58;
369
+ const Tfoot = 59;
325
370
  const Ul = 60;
371
+ const Video = 61;
372
+ const TextArea = 62;
373
+ const Select = 63;
374
+ const Option = 64;
326
375
  const Code$2 = 65;
376
+ const Label = 66;
327
377
  const Dt = 67;
378
+ const Iframe = 68;
379
+ const Main = 69;
380
+ const Strong = 70;
381
+ const Em = 71;
382
+ const Style = 72;
383
+ const Html = 73;
384
+ const Head = 74;
385
+ const Title = 75;
386
+ const Meta = 76;
387
+ const Canvas = 77;
388
+ const Form = 78;
389
+ const BlockQuote = 79;
390
+ const Quote = 80;
391
+ const Circle = 81;
392
+ const Defs = 82;
393
+ const Ellipse = 83;
394
+ const G = 84;
395
+ const Line = 85;
396
+ const Path = 86;
397
+ const Polygon = 87;
398
+ const Polyline = 88;
399
+ const Rect = 89;
400
+ const Svg = 90;
401
+ const Use = 91;
328
402
  const Reference = 100;
329
403
 
404
+ const VirtualDomElements = {
405
+ __proto__: null,
406
+ A,
407
+ Abbr,
408
+ Article,
409
+ Aside: Aside$1,
410
+ Audio,
411
+ BlockQuote,
412
+ Br,
413
+ Button: Button$1,
414
+ Canvas,
415
+ Circle,
416
+ Cite,
417
+ Code: Code$2,
418
+ Col,
419
+ ColGroup,
420
+ Data,
421
+ Dd,
422
+ Defs,
423
+ Del,
424
+ Div,
425
+ Dl,
426
+ Dt,
427
+ Ellipse,
428
+ Em,
429
+ Figcaption,
430
+ Figure,
431
+ Footer,
432
+ Form,
433
+ G,
434
+ H1,
435
+ H2,
436
+ H3,
437
+ H4,
438
+ H5,
439
+ H6,
440
+ Head,
441
+ Header,
442
+ Hr,
443
+ Html,
444
+ I,
445
+ Iframe,
446
+ Img,
447
+ Input,
448
+ Ins,
449
+ Kbd,
450
+ Label,
451
+ Li,
452
+ Line,
453
+ Main,
454
+ Meta,
455
+ Nav,
456
+ Ol,
457
+ Option,
458
+ P,
459
+ Path,
460
+ Polygon,
461
+ Polyline,
462
+ Pre,
463
+ Quote,
464
+ Rect,
465
+ Reference,
466
+ Root,
467
+ Search,
468
+ Section,
469
+ Select,
470
+ Span,
471
+ Strong,
472
+ Style,
473
+ Svg,
474
+ TBody,
475
+ THead,
476
+ Table: Table$1,
477
+ Td,
478
+ Text: Text$1,
479
+ TextArea,
480
+ Tfoot,
481
+ Th,
482
+ Time,
483
+ Title,
484
+ Tr,
485
+ Ul,
486
+ Use,
487
+ Video
488
+ };
489
+
330
490
  const ClientX = 'event.clientX';
331
491
  const ClientY = 'event.clientY';
332
492
  const TargetHref = 'event.target.href';
@@ -371,6 +531,8 @@ const text = data => {
371
531
  };
372
532
  };
373
533
 
534
+ new Set(Object.values(VirtualDomElements));
535
+
374
536
  const SetText = 1;
375
537
  const Replace = 2;
376
538
  const SetAttribute = 3;
@@ -642,6 +804,7 @@ const Categories = 'Categories';
642
804
  const Category = 'Category';
643
805
  const Changelog$1 = 'Changelog';
644
806
  const MaskIcon = 'MaskIcon';
807
+ const MaskIconWarning = 'MaskIconWarning';
645
808
  const ResourceIcon = 'ResourceIcon';
646
809
  const Code$1 = 'Code';
647
810
  const DefaultMarkdown = 'DefaultMarkdown';
@@ -651,6 +814,11 @@ const DefinitionListItemHeading = 'DefinitionListItemHeading';
651
814
  const DefinitionListItemValue = 'DefinitionListItemValue';
652
815
  const ExtensionDetail = 'ExtensionDetail';
653
816
  const ExtensionDetailDescription = 'ExtensionDetailDescription';
817
+ const ExtensionDetailError = 'ExtensionDetailError';
818
+ const ExtensionDetailErrorCard = 'ExtensionDetailErrorCard';
819
+ const ExtensionDetailErrorIcon = 'ExtensionDetailErrorIcon';
820
+ const ExtensionDetailErrorMessage = 'ExtensionDetailErrorMessage';
821
+ const ExtensionDetailErrorTitle = 'ExtensionDetailErrorTitle';
654
822
  const ExtensionDetailHeader = 'ExtensionDetailHeader';
655
823
  const ExtensionDetailHeaderActions = 'ExtensionDetailHeaderActions';
656
824
  const ExtensionDetailHeaderDetails = 'ExtensionDetailHeaderDetails';
@@ -1375,7 +1543,7 @@ class AssertionError extends Error {
1375
1543
  const Object$1 = 1;
1376
1544
  const Number$1 = 2;
1377
1545
  const Array$1 = 3;
1378
- const String = 4;
1546
+ const String$1 = 4;
1379
1547
  const Boolean$1 = 5;
1380
1548
  const Function = 6;
1381
1549
  const Null = 7;
@@ -1387,7 +1555,7 @@ const getType = value => {
1387
1555
  case 'function':
1388
1556
  return Function;
1389
1557
  case 'string':
1390
- return String;
1558
+ return String$1;
1391
1559
  case 'object':
1392
1560
  if (value === null) {
1393
1561
  return Null;
@@ -1410,7 +1578,7 @@ const number = value => {
1410
1578
  };
1411
1579
  const string = value => {
1412
1580
  const type = getType(value);
1413
- if (type !== String) {
1581
+ if (type !== String$1) {
1414
1582
  throw new AssertionError('expected value to be of type string');
1415
1583
  }
1416
1584
  };
@@ -1457,7 +1625,6 @@ const walkValue = (value, transferrables, isTransferrable) => {
1457
1625
  for (const property of Object.values(value)) {
1458
1626
  walkValue(property, transferrables, isTransferrable);
1459
1627
  }
1460
- return;
1461
1628
  }
1462
1629
  };
1463
1630
  const getTransferrables = value => {
@@ -1611,7 +1778,14 @@ class IpcError extends VError {
1611
1778
  const cause = new Error(message);
1612
1779
  // @ts-ignore
1613
1780
  cause.code = code;
1614
- cause.stack = stack;
1781
+ if (stack) {
1782
+ Object.defineProperty(cause, 'stack', {
1783
+ configurable: true,
1784
+ enumerable: false,
1785
+ value: stack,
1786
+ writable: true
1787
+ });
1788
+ }
1615
1789
  super(cause, betterMessage);
1616
1790
  } else {
1617
1791
  super(betterMessage);
@@ -2810,6 +2984,7 @@ const HandleTabFocus = 18;
2810
2984
  const HandleResourceLinkClick = 19;
2811
2985
  const HandleMouseEnterEnable = 20;
2812
2986
  const HandleMouseLeaveEnable = 21;
2987
+ const HandleHeaderContextMenu = 22;
2813
2988
 
2814
2989
  const ActivationEvents = 'ActivationEvents';
2815
2990
  const Changelog = 'Changelog';
@@ -2851,19 +3026,22 @@ const {
2851
3026
  set: set$3
2852
3027
  } = MarkdownWorker;
2853
3028
 
3029
+ const addScrollToTopVirtualDom = dom => {
3030
+ const [firstNode, ...rest] = dom;
3031
+ const extraDom = getScrollToTopVirtualDom();
3032
+ return [{
3033
+ ...firstNode,
3034
+ childCount: firstNode.childCount + 1,
3035
+ onClick: HandleReadmeClick,
3036
+ onScroll: HandleReadmeScroll,
3037
+ onSelectionChange: HandleSelectionChange
3038
+ }, ...extraDom, ...rest];
3039
+ };
2854
3040
  const getMarkdownVirtualDom = async (html, options) => {
2855
3041
  string(html);
2856
3042
  const dom = await getVirtualDom(html);
2857
3043
  if (options?.scrollToTopEnabled) {
2858
- const [firstNode, ...rest] = dom;
2859
- const extraDom = getScrollToTopVirtualDom();
2860
- return [{
2861
- ...firstNode,
2862
- childCount: firstNode.childCount + 1,
2863
- onClick: HandleReadmeClick,
2864
- onScroll: HandleReadmeScroll,
2865
- onSelectionChange: HandleSelectionChange
2866
- }, ...extraDom, ...rest];
3044
+ return addScrollToTopVirtualDom(dom);
2867
3045
  }
2868
3046
  return dom;
2869
3047
  };
@@ -3431,6 +3609,8 @@ const create = (uid, uri, x, y, width, height, platform, assetDir) => {
3431
3609
  disabled: false,
3432
3610
  displaySize: '',
3433
3611
  downloadCount: 'n/a',
3612
+ errorMessage: '',
3613
+ errorTitle: '',
3434
3614
  extension: {},
3435
3615
  extensionId: '',
3436
3616
  extensionUri: '',
@@ -3703,6 +3883,96 @@ const getMenus = () => {
3703
3883
  }];
3704
3884
  };
3705
3885
 
3886
+ let mock;
3887
+ const getPage = url => {
3888
+ return Number(new URL(url).searchParams.get('page')) || 1;
3889
+ };
3890
+ const createGeneratedReleases = (url, count) => {
3891
+ const page = getPage(url);
3892
+ const start = (page - 1) * 100;
3893
+ const end = Math.min(start + 100, count);
3894
+ return Array.from({
3895
+ length: Math.max(0, end - start)
3896
+ }, (_, index) => {
3897
+ const releaseNumber = count - start - index;
3898
+ return {
3899
+ body: `Release notes for version ${releaseNumber}`,
3900
+ html_url: `https://github.com/test-owner/test-repository/releases/tag/v${releaseNumber}`,
3901
+ name: `Version ${releaseNumber}`,
3902
+ published_at: '2026-01-01T00:00:00Z',
3903
+ tag_name: `v${releaseNumber}`
3904
+ };
3905
+ });
3906
+ };
3907
+ const createMockResponse = (url, value) => {
3908
+ const status = value.status ?? 200;
3909
+ const statusText = value.statusText || getStatusText(status);
3910
+ const headers = new Headers(value.headers);
3911
+ if (value.type === 'invalid-json') {
3912
+ headers.set('content-type', 'text/html');
3913
+ return new Response('<!doctype html><title>Unexpected response</title>', {
3914
+ headers,
3915
+ status,
3916
+ statusText
3917
+ });
3918
+ }
3919
+ const body = value.type === 'generated' ? createGeneratedReleases(url, value.releaseCount ?? 0) : value.body ?? [];
3920
+ headers.set('content-type', 'application/json');
3921
+ return Response.json(body, {
3922
+ headers,
3923
+ status,
3924
+ statusText
3925
+ });
3926
+ };
3927
+ const getStatusText = status => {
3928
+ switch (status) {
3929
+ case 400:
3930
+ return 'Bad Request';
3931
+ case 401:
3932
+ return 'Unauthorized';
3933
+ case 403:
3934
+ return 'Forbidden';
3935
+ case 404:
3936
+ return 'Not Found';
3937
+ case 429:
3938
+ return 'Too Many Requests';
3939
+ case 500:
3940
+ return 'Internal Server Error';
3941
+ case 503:
3942
+ return 'Service Unavailable';
3943
+ default:
3944
+ return '';
3945
+ }
3946
+ };
3947
+ const request = url => {
3948
+ if (!mock) {
3949
+ return fetch(url, {
3950
+ headers: {
3951
+ Accept: 'application/vnd.github+json',
3952
+ 'X-GitHub-Api-Version': '2022-11-28'
3953
+ }
3954
+ });
3955
+ }
3956
+ if (mock.type === 'network-error') {
3957
+ return Promise.reject(new TypeError(mock.message || 'Failed to fetch'));
3958
+ }
3959
+ return Promise.resolve(createMockResponse(url, mock));
3960
+ };
3961
+ const mockGithubApi = value => {
3962
+ mock = value;
3963
+ };
3964
+ const handleMockGithubApi = (state, value) => {
3965
+ mockGithubApi(value);
3966
+ return state;
3967
+ };
3968
+ const resetGithubApiMock = () => {
3969
+ mock = undefined;
3970
+ };
3971
+ const handleResetGithubApiMock = state => {
3972
+ resetGithubApiMock();
3973
+ return state;
3974
+ };
3975
+
3706
3976
  const show2 = async (uid, menuId, x, y, args) => {
3707
3977
  await showContextMenu2(uid, menuId, x, y, args);
3708
3978
  };
@@ -3900,6 +4170,15 @@ const handleClickFeatures = async (state, name) => {
3900
4170
  };
3901
4171
 
3902
4172
  const handleClickScrollToTop = state => {
4173
+ if (state.selectedTab === Changelog) {
4174
+ if (state.changelogScrollTop === 0) {
4175
+ return state;
4176
+ }
4177
+ return {
4178
+ ...state,
4179
+ changelogScrollTop: 0
4180
+ };
4181
+ }
3903
4182
  const {
3904
4183
  readmeScrollTop
3905
4184
  } = state;
@@ -4033,6 +4312,16 @@ const getCurrentColorTheme = async () => {
4033
4312
  return '';
4034
4313
  };
4035
4314
 
4315
+ const getErrorMessage$1 = error => {
4316
+ if (error instanceof Error && error.message) {
4317
+ return error.message;
4318
+ }
4319
+ if (typeof error === 'string' && error) {
4320
+ return error;
4321
+ }
4322
+ return 'Unknown error';
4323
+ };
4324
+
4036
4325
  const getExtensionIdFromUri = uri => {
4037
4326
  const id = uri.slice('extension-detail://'.length);
4038
4327
  return id;
@@ -4045,6 +4334,47 @@ const getExtensionUri = (uri, platform, origin) => {
4045
4334
  return new URL(uri, origin).href;
4046
4335
  };
4047
4336
 
4337
+ const segmentRegex = /^[\w.-]+$/;
4338
+ const getRepositoryUrl = extension => {
4339
+ if (!extension || !hasProperty(extension, 'repository')) {
4340
+ return '';
4341
+ }
4342
+ const {
4343
+ repository
4344
+ } = extension;
4345
+ if (typeof repository === 'string') {
4346
+ return repository;
4347
+ }
4348
+ if (repository && hasProperty(repository, 'url') && typeof repository.url === 'string') {
4349
+ return repository.url;
4350
+ }
4351
+ return '';
4352
+ };
4353
+ const getGithubRepository = extension => {
4354
+ const rawUrl = getRepositoryUrl(extension).replace(/^git\+/, '');
4355
+ try {
4356
+ const url = new URL(rawUrl);
4357
+ if (url.protocol !== 'https:' || url.hostname.toLowerCase() !== 'github.com' || url.username || url.password || url.port) {
4358
+ return undefined;
4359
+ }
4360
+ const segments = url.pathname.split('/').filter(Boolean);
4361
+ if (segments.length !== 2) {
4362
+ return undefined;
4363
+ }
4364
+ const owner = segments[0];
4365
+ const repository = segments[1].replace(/\.git$/i, '');
4366
+ if (!owner || !repository || !segmentRegex.test(owner) || !segmentRegex.test(repository)) {
4367
+ return undefined;
4368
+ }
4369
+ return {
4370
+ owner,
4371
+ repository
4372
+ };
4373
+ } catch {
4374
+ return undefined;
4375
+ }
4376
+ };
4377
+
4048
4378
  const getLinkProtectionEnabled = async () => {
4049
4379
  try {
4050
4380
  const setting = await getPreference('application.linkProtectionEnabled');
@@ -4507,23 +4837,8 @@ const formatCreated = (created, now = Date.now()) => {
4507
4837
  return relativeTimeFormat.format(value, unit);
4508
4838
  };
4509
4839
 
4510
- const formatLastUpdated = lastUpdated => {
4511
- if (lastUpdated === null) {
4512
- return 'n/a';
4513
- }
4514
- try {
4515
- const date = new Date(lastUpdated * 1000);
4516
- if (Number.isNaN(date.getTime())) {
4517
- return 'n/a';
4518
- }
4519
- return date.toLocaleDateString(undefined, {
4520
- day: 'numeric',
4521
- month: 'short',
4522
- year: 'numeric'
4523
- });
4524
- } catch {
4525
- return 'n/a';
4526
- }
4840
+ const formatLastUpdated = (lastUpdated, now = Date.now()) => {
4841
+ return formatCreated(lastUpdated, now);
4527
4842
  };
4528
4843
 
4529
4844
  const getSizeEntries = (showSizeLink, displaySize, extensionUri) => {
@@ -4704,6 +5019,16 @@ const parseCreated = extension => {
4704
5019
  return timestamp;
4705
5020
  };
4706
5021
 
5022
+ const secondsToMillisecondsThreshold = 10_000_000_000;
5023
+ const normalizeTimestamp = timestamp => {
5024
+ if (!Number.isFinite(timestamp) || timestamp <= 0) {
5025
+ return null;
5026
+ }
5027
+ if (timestamp < secondsToMillisecondsThreshold) {
5028
+ return timestamp * 1000;
5029
+ }
5030
+ return timestamp;
5031
+ };
4707
5032
  const parseLastUpdated = extension => {
4708
5033
  if (!extension || typeof extension !== 'object') {
4709
5034
  return null;
@@ -4715,16 +5040,14 @@ const parseLastUpdated = extension => {
4715
5040
  return null;
4716
5041
  }
4717
5042
  if (typeof lastUpdated === 'number') {
4718
- if (Number.isFinite(lastUpdated) && lastUpdated > 0) {
4719
- return lastUpdated;
4720
- }
4721
- return null;
5043
+ return normalizeTimestamp(lastUpdated);
4722
5044
  }
4723
5045
  if (typeof lastUpdated === 'string') {
4724
- const parsed = Number.parseFloat(lastUpdated);
4725
- if (Number.isFinite(parsed) && parsed > 0) {
4726
- return parsed;
5046
+ const numericTimestamp = Number(lastUpdated);
5047
+ if (Number.isFinite(numericTimestamp)) {
5048
+ return normalizeTimestamp(numericTimestamp);
4727
5049
  }
5050
+ return normalizeTimestamp(Date.parse(lastUpdated));
4728
5051
  }
4729
5052
  return null;
4730
5053
  };
@@ -4777,7 +5100,7 @@ const restoreState = savedState => {
4777
5100
  const isEnabled = tab => {
4778
5101
  return tab.enabled;
4779
5102
  };
4780
- const loadContent = async (state, platform, savedState, isTest = false) => {
5103
+ const loadContentInternal = async (state, platform, savedState, isTest = false) => {
4781
5104
  if (isTest) {
4782
5105
  savedState = undefined;
4783
5106
  }
@@ -4839,7 +5162,8 @@ const loadContent = async (state, platform, savedState, isTest = false) => {
4839
5162
  } = restoreState(savedState);
4840
5163
  const features = getFeatures(selectedFeature || Theme, extension);
4841
5164
  const hasFeatures = features.length > 0;
4842
- const tabs = getTabs(selectedTab, hasReadme, hasFeatures, hasChangelog);
5165
+ const hasGithubReleases = Boolean(getGithubRepository(extension));
5166
+ const tabs = getTabs(selectedTab, hasReadme, hasFeatures, hasChangelog || hasGithubReleases);
4843
5167
  const enabledTabs = tabs.filter(isEnabled);
4844
5168
  const sizeValue = getViewletSize(width || 0);
4845
5169
  const showSizeLink = platform !== Web$1;
@@ -4872,6 +5196,8 @@ const loadContent = async (state, platform, savedState, isTest = false) => {
4872
5196
  disabled,
4873
5197
  displaySize,
4874
5198
  downloadCount,
5199
+ errorMessage: '',
5200
+ errorTitle: '',
4875
5201
  extension,
4876
5202
  extensionId,
4877
5203
  extensionUri,
@@ -4908,6 +5234,21 @@ const loadContent = async (state, platform, savedState, isTest = false) => {
4908
5234
  tabs: enabledTabs
4909
5235
  };
4910
5236
  };
5237
+ const loadContent = async (state, platform, savedState, isTest = false) => {
5238
+ try {
5239
+ return await loadContentInternal(state, platform, savedState, isTest);
5240
+ } catch (error) {
5241
+ const extensionId = getExtensionIdFromUri(state.uri);
5242
+ const errorMessage = error instanceof ExtensionNotFoundError ? extensionNotAvailable(extensionId) : unableToLoadExtensionWithError(getErrorMessage$1(error));
5243
+ return {
5244
+ ...state,
5245
+ errorMessage,
5246
+ errorTitle: unableToLoadExtension(),
5247
+ extensionId,
5248
+ initial: false
5249
+ };
5250
+ }
5251
+ };
4911
5252
 
4912
5253
  const handleExtensionsChanged = async state => {
4913
5254
  const {
@@ -4927,6 +5268,10 @@ const handleExtensionsStatusUpdate = async state => {
4927
5268
  };
4928
5269
  };
4929
5270
 
5271
+ const handleHeaderContextMenu = async state => {
5272
+ return state;
5273
+ };
5274
+
4930
5275
  const handleIconError = state => {
4931
5276
  const {
4932
5277
  assetDir,
@@ -5057,6 +5402,13 @@ const handleResourceLinkClick = async (state, href) => {
5057
5402
 
5058
5403
  const handleScroll = (state, scrollTop, scrollSource = Script) => {
5059
5404
  const newScrollTop = Math.max(0, scrollTop);
5405
+ if (state.selectedTab === Changelog) {
5406
+ return {
5407
+ ...state,
5408
+ changelogScrollTop: newScrollTop,
5409
+ scrollSource
5410
+ };
5411
+ }
5060
5412
  return {
5061
5413
  ...state,
5062
5414
  readmeScrollTop: newScrollTop,
@@ -5083,6 +5435,39 @@ const handleTabFocus = (state, name) => {
5083
5435
  };
5084
5436
  };
5085
5437
 
5438
+ const escapeMarkdown = value => {
5439
+ return value.replaceAll('\\', '\\\\').replaceAll('[', '\\[').replaceAll(']', '\\]');
5440
+ };
5441
+ const getPublishedText = publishedAt => {
5442
+ if (!publishedAt) {
5443
+ return 'Publication date unavailable';
5444
+ }
5445
+ const date = new Date(publishedAt);
5446
+ if (Number.isNaN(date.getTime())) {
5447
+ return 'Publication date unavailable';
5448
+ }
5449
+ return `Published ${date.toLocaleDateString(undefined, {
5450
+ dateStyle: 'long'
5451
+ })}`;
5452
+ };
5453
+ const getGithubReleasesMarkdown = (releases, githubRepository) => {
5454
+ if (releases.length === 0) {
5455
+ return `# Changelog\n\nNo GitHub releases were found for **${escapeMarkdown(githubRepository.owner)}/${escapeMarkdown(githubRepository.repository)}**.`;
5456
+ }
5457
+ return releases.map(release => {
5458
+ const title = release.name || release.tagName;
5459
+ const body = release.body.trim() || '_No release notes were provided._';
5460
+ return `# [${escapeMarkdown(title)}](${release.htmlUrl})\n\n${getPublishedText(release.publishedAt)} · \`${release.tagName}\`\n\n${body}`;
5461
+ }).join('\n\n---\n\n');
5462
+ };
5463
+
5464
+ class GithubReleasesError extends Error {
5465
+ constructor(message) {
5466
+ super(message);
5467
+ this.name = 'GithubReleasesError';
5468
+ }
5469
+ }
5470
+
5086
5471
  const error = async error => {
5087
5472
  // TODO send message to error worker or log worker
5088
5473
  // @ts-ignore
@@ -5103,21 +5488,206 @@ const loadChangelogContent = async path => {
5103
5488
  }
5104
5489
  };
5105
5490
 
5491
+ const pageSize = 100;
5492
+ const maximumReleases = 100;
5493
+ const getErrorMessage = async response => {
5494
+ try {
5495
+ const body = await response.clone().json();
5496
+ if (body && hasProperty(body, 'message') && typeof body.message === 'string') {
5497
+ return body.message;
5498
+ }
5499
+ } catch {
5500
+ // The status code still provides a useful error when the error body is invalid.
5501
+ }
5502
+ return '';
5503
+ };
5504
+ const getHttpError = async response => {
5505
+ const apiMessage = await getErrorMessage(response);
5506
+ const rateLimitRemaining = response.headers.get('x-ratelimit-remaining');
5507
+ if (response.status === 429 || response.status === 403 && (rateLimitRemaining === '0' || /rate limit/i.test(apiMessage))) {
5508
+ const resetValue = Number(response.headers.get('x-ratelimit-reset'));
5509
+ const retry = Number.isFinite(resetValue) && resetValue > 0 ? ` Try again after ${new Date(resetValue * 1000).toLocaleString()}.` : ' Please try again later.';
5510
+ return new GithubReleasesError(`GitHub API rate limit exceeded.${retry}`);
5511
+ }
5512
+ if (response.status === 404) {
5513
+ return new GithubReleasesError('The GitHub repository could not be found or is not publicly reachable.');
5514
+ }
5515
+ if (response.status >= 500) {
5516
+ return new GithubReleasesError(`GitHub is temporarily unavailable (server error ${response.status}). Please try again later.`);
5517
+ }
5518
+ if (response.status >= 400) {
5519
+ const detail = apiMessage ? `: ${apiMessage}` : '';
5520
+ return new GithubReleasesError(`GitHub rejected the releases request (${response.status} ${response.statusText || 'Client Error'})${detail}.`);
5521
+ }
5522
+ return new GithubReleasesError(`GitHub returned an unexpected HTTP status (${response.status}).`);
5523
+ };
5524
+ const parseNullableString = value => {
5525
+ if (typeof value === 'string') {
5526
+ return value;
5527
+ }
5528
+ if (value === null) {
5529
+ return '';
5530
+ }
5531
+ return undefined;
5532
+ };
5533
+ const parseRelease = value => {
5534
+ if (!value || !hasProperty(value, 'body') || !hasProperty(value, 'html_url') || !hasProperty(value, 'name') || !hasProperty(value, 'published_at') || !hasProperty(value, 'tag_name')) {
5535
+ return undefined;
5536
+ }
5537
+ const body = parseNullableString(value.body);
5538
+ const name = parseNullableString(value.name);
5539
+ const publishedAt = parseNullableString(value.published_at);
5540
+ if (body === undefined || name === undefined || publishedAt === undefined || typeof value.html_url !== 'string' || typeof value.tag_name !== 'string') {
5541
+ return undefined;
5542
+ }
5543
+ try {
5544
+ const htmlUrl = new URL(value.html_url);
5545
+ if (htmlUrl.protocol !== 'https:' || htmlUrl.hostname !== 'github.com') {
5546
+ return undefined;
5547
+ }
5548
+ } catch {
5549
+ return undefined;
5550
+ }
5551
+ return {
5552
+ body,
5553
+ htmlUrl: value.html_url,
5554
+ name,
5555
+ publishedAt,
5556
+ tagName: value.tag_name
5557
+ };
5558
+ };
5559
+ const parsePage = async response => {
5560
+ let value;
5561
+ try {
5562
+ value = await response.json();
5563
+ } catch {
5564
+ throw new GithubReleasesError('GitHub returned an invalid response instead of JSON release data. Please try again later.');
5565
+ }
5566
+ if (!Array.isArray(value)) {
5567
+ throw new GithubReleasesError('GitHub returned invalid release data. Please try again later.');
5568
+ }
5569
+ const releases = value.map(parseRelease);
5570
+ if (releases.some(release => !release)) {
5571
+ throw new GithubReleasesError('GitHub returned invalid release data. Please try again later.');
5572
+ }
5573
+ return releases;
5574
+ };
5575
+ const loadGithubReleases = async ({
5576
+ owner,
5577
+ repository
5578
+ }) => {
5579
+ const releases = [];
5580
+ for (let page = 1;; page++) {
5581
+ const url = `https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repository)}/releases?per_page=${pageSize}&page=${page}`;
5582
+ let response;
5583
+ try {
5584
+ response = await request(url);
5585
+ } catch {
5586
+ throw new GithubReleasesError('GitHub releases could not be loaded because GitHub is not reachable. Check your connection and try again.');
5587
+ }
5588
+ if (!response.ok) {
5589
+ throw await getHttpError(response);
5590
+ }
5591
+ const pageReleases = await parsePage(response);
5592
+ const remaining = maximumReleases - releases.length;
5593
+ if (pageReleases.length > remaining) {
5594
+ releases.push(...pageReleases.slice(0, remaining));
5595
+ return {
5596
+ isTruncated: true,
5597
+ releases
5598
+ };
5599
+ }
5600
+ releases.push(...pageReleases);
5601
+ if (pageReleases.length < pageSize) {
5602
+ return {
5603
+ isTruncated: false,
5604
+ releases
5605
+ };
5606
+ }
5607
+ }
5608
+ };
5609
+
5610
+ const releaseBatchSize = 100;
5611
+ const mergeMarkdownVirtualDoms = chunks => {
5612
+ let root;
5613
+ const children = [];
5614
+ for (const chunk of chunks) {
5615
+ const [chunkRoot, ...chunkChildren] = chunk;
5616
+ if (!chunkRoot) {
5617
+ continue;
5618
+ }
5619
+ root = root ? {
5620
+ ...root,
5621
+ childCount: root.childCount + chunkRoot.childCount
5622
+ } : chunkRoot;
5623
+ children.push(...chunkChildren);
5624
+ }
5625
+ return root ? [root, ...children] : [];
5626
+ };
5627
+ const renderGithubReleases = async (result, githubRepository, languages, locationProtocol) => {
5628
+ const chunks = [];
5629
+ const {
5630
+ isTruncated,
5631
+ releases
5632
+ } = result;
5633
+ const releaseGroups = releases.length === 0 ? [[]] : Array.from({
5634
+ length: Math.ceil(releases.length / releaseBatchSize)
5635
+ }, (_, index) => {
5636
+ return releases.slice(index * releaseBatchSize, (index + 1) * releaseBatchSize);
5637
+ });
5638
+ const baseUrl = `https://github.com/${githubRepository.owner}/${githubRepository.repository}/blob/HEAD/`;
5639
+ for (const [index, releaseGroup] of releaseGroups.entries()) {
5640
+ const limitMessage = index === 0 && isTruncated ? `> Showing the newest ${releases.length} GitHub releases. Older releases are not displayed to keep the editor responsive.\n\n` : '';
5641
+ const markdown = limitMessage + getGithubReleasesMarkdown(releaseGroup, githubRepository);
5642
+ const html = await renderMarkdown(markdown, {
5643
+ baseUrl,
5644
+ languages,
5645
+ linksExternal: true,
5646
+ locationProtocol
5647
+ });
5648
+ chunks.push([...(await getMarkdownVirtualDom(html))]);
5649
+ }
5650
+ return addScrollToTopVirtualDom(mergeMarkdownVirtualDoms(chunks));
5651
+ };
5106
5652
  const selectTabChangelog = async state => {
5107
5653
  const {
5108
5654
  baseUrl,
5655
+ extension,
5109
5656
  extensionUri,
5110
5657
  languages,
5111
5658
  locationProtocol,
5112
5659
  tabs
5113
5660
  } = state;
5114
- const changelogContent = await loadChangelogContent(extensionUri);
5115
- const changelogMarkdownHtml = await renderMarkdown(changelogContent, {
5116
- baseUrl,
5117
- languages,
5118
- locationProtocol
5119
- });
5120
- const changelogDom = await getMarkdownVirtualDom(changelogMarkdownHtml);
5661
+ const githubRepository = getGithubRepository(extension);
5662
+ let changelogDom;
5663
+ if (githubRepository) {
5664
+ try {
5665
+ const result = await loadGithubReleases(githubRepository);
5666
+ changelogDom = await renderGithubReleases(result, githubRepository, languages, locationProtocol);
5667
+ } catch (error) {
5668
+ const message = error instanceof GithubReleasesError ? error.message : 'GitHub releases could not be loaded. Please try again later.';
5669
+ const html = await renderMarkdown(`# Changelog\n\n${message}`, {
5670
+ baseUrl,
5671
+ languages,
5672
+ linksExternal: true,
5673
+ locationProtocol
5674
+ });
5675
+ changelogDom = await getMarkdownVirtualDom(html, {
5676
+ scrollToTopEnabled: true
5677
+ });
5678
+ }
5679
+ } else {
5680
+ const changelogContent = await loadChangelogContent(extensionUri);
5681
+ const html = await renderMarkdown(changelogContent, {
5682
+ baseUrl,
5683
+ languages,
5684
+ linksExternal: true,
5685
+ locationProtocol
5686
+ });
5687
+ changelogDom = await getMarkdownVirtualDom(html, {
5688
+ scrollToTopEnabled: true
5689
+ });
5690
+ }
5121
5691
  const newTabs = tabs.map(tab => {
5122
5692
  return {
5123
5693
  ...tab,
@@ -5391,7 +5961,8 @@ const getChangelogVirtualDom = changelogDom => {
5391
5961
  // TODO set tabpanel role
5392
5962
  return [{
5393
5963
  childCount: 1,
5394
- className: Changelog$1,
5964
+ className: `${ExtensionDetailPanel} ${Changelog$1}`,
5965
+ role: Panel,
5395
5966
  type: Div
5396
5967
  }, ...changelogDom];
5397
5968
  };
@@ -5695,6 +6266,31 @@ const getExtensionDetailContentVirtualDom = (sanitizedReadmeHtml, selectedTab, w
5695
6266
  }
5696
6267
  };
5697
6268
 
6269
+ const getExtensionDetailErrorVirtualDom = (title, message) => {
6270
+ return [{
6271
+ childCount: 1,
6272
+ className: mergeClassNames(Viewlet, ExtensionDetail, ExtensionDetailError),
6273
+ type: Div
6274
+ }, {
6275
+ childCount: 3,
6276
+ className: ExtensionDetailErrorCard,
6277
+ role: 'alert',
6278
+ type: Div
6279
+ }, {
6280
+ childCount: 0,
6281
+ className: mergeClassNames(MaskIcon, MaskIconWarning, ExtensionDetailErrorIcon),
6282
+ type: Div
6283
+ }, {
6284
+ childCount: 1,
6285
+ className: ExtensionDetailErrorTitle,
6286
+ type: H1
6287
+ }, text(title), {
6288
+ childCount: 1,
6289
+ className: ExtensionDetailErrorMessage,
6290
+ type: P
6291
+ }, text(message)];
6292
+ };
6293
+
5698
6294
  const getExtensionDetailDescriptionVirtualDom = description => {
5699
6295
  return [{
5700
6296
  childCount: 1,
@@ -5807,6 +6403,7 @@ const getExtensionDetailHeaderVirtualDom = (name, iconSrc, description, badge, b
5807
6403
  }, getExtensionDetailIconVirtualDom(iconSrc), {
5808
6404
  childCount: 3,
5809
6405
  className: ExtensionDetailHeaderDetails,
6406
+ onContextMenu: HandleHeaderContextMenu,
5810
6407
  type: Div
5811
6408
  }, ...getExtensionDetailNameVirtualDom(name, badge), ...getExtensionDetailDescriptionVirtualDom(description), ...getExtensionDetailHeaderActionsVirtualDom(buttonDefs, settingsButtonEnabled)];
5812
6409
  return dom;
@@ -5863,6 +6460,9 @@ const getClassNames = size => {
5863
6460
  };
5864
6461
 
5865
6462
  const getExtensionDetailVirtualDom = (newState, selectedTab) => {
6463
+ if (newState.errorMessage) {
6464
+ return getExtensionDetailErrorVirtualDom(newState.errorTitle, newState.errorMessage);
6465
+ }
5866
6466
  // TODO move this to view model so that rendering occurs like
5867
6467
  // 1. state
5868
6468
  // 2. view model
@@ -6029,6 +6629,10 @@ const renderEventListeners = () => {
6029
6629
  name: HandleImageContextMenu,
6030
6630
  params: ['handleImageContextMenu', ClientX, ClientY],
6031
6631
  preventDefault: true
6632
+ }, {
6633
+ name: HandleHeaderContextMenu,
6634
+ params: ['handleHeaderContextMenu'],
6635
+ preventDefault: true
6032
6636
  }, {
6033
6637
  name: HandleReadmeScroll,
6034
6638
  params: ['handleScroll', 'event.target.scrollTop', User],
@@ -6141,6 +6745,7 @@ const commandMap = {
6141
6745
  'ExtensionDetail.handleExtensionsChanged': wrapCommand(handleExtensionsChanged),
6142
6746
  'ExtensionDetail.handleExtensionsStatusUpdate': wrapCommand(handleExtensionsStatusUpdate),
6143
6747
  'ExtensionDetail.handleFeaturesClick': wrapCommand(handleClickFeatures),
6748
+ 'ExtensionDetail.handleHeaderContextMenu': wrapCommand(handleHeaderContextMenu),
6144
6749
  'ExtensionDetail.handleIconError': wrapCommand(handleIconError),
6145
6750
  'ExtensionDetail.handleImageContextMenu': wrapCommand(handleImageContextMenu),
6146
6751
  'ExtensionDetail.handleMouseEnterEnable': wrapCommand(handleMouseEnterEnable),
@@ -6157,9 +6762,11 @@ const commandMap = {
6157
6762
  'ExtensionDetail.hideSizeLink': wrapCommand(hideSizeLink),
6158
6763
  'ExtensionDetail.initialize': initialize,
6159
6764
  'ExtensionDetail.loadContent2': wrapCommand(loadContent2),
6765
+ 'ExtensionDetail.mockGithubApi': wrapCommand(handleMockGithubApi),
6160
6766
  'ExtensionDetail.openImageInNewTab': wrapCommand(openImageInNewTab),
6161
6767
  'ExtensionDetail.render2': render2,
6162
6768
  'ExtensionDetail.renderEventListeners': renderEventListeners,
6769
+ 'ExtensionDetail.resetGithubApiMock': wrapCommand(handleResetGithubApiMock),
6163
6770
  'ExtensionDetail.resize': wrapCommand(resize),
6164
6771
  'ExtensionDetail.saveState': wrapGetter(saveState),
6165
6772
  'ExtensionDetail.selectTab': wrapCommand(selectTab),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lvce-editor/extension-detail-view",
3
- "version": "7.11.0",
3
+ "version": "7.13.0",
4
4
  "description": "Extension Detail View Worker",
5
5
  "repository": {
6
6
  "type": "git",