gt-sanity 0.0.6 → 1.0.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.
Files changed (88) hide show
  1. package/LICENSE.md +1 -1
  2. package/dist/index.d.mts +95 -73
  3. package/dist/index.d.ts +95 -73
  4. package/dist/index.js +9066 -1207
  5. package/dist/index.js.map +1 -1
  6. package/dist/index.mjs +9083 -1197
  7. package/dist/index.mjs.map +1 -1
  8. package/package.json +8 -3
  9. package/src/adapter/core.ts +41 -4
  10. package/src/adapter/getLocales.ts +2 -2
  11. package/src/components/TranslationsProvider.tsx +942 -0
  12. package/src/components/page/BatchProgress.tsx +27 -0
  13. package/src/components/page/ImportAllDialog.tsx +51 -0
  14. package/src/components/page/ImportMissingDialog.tsx +55 -0
  15. package/src/components/page/TranslateAllDialog.tsx +55 -0
  16. package/src/components/page/TranslationsTable.tsx +81 -0
  17. package/src/components/page/TranslationsTool.tsx +299 -837
  18. package/src/components/shared/BaseTranslationWrapper.tsx +82 -0
  19. package/src/components/shared/LocaleCheckbox.tsx +47 -0
  20. package/src/components/shared/SingleDocumentView.tsx +108 -0
  21. package/src/components/tab/TranslationView.tsx +379 -0
  22. package/src/components/tab/TranslationsTab.tsx +25 -0
  23. package/src/configuration/baseDocumentLevelConfig/documentLevelPatch.ts +6 -9
  24. package/src/configuration/baseDocumentLevelConfig/helpers/createI18nDocAndPatchMetadata.ts +5 -24
  25. package/src/configuration/baseDocumentLevelConfig/helpers/patchI18nDoc.ts +3 -23
  26. package/src/configuration/baseDocumentLevelConfig/index.ts +16 -68
  27. package/src/configuration/baseFieldLevelConfig.ts +15 -50
  28. package/src/index.ts +29 -43
  29. package/src/sanity-api/findDocuments.ts +44 -0
  30. package/src/sanity-api/publishDocuments.ts +49 -0
  31. package/src/sanity-api/resolveRefs.ts +146 -0
  32. package/src/serialization/BaseDocumentMerger.ts +138 -0
  33. package/src/serialization/BaseSerializationConfig.ts +220 -0
  34. package/src/serialization/__tests__/BaseDocumentDeserializer/__snapshots__/documentLevelDeserialization.test.ts.snap +189 -0
  35. package/src/serialization/__tests__/BaseDocumentDeserializer/__snapshots__/fieldLevelDeserialization.test.ts.snap +107 -0
  36. package/src/serialization/__tests__/BaseDocumentDeserializer/baseDeserialization.test.ts +397 -0
  37. package/src/serialization/__tests__/BaseDocumentDeserializer/documentLevelDeserialization.test.ts +107 -0
  38. package/src/serialization/__tests__/BaseDocumentDeserializer/fieldLevelDeserialization.test.ts +107 -0
  39. package/src/serialization/__tests__/BaseDocumentMerger/__snapshots__/documentLevelMerge.test.ts.snap +193 -0
  40. package/src/serialization/__tests__/BaseDocumentMerger/__snapshots__/fieldLevelMerge.test.ts.snap +97 -0
  41. package/src/serialization/__tests__/BaseDocumentMerger/baseMerge.test.ts +36 -0
  42. package/src/serialization/__tests__/BaseDocumentMerger/documentLevelMerge.test.ts +96 -0
  43. package/src/serialization/__tests__/BaseDocumentMerger/fieldLevelMerge.test.ts +142 -0
  44. package/src/serialization/__tests__/BaseDocumentMerger/utils.ts +52 -0
  45. package/src/serialization/__tests__/BaseDocumentSerializer/__snapshots__/documentInlineMarks.test.ts.snap +39 -0
  46. package/src/serialization/__tests__/BaseDocumentSerializer/__snapshots__/documentLevelSerialization.test.ts.snap +8 -0
  47. package/src/serialization/__tests__/BaseDocumentSerializer/__snapshots__/fieldLevelSerialization.test.ts.snap +8 -0
  48. package/src/serialization/__tests__/BaseDocumentSerializer/baseSerialization.test.ts +345 -0
  49. package/src/serialization/__tests__/BaseDocumentSerializer/documentInlineMarks.test.ts +53 -0
  50. package/src/serialization/__tests__/BaseDocumentSerializer/documentLevelSerialization.test.ts +120 -0
  51. package/src/serialization/__tests__/BaseDocumentSerializer/fieldLevelSerialization.test.ts +153 -0
  52. package/src/serialization/__tests__/BaseDocumentSerializer/utils.ts +27 -0
  53. package/src/serialization/__tests__/README +2 -0
  54. package/src/serialization/__tests__/__fixtures__/annotationAndInlineBlocks.json +140 -0
  55. package/src/serialization/__tests__/__fixtures__/customStyles.json +62 -0
  56. package/src/serialization/__tests__/__fixtures__/documentInlineMarks.json +70 -0
  57. package/src/serialization/__tests__/__fixtures__/documentLevelArticle.json +185 -0
  58. package/src/serialization/__tests__/__fixtures__/fieldLevelArticle.json +107 -0
  59. package/src/serialization/__tests__/__fixtures__/inlineDocumentLevelArticle.json +134 -0
  60. package/src/serialization/__tests__/__fixtures__/inlineSchema.ts +270 -0
  61. package/src/serialization/__tests__/__fixtures__/messy-html.html +26 -0
  62. package/src/serialization/__tests__/__fixtures__/nestedLanguageFields.json +54 -0
  63. package/src/serialization/__tests__/__fixtures__/schema.ts +310 -0
  64. package/src/serialization/__tests__/global.setup.ts +40 -0
  65. package/src/serialization/__tests__/helpers.ts +132 -0
  66. package/src/serialization/data.ts +82 -0
  67. package/src/serialization/deserialize/BaseDocumentDeserializer.ts +171 -0
  68. package/src/serialization/deserialize/helpers.ts +42 -0
  69. package/src/serialization/helpers.ts +18 -0
  70. package/src/serialization/index.ts +11 -0
  71. package/src/serialization/serialize/fieldFilters.ts +124 -0
  72. package/src/serialization/serialize/index.ts +284 -0
  73. package/src/serialization/types.ts +41 -0
  74. package/src/translation/importDocument.ts +4 -5
  75. package/src/translation/uploadFiles.ts +1 -1
  76. package/src/types.ts +3 -19
  77. package/src/utils/batchProcessor.ts +111 -0
  78. package/src/utils/importUtils.ts +95 -0
  79. package/src/utils/serialize.ts +25 -5
  80. package/src/utils/shared.ts +1 -1
  81. package/src/adapter/index.ts +0 -13
  82. package/src/components/NewTask.tsx +0 -251
  83. package/src/components/TaskView.tsx +0 -257
  84. package/src/components/TranslationContext.tsx +0 -24
  85. package/src/components/TranslationView.tsx +0 -114
  86. package/src/components/TranslationsTab.tsx +0 -181
  87. /package/src/components/{LanguageStatus.tsx → shared/LanguageStatus.tsx} +0 -0
  88. /package/src/components/{ProgressBar.tsx → shared/ProgressBar.tsx} +0 -0
@@ -0,0 +1,310 @@
1
+ import { Schema } from '@sanity/schema';
2
+
3
+ const arrayField = {
4
+ name: 'arrayField',
5
+ title: 'Array Field',
6
+ type: 'array',
7
+ of: [
8
+ {
9
+ type: 'block',
10
+ marks: {
11
+ annotations: [{ type: 'linkField' }],
12
+ },
13
+ },
14
+ { type: 'objectField' },
15
+ { type: 'linkField' },
16
+ ],
17
+ };
18
+ const linkField = {
19
+ name: 'linkField',
20
+ title: 'Link Field',
21
+ type: 'object',
22
+ fields: [
23
+ {
24
+ name: 'label',
25
+ title: 'Label',
26
+ type: 'string',
27
+ },
28
+ {
29
+ name: 'linkType',
30
+ title: 'Link Type',
31
+ type: 'string',
32
+ options: {
33
+ list: [
34
+ { title: 'None', value: 'none' },
35
+ { title: 'URL', value: 'href' },
36
+ { title: 'Page', value: 'page' },
37
+ { title: 'Simple Page', value: 'simplePage' },
38
+ { title: 'Post', value: 'post' },
39
+ { title: 'File', value: 'file' },
40
+ ],
41
+ },
42
+ },
43
+ {
44
+ name: 'href',
45
+ title: 'URL',
46
+ type: 'url',
47
+ hidden: ({ parent }: { parent: any }) => parent?.linkType !== 'href',
48
+ },
49
+ {
50
+ name: 'page',
51
+ title: 'Page',
52
+ type: 'reference',
53
+ to: [{ type: 'page' }],
54
+ weak: true,
55
+ hidden: ({ parent }: { parent: any }) => parent?.linkType !== 'page',
56
+ },
57
+ {
58
+ name: 'simplePage',
59
+ title: 'Simple Page',
60
+ type: 'reference',
61
+ to: [{ type: 'simplePage' }],
62
+ weak: true,
63
+ hidden: ({ parent }: { parent: any }) =>
64
+ parent?.linkType !== 'simplePage',
65
+ },
66
+ {
67
+ name: 'post',
68
+ title: 'Post',
69
+ type: 'reference',
70
+ to: [{ type: 'post' }],
71
+ weak: true,
72
+ hidden: ({ parent }: { parent: any }) => parent?.linkType !== 'post',
73
+ },
74
+ {
75
+ name: 'file',
76
+ title: 'File',
77
+ type: 'file',
78
+ hidden: ({ parent }: { parent: any }) => parent?.linkType !== 'file',
79
+ },
80
+ {
81
+ name: 'openInNewTab',
82
+ title: 'Open in New Tab',
83
+ type: 'boolean',
84
+ },
85
+ ],
86
+ };
87
+
88
+ const childObjectField = {
89
+ name: 'childObjectField',
90
+ title: 'Child Object Field',
91
+ type: 'object',
92
+ fields: [
93
+ {
94
+ name: 'title',
95
+ title: 'Title',
96
+ type: 'string',
97
+ },
98
+ {
99
+ name: 'content',
100
+ title: 'Content',
101
+ type: 'array',
102
+ of: [
103
+ {
104
+ type: 'block',
105
+ marks: {
106
+ annotations: [{ type: 'linkField' }],
107
+ },
108
+ },
109
+ { type: 'linkField' },
110
+ ],
111
+ },
112
+ ],
113
+ };
114
+
115
+ const objectField = {
116
+ name: 'objectField',
117
+ title: 'Object Field',
118
+ type: 'object',
119
+ fields: [
120
+ {
121
+ name: 'title',
122
+ title: 'Title',
123
+ type: 'string',
124
+ },
125
+ {
126
+ name: 'objectAsField',
127
+ title: 'Object As Field',
128
+ type: 'childObjectField',
129
+ },
130
+ {
131
+ name: 'nestedArrayField',
132
+ title: 'Nested Array Field',
133
+ type: 'array',
134
+ of: [
135
+ {
136
+ type: 'block',
137
+ marks: {
138
+ annotations: [{ type: 'linkField' }],
139
+ },
140
+ },
141
+ { type: 'childObjectField' },
142
+ ],
143
+ },
144
+ ],
145
+ };
146
+
147
+ const documentLevelArticle = {
148
+ name: 'documentLevelArticle',
149
+ title: 'Document Level Article',
150
+ type: 'document',
151
+ fields: [
152
+ {
153
+ name: 'title',
154
+ title: 'Title',
155
+ type: 'string',
156
+ },
157
+ {
158
+ name: 'meta',
159
+ title: 'Meta',
160
+ type: 'string',
161
+ localize: false,
162
+ },
163
+ {
164
+ name: 'snippet',
165
+ title: 'Snippet',
166
+ type: 'text',
167
+ },
168
+ {
169
+ name: 'tags',
170
+ title: 'Tags',
171
+ type: 'array',
172
+ of: [{ type: 'string' }],
173
+ },
174
+ {
175
+ name: 'hidden',
176
+ title: 'Hidden',
177
+ type: 'boolean',
178
+ },
179
+ {
180
+ name: 'config',
181
+ title: 'Config',
182
+ type: 'objectField',
183
+ },
184
+ {
185
+ name: 'content',
186
+ title: 'Content',
187
+ type: 'arrayField',
188
+ },
189
+ ],
190
+ };
191
+
192
+ function createLocaleFields(locales: string[], fieldType: Record<string, any>) {
193
+ return locales.map((locale) => ({
194
+ ...{ name: locale },
195
+ ...fieldType,
196
+ }));
197
+ }
198
+
199
+ const fieldLevelArticle = {
200
+ name: 'fieldLevelArticle',
201
+ title: 'Field Level Article',
202
+ type: 'document',
203
+ fields: [
204
+ {
205
+ name: 'title',
206
+ title: 'Title',
207
+ type: 'localeString',
208
+ },
209
+ {
210
+ name: 'meta',
211
+ title: 'Meta',
212
+ type: 'string',
213
+ localize: false,
214
+ },
215
+ {
216
+ name: 'snippet',
217
+ title: 'Snippet',
218
+ type: 'object',
219
+ fields: createLocaleFields(['en', 'fr', 'de'], { type: 'text' }),
220
+ },
221
+ {
222
+ name: 'tags',
223
+ title: 'Tags',
224
+ type: 'object',
225
+ fields: createLocaleFields(['en', 'fr', 'de'], {
226
+ type: 'array',
227
+ of: [{ type: 'string' }],
228
+ }),
229
+ },
230
+ {
231
+ name: 'hidden',
232
+ title: 'Hidden',
233
+ type: 'boolean',
234
+ },
235
+ {
236
+ name: 'config',
237
+ title: 'Config',
238
+ type: 'object',
239
+ fields: createLocaleFields(['en', 'fr', 'de'], { type: 'objectField' }),
240
+ },
241
+ {
242
+ name: 'content',
243
+ title: 'Content',
244
+ type: 'object',
245
+ fields: createLocaleFields(['en', 'fr', 'de'], { type: 'arrayField' }),
246
+ },
247
+ {
248
+ name: 'slices',
249
+ title: 'Slices',
250
+ type: 'array',
251
+ of: [
252
+ { type: 'localeBlock' },
253
+ { type: 'reference', to: [{ type: 'marketText' }] },
254
+ ],
255
+ },
256
+ {
257
+ name: 'pageFields',
258
+ title: 'Page Fields',
259
+ type: 'pageFields',
260
+ },
261
+ ],
262
+ };
263
+
264
+ const localeBlock = {
265
+ name: 'localeBlock',
266
+ title: 'Locale Block',
267
+ type: 'object',
268
+ fields: createLocaleFields(['en', 'fr_FR', 'de_DE'], { type: 'arrayField' }),
269
+ };
270
+
271
+ const localeString = {
272
+ name: 'localeString',
273
+ title: 'Locale String',
274
+ type: 'object',
275
+ fields: createLocaleFields(['en', 'fr_FR', 'de_DE'], { type: 'string' }),
276
+ };
277
+
278
+ const pageFields = {
279
+ name: 'pageFields',
280
+ title: 'Page Fields',
281
+ type: 'object',
282
+ fields: [
283
+ {
284
+ title: 'Page Name',
285
+ name: 'name',
286
+ type: 'localeString',
287
+ },
288
+ {
289
+ name: 'slug',
290
+ type: 'string',
291
+ },
292
+ ],
293
+ };
294
+
295
+ const types = [
296
+ arrayField,
297
+ linkField,
298
+ childObjectField,
299
+ objectField,
300
+ documentLevelArticle,
301
+ fieldLevelArticle,
302
+ pageFields,
303
+ localeBlock,
304
+ localeString,
305
+ ];
306
+
307
+ export default new Schema({
308
+ name: 'test',
309
+ types,
310
+ });
@@ -0,0 +1,40 @@
1
+ import {
2
+ PortableTextTextBlock,
3
+ PortableTextSpan,
4
+ PortableTextObject,
5
+ } from 'sanity';
6
+ import { vi } from 'vitest';
7
+
8
+ let mockTestKey = 0;
9
+
10
+ vi.mock('@portabletext/block-tools', async () => {
11
+ const originalModule = await vi.importActual<
12
+ typeof import('@portabletext/block-tools')
13
+ >('@portabletext/block-tools');
14
+ return {
15
+ ...originalModule,
16
+ //not ideal but vi.mock('@sanity/block-tools/src/util/randomKey.ts' is not working
17
+ htmlToBlocks: (html: string, blockContentType: any, options: any) => {
18
+ const blocks = originalModule.htmlToBlocks(
19
+ html,
20
+ blockContentType,
21
+ options
22
+ );
23
+ const newBlocks = blocks.map((block) => {
24
+ const newChildren = (
25
+ block as unknown as PortableTextTextBlock<
26
+ PortableTextSpan | PortableTextObject
27
+ >
28
+ ).children.map((child) => {
29
+ return { ...child, _key: `randomKey-${mockTestKey++}` };
30
+ });
31
+ return {
32
+ ...block,
33
+ children: newChildren,
34
+ _key: `randomKey-${mockTestKey++}`,
35
+ };
36
+ });
37
+ return newBlocks;
38
+ },
39
+ };
40
+ });
@@ -0,0 +1,132 @@
1
+ import { BaseDocumentSerializer, BaseDocumentDeserializer } from '../index';
2
+ import {
3
+ customSerializers,
4
+ customDeserializers,
5
+ customBlockDeserializers,
6
+ } from '../BaseSerializationConfig';
7
+ import { PortableTextBlock, SanityDocument, TypedObject } from 'sanity';
8
+ import { SerializedDocument, TranslationLevel } from '../types';
9
+ import schema from './__fixtures__/schema';
10
+ import clone from 'just-clone';
11
+
12
+ export const getSerialized = (
13
+ document: SanityDocument,
14
+ level: TranslationLevel
15
+ ): SerializedDocument => {
16
+ const serializer = BaseDocumentSerializer(schema);
17
+ return serializer.serializeDocument(document, level);
18
+ };
19
+
20
+ export const getDeserialized = (
21
+ document: SanityDocument,
22
+ level: TranslationLevel
23
+ ): Record<string, any> => {
24
+ const serialized = getSerialized(document, level);
25
+ const deserializer = BaseDocumentDeserializer;
26
+ return deserializer.deserializeDocument(serialized.content);
27
+ };
28
+
29
+ export const getValidFields = (
30
+ field: Record<string, any>
31
+ ): Record<string, any> => {
32
+ const invalidFields = ['_type', '_key'];
33
+ return Object.keys(field).filter((key) => !invalidFields.includes(key));
34
+ };
35
+
36
+ export const toPlainText = (blocks: PortableTextBlock[]): string => {
37
+ return blocks
38
+ .map((block) => {
39
+ if (block._type !== 'block' || !block.children) {
40
+ return '';
41
+ }
42
+ return (block.children as Array<any>).map((child) => child.text).join('');
43
+ })
44
+ .join('\n\n');
45
+ };
46
+
47
+ export const createCustomInnerHTML = (title: string): string =>
48
+ `Custom serializer works and includes title: '${title}'`;
49
+
50
+ const additionalSerializerTypes = {
51
+ //block and top-level tests
52
+ objectField: ({ value }: { value: TypedObject }) => {
53
+ const innerText = createCustomInnerHTML(value.title as string);
54
+ const html = `<div class="${value._type}" id="${value._key ?? value._id}">${innerText}</div>`;
55
+ return html;
56
+ },
57
+ //inline-level tests
58
+ childObjectField: ({ value }: { value: TypedObject }) => {
59
+ const innerText = createCustomInnerHTML(value.title as string);
60
+ const html = `<span class="${value._type}" id="${value._key ?? value._id}">${innerText}</span>`;
61
+ return html;
62
+ },
63
+ };
64
+
65
+ const tempSerializers = clone(customSerializers);
66
+ tempSerializers.types = {
67
+ ...tempSerializers.types,
68
+ ...additionalSerializerTypes,
69
+ };
70
+ tempSerializers.marks = {
71
+ annotation: ({ value, markType, children }) => {
72
+ return `<span class="${markType}" id="${value._key}">${children}</span>`;
73
+ },
74
+ };
75
+
76
+ export const addedCustomSerializers = tempSerializers;
77
+
78
+ export const addedDeserializerTypes = {
79
+ objectField: (html: HTMLElement): TypedObject => {
80
+ const title = html.innerHTML.split(':')[1].replace(/'/g, '').trim();
81
+ const _type = html.className;
82
+ const _key = html.id;
83
+ return { title, _type, _key };
84
+ },
85
+ };
86
+
87
+ const tempDeserializers = clone(customDeserializers);
88
+ tempDeserializers.types = {
89
+ ...tempDeserializers.types,
90
+ ...addedDeserializerTypes,
91
+ };
92
+
93
+ export const addedCustomDeserializers = tempDeserializers;
94
+
95
+ export const addedBlockDeserializers = [
96
+ ...customBlockDeserializers,
97
+ {
98
+ deserialize(el: HTMLElement): TypedObject | undefined {
99
+ if (!el.className || el.className.toLowerCase() !== 'childobjectfield') {
100
+ return undefined;
101
+ }
102
+
103
+ const title = el.innerHTML.split(':')[1].replace(/'/g, '').trim();
104
+ const _type = el.className;
105
+ const _key = el.id;
106
+
107
+ return { title, _type, _key };
108
+ },
109
+ },
110
+ {
111
+ deserialize(
112
+ el: HTMLElement,
113
+ //eslint-disable-next-line no-undef -- not picking up NodeListOf/ChildNode
114
+ next: (nodes: NodeListOf<ChildNode>) => any
115
+ ): TypedObject | undefined {
116
+ if (!el.className || el.className?.toLowerCase() !== 'annotation') {
117
+ return undefined;
118
+ }
119
+
120
+ const markDef = {
121
+ _key: el.id,
122
+ _type: 'annotation',
123
+ };
124
+
125
+ return {
126
+ _type: '__annotation',
127
+ markDef: markDef,
128
+ children: next(el.childNodes),
129
+ };
130
+ },
131
+ },
132
+ ];
@@ -0,0 +1,82 @@
1
+ export function attachGTData(
2
+ html: string,
3
+ data: Record<string, any>,
4
+ type: 'markDef'
5
+ ): string {
6
+ // Parse the HTML string to find the first element
7
+ const parser = new DOMParser();
8
+ const doc = parser.parseFromString(html, 'text/html');
9
+ const firstElement = doc.body.firstElementChild;
10
+
11
+ if (!firstElement) {
12
+ // If no element found, return original HTML
13
+ return html;
14
+ }
15
+
16
+ // Encode the data as base64 JSON
17
+ const encodedData = encode(JSON.stringify({ [type]: data }));
18
+
19
+ // Add the data-gt-internal attribute
20
+ firstElement.setAttribute('data-gt-internal', encodedData);
21
+
22
+ return firstElement.outerHTML;
23
+ }
24
+
25
+ export function detachGTData(html: string): {
26
+ html: string;
27
+ data?: Record<'markDef', Record<string, any>>;
28
+ } {
29
+ // Parse the HTML string to find the first element
30
+ const parser = new DOMParser();
31
+ const doc = parser.parseFromString(html, 'text/html');
32
+ const firstElement = doc.body.firstElementChild;
33
+
34
+ if (!firstElement) {
35
+ // If no element found, return original HTML with no data
36
+ return { html };
37
+ }
38
+
39
+ // Get the encoded data
40
+ const encodedData = firstElement.getAttribute('data-gt-internal');
41
+
42
+ let extractedData: Record<'markDef', Record<string, any>> | undefined;
43
+ if (encodedData) {
44
+ try {
45
+ // Decode and parse the data
46
+ const decodedData = decode(encodedData);
47
+ extractedData = JSON.parse(decodedData);
48
+
49
+ // Remove the data attribute to clean up the HTML
50
+ firstElement.removeAttribute('data-gt-internal');
51
+ } catch (error) {
52
+ console.warn('Failed to decode GT internal data:', error);
53
+ }
54
+ }
55
+
56
+ return {
57
+ html: firstElement.outerHTML,
58
+ data: extractedData,
59
+ };
60
+ }
61
+
62
+ // Encode a string to base64
63
+ export function encode(data: string): string {
64
+ // Browser path
65
+ const bytes = new TextEncoder().encode(data);
66
+ let binary = '';
67
+ for (let i = 0; i < bytes.length; i++) {
68
+ binary += String.fromCharCode(bytes[i]);
69
+ }
70
+ return btoa(binary);
71
+ }
72
+
73
+ // Decode a base64 string to a string
74
+ export function decode(base64: string): string {
75
+ // Browser path
76
+ const binary = atob(base64);
77
+ const bytes = new Uint8Array(binary.length);
78
+ for (let i = 0; i < binary.length; i++) {
79
+ bytes[i] = binary.charCodeAt(i);
80
+ }
81
+ return new TextDecoder().decode(bytes);
82
+ }