gt-sanity 0.0.5 → 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 (108) hide show
  1. package/LICENSE.md +1 -8
  2. package/README.md +5 -5
  3. package/dist/index.d.mts +122 -95
  4. package/dist/index.d.ts +122 -95
  5. package/dist/index.js +9089 -1119
  6. package/dist/index.js.map +1 -1
  7. package/dist/index.mjs +9099 -1100
  8. package/dist/index.mjs.map +1 -1
  9. package/package.json +11 -4
  10. package/src/adapter/core.ts +111 -9
  11. package/src/adapter/createTask.ts +1 -1
  12. package/src/adapter/getLocales.ts +2 -2
  13. package/src/adapter/types.ts +9 -0
  14. package/src/components/TranslationsProvider.tsx +942 -0
  15. package/src/components/page/BatchProgress.tsx +27 -0
  16. package/src/components/page/ImportAllDialog.tsx +51 -0
  17. package/src/components/page/ImportMissingDialog.tsx +55 -0
  18. package/src/components/page/TranslateAllDialog.tsx +55 -0
  19. package/src/components/page/TranslationsTable.tsx +81 -0
  20. package/src/components/page/TranslationsTool.tsx +338 -0
  21. package/src/components/shared/BaseTranslationWrapper.tsx +82 -0
  22. package/src/components/{LanguageStatus.tsx → shared/LanguageStatus.tsx} +2 -0
  23. package/src/components/shared/LocaleCheckbox.tsx +47 -0
  24. package/src/components/{ProgressBar.tsx → shared/ProgressBar.tsx} +2 -0
  25. package/src/components/shared/SingleDocumentView.tsx +108 -0
  26. package/src/components/tab/TranslationView.tsx +379 -0
  27. package/src/components/tab/TranslationsTab.tsx +25 -0
  28. package/src/configuration/baseDocumentLevelConfig/documentLevelPatch.ts +21 -11
  29. package/src/configuration/baseDocumentLevelConfig/helpers/createI18nDocAndPatchMetadata.ts +57 -23
  30. package/src/configuration/baseDocumentLevelConfig/helpers/createTranslationMetadata.ts +2 -0
  31. package/src/configuration/baseDocumentLevelConfig/helpers/getOrCreateTranslationMetadata.ts +2 -0
  32. package/src/configuration/baseDocumentLevelConfig/helpers/getTranslationMetadata.ts +2 -0
  33. package/src/configuration/baseDocumentLevelConfig/helpers/patchI18nDoc.ts +31 -8
  34. package/src/configuration/baseDocumentLevelConfig/index.ts +18 -101
  35. package/src/configuration/baseFieldLevelConfig.ts +19 -51
  36. package/src/configuration/utils/checkSerializationVersion.ts +2 -0
  37. package/src/configuration/utils/findDocumentAtRevision.ts +2 -0
  38. package/src/configuration/utils/findLatestDraft.ts +2 -0
  39. package/src/hooks/useClient.ts +3 -1
  40. package/src/hooks/useSecrets.ts +2 -0
  41. package/src/index.ts +91 -67
  42. package/src/sanity-api/findDocuments.ts +44 -0
  43. package/src/sanity-api/publishDocuments.ts +49 -0
  44. package/src/sanity-api/resolveRefs.ts +146 -0
  45. package/src/serialization/BaseDocumentMerger.ts +138 -0
  46. package/src/serialization/BaseSerializationConfig.ts +220 -0
  47. package/src/serialization/__tests__/BaseDocumentDeserializer/__snapshots__/documentLevelDeserialization.test.ts.snap +189 -0
  48. package/src/serialization/__tests__/BaseDocumentDeserializer/__snapshots__/fieldLevelDeserialization.test.ts.snap +107 -0
  49. package/src/serialization/__tests__/BaseDocumentDeserializer/baseDeserialization.test.ts +397 -0
  50. package/src/serialization/__tests__/BaseDocumentDeserializer/documentLevelDeserialization.test.ts +107 -0
  51. package/src/serialization/__tests__/BaseDocumentDeserializer/fieldLevelDeserialization.test.ts +107 -0
  52. package/src/serialization/__tests__/BaseDocumentMerger/__snapshots__/documentLevelMerge.test.ts.snap +193 -0
  53. package/src/serialization/__tests__/BaseDocumentMerger/__snapshots__/fieldLevelMerge.test.ts.snap +97 -0
  54. package/src/serialization/__tests__/BaseDocumentMerger/baseMerge.test.ts +36 -0
  55. package/src/serialization/__tests__/BaseDocumentMerger/documentLevelMerge.test.ts +96 -0
  56. package/src/serialization/__tests__/BaseDocumentMerger/fieldLevelMerge.test.ts +142 -0
  57. package/src/serialization/__tests__/BaseDocumentMerger/utils.ts +52 -0
  58. package/src/serialization/__tests__/BaseDocumentSerializer/__snapshots__/documentInlineMarks.test.ts.snap +39 -0
  59. package/src/serialization/__tests__/BaseDocumentSerializer/__snapshots__/documentLevelSerialization.test.ts.snap +8 -0
  60. package/src/serialization/__tests__/BaseDocumentSerializer/__snapshots__/fieldLevelSerialization.test.ts.snap +8 -0
  61. package/src/serialization/__tests__/BaseDocumentSerializer/baseSerialization.test.ts +345 -0
  62. package/src/serialization/__tests__/BaseDocumentSerializer/documentInlineMarks.test.ts +53 -0
  63. package/src/serialization/__tests__/BaseDocumentSerializer/documentLevelSerialization.test.ts +120 -0
  64. package/src/serialization/__tests__/BaseDocumentSerializer/fieldLevelSerialization.test.ts +153 -0
  65. package/src/serialization/__tests__/BaseDocumentSerializer/utils.ts +27 -0
  66. package/src/serialization/__tests__/README +2 -0
  67. package/src/serialization/__tests__/__fixtures__/annotationAndInlineBlocks.json +140 -0
  68. package/src/serialization/__tests__/__fixtures__/customStyles.json +62 -0
  69. package/src/serialization/__tests__/__fixtures__/documentInlineMarks.json +70 -0
  70. package/src/serialization/__tests__/__fixtures__/documentLevelArticle.json +185 -0
  71. package/src/serialization/__tests__/__fixtures__/fieldLevelArticle.json +107 -0
  72. package/src/serialization/__tests__/__fixtures__/inlineDocumentLevelArticle.json +134 -0
  73. package/src/serialization/__tests__/__fixtures__/inlineSchema.ts +270 -0
  74. package/src/serialization/__tests__/__fixtures__/messy-html.html +26 -0
  75. package/src/serialization/__tests__/__fixtures__/nestedLanguageFields.json +54 -0
  76. package/src/serialization/__tests__/__fixtures__/schema.ts +310 -0
  77. package/src/serialization/__tests__/global.setup.ts +40 -0
  78. package/src/serialization/__tests__/helpers.ts +132 -0
  79. package/src/serialization/data.ts +82 -0
  80. package/src/serialization/deserialize/BaseDocumentDeserializer.ts +171 -0
  81. package/src/serialization/deserialize/helpers.ts +42 -0
  82. package/src/serialization/helpers.ts +18 -0
  83. package/src/serialization/index.ts +11 -0
  84. package/src/serialization/serialize/fieldFilters.ts +124 -0
  85. package/src/serialization/serialize/index.ts +284 -0
  86. package/src/serialization/types.ts +41 -0
  87. package/src/translation/checkTranslationStatus.ts +42 -0
  88. package/src/translation/createJobs.ts +16 -0
  89. package/src/translation/downloadTranslations.ts +68 -0
  90. package/src/translation/importDocument.ts +23 -0
  91. package/src/translation/initProject.ts +61 -0
  92. package/src/translation/uploadFiles.ts +32 -0
  93. package/src/types.ts +7 -20
  94. package/src/utils/applyDocuments.ts +72 -0
  95. package/src/utils/batchProcessor.ts +111 -0
  96. package/src/utils/importUtils.ts +95 -0
  97. package/src/utils/serialize.ts +52 -0
  98. package/src/utils/shared.ts +1 -0
  99. package/src/adapter/index.ts +0 -13
  100. package/src/components/NewTask.tsx +0 -249
  101. package/src/components/TaskView.tsx +0 -255
  102. package/src/components/TranslationContext.tsx +0 -19
  103. package/src/components/TranslationView.tsx +0 -82
  104. package/src/components/TranslationsTab.tsx +0 -177
  105. package/src/configuration/baseDocumentLevelConfig/helpers/index.ts +0 -5
  106. package/src/configuration/baseDocumentLevelConfig/legacyDocumentLevelPatch.ts +0 -69
  107. package/src/configuration/index.ts +0 -18
  108. package/src/configuration/utils/index.ts +0 -3
@@ -0,0 +1,44 @@
1
+ import { SanityClient } from 'sanity';
2
+ import { pluginConfig } from '../adapter/core';
3
+
4
+ export async function findTranslatedDocuments(
5
+ documentId: string,
6
+ client: SanityClient
7
+ ) {
8
+ const documents = await client.fetch(
9
+ `*[_type == "translation.metadata" && references($documentId)]`
10
+ );
11
+ return documents;
12
+ }
13
+
14
+ export async function findTranslatedDocumentForLocale(
15
+ sourceDocumentId: string,
16
+ localeId: string,
17
+ client: SanityClient
18
+ ) {
19
+ const cleanDocId = sourceDocumentId.replace('drafts.', '');
20
+
21
+ // Try both clean and original IDs to be safe, and use -> to directly fetch the translated doc
22
+ const query = `*[
23
+ _type == "translation.metadata" &&
24
+ (
25
+ translations[_key == $sourceLocale][0].value._ref == $cleanDocId
26
+ ) &&
27
+ defined(translations[_key == $localeId])
28
+ ][0].translations[_key == $localeId][0].value->`;
29
+
30
+ const translatedDoc = await client.fetch(query, {
31
+ sourceLocale: pluginConfig.getSourceLocale(),
32
+ cleanDocId,
33
+ localeId,
34
+ });
35
+
36
+ return translatedDoc || null;
37
+ }
38
+
39
+ export async function findDocument(documentId: string, client: SanityClient) {
40
+ const query = `*[_id == $id]`;
41
+ const params = { id: documentId };
42
+ const document = await client.fetch(query, params);
43
+ return document[0] || null;
44
+ }
@@ -0,0 +1,49 @@
1
+ import { SanityClient } from 'sanity';
2
+ import { processBatch } from '../utils/batchProcessor';
3
+ import { findDocument } from './findDocuments';
4
+
5
+ export async function publishDocument(
6
+ documentId: string,
7
+ client: SanityClient
8
+ ) {
9
+ try {
10
+ // only publish if the document is a draft
11
+ if (documentId.startsWith('drafts.')) {
12
+ await client.action(
13
+ {
14
+ actionType: 'sanity.action.document.publish',
15
+ draftId: documentId,
16
+ publishedId: documentId.replace('drafts.', ''),
17
+ },
18
+ {}
19
+ );
20
+ }
21
+ } catch (error) {
22
+ console.error('Error publishing document', error);
23
+ }
24
+ }
25
+
26
+ export async function publishTranslations(
27
+ documentIds: string[],
28
+ client: SanityClient
29
+ ) {
30
+ const publishedDocumentIds: string[] = [];
31
+ await processBatch(
32
+ documentIds,
33
+ async (documentId) => {
34
+ const document = await findDocument(`drafts.${documentId}`, client);
35
+ if (!document) {
36
+ return { documentId, published: false };
37
+ }
38
+ await publishDocument(document._id, client);
39
+ publishedDocumentIds.push(documentId);
40
+ return { documentId, published: true };
41
+ },
42
+ {
43
+ onItemFailure: (documentId, error) => {
44
+ console.error(`Failed to publish document ${documentId}:`, error);
45
+ },
46
+ }
47
+ );
48
+ return publishedDocumentIds;
49
+ }
@@ -0,0 +1,146 @@
1
+ import { SanityClient, SanityDocument } from 'sanity';
2
+ import { pluginConfig } from '../adapter/core';
3
+
4
+ interface Reference {
5
+ _type: 'reference';
6
+ _ref: string;
7
+ }
8
+
9
+ interface TranslationMetadata {
10
+ _id: string;
11
+ _type: 'translation.metadata';
12
+ translations: {
13
+ _key: string;
14
+ value: Reference;
15
+ }[];
16
+ }
17
+
18
+ /**
19
+ * Function that:
20
+ * 1. Finds all references in the document
21
+ * 2. Fetches the referenced documents, looks for the translation.metadata file for each reference
22
+ * 3. Updates the document reference with the other translated document reference
23
+ * 4. Returns the document with the updated references
24
+ */
25
+ export async function resolveRefs(
26
+ doc: SanityDocument,
27
+ locale: string,
28
+ client: SanityClient
29
+ ) {
30
+ const references = findReferences(doc);
31
+
32
+ if (references.length === 0) {
33
+ return doc;
34
+ }
35
+
36
+ const translatedRefs = await resolveTranslatedReferences(
37
+ references,
38
+ locale,
39
+ client
40
+ );
41
+ return updateDocumentReferences(doc, translatedRefs);
42
+ }
43
+
44
+ /**
45
+ * Recursively finds all references in a document or object
46
+ */
47
+ function findReferences(
48
+ obj: any,
49
+ path: string[] = []
50
+ ): { ref: Reference; path: string[] }[] {
51
+ if (!obj || typeof obj !== 'object') {
52
+ return [];
53
+ }
54
+
55
+ const references: { ref: Reference; path: string[] }[] = [];
56
+
57
+ if (obj._ref) {
58
+ references.push({ ref: obj as Reference, path });
59
+ }
60
+
61
+ if (Array.isArray(obj)) {
62
+ obj.forEach((item, index) => {
63
+ references.push(...findReferences(item, [...path, index.toString()]));
64
+ });
65
+ } else {
66
+ Object.keys(obj).forEach((key) => {
67
+ references.push(...findReferences(obj[key], [...path, key]));
68
+ });
69
+ }
70
+
71
+ return references;
72
+ }
73
+
74
+ /**
75
+ * Fetches translation metadata and resolves translated references
76
+ */
77
+ async function resolveTranslatedReferences(
78
+ references: { ref: Reference; path: string[] }[],
79
+ locale: string,
80
+ client: SanityClient
81
+ ): Promise<Map<string, string>> {
82
+ const refIds = references.map((r) => r.ref._ref);
83
+ const translatedRefs = new Map<string, string>();
84
+
85
+ if (refIds.length === 0) {
86
+ return translatedRefs;
87
+ }
88
+
89
+ const sourceLocale = pluginConfig.getSourceLocale();
90
+
91
+ // Optimized GROQ query that directly returns only the needed translation pairs
92
+ const query = `*[_type == "translation.metadata" && count(translations[_key == $sourceLocale && value._ref in $refIds]) > 0] {
93
+ "originalRef": translations[_key == $sourceLocale][0].value._ref,
94
+ "translatedRef": translations[_key == $locale][0].value._ref
95
+ }[defined(originalRef) && defined(translatedRef)]`;
96
+
97
+ const translationPairs: { originalRef: string; translatedRef: string }[] =
98
+ await client.fetch(query, { refIds, sourceLocale, locale });
99
+
100
+ // Build the translation map
101
+ for (const { originalRef, translatedRef } of translationPairs) {
102
+ translatedRefs.set(originalRef, translatedRef);
103
+ }
104
+
105
+ return translatedRefs;
106
+ }
107
+
108
+ /**
109
+ * Updates document references with their translated versions
110
+ */
111
+ function updateDocumentReferences(
112
+ doc: SanityDocument,
113
+ translatedRefs: Map<string, string>
114
+ ): SanityDocument {
115
+ return updateReferencesRecursive(doc, translatedRefs);
116
+ }
117
+
118
+ /**
119
+ * Recursively updates references in an object
120
+ */
121
+ function updateReferencesRecursive(
122
+ obj: any,
123
+ translatedRefs: Map<string, string>
124
+ ): any {
125
+ if (!obj || typeof obj !== 'object') {
126
+ return obj;
127
+ }
128
+
129
+ if (obj._ref && translatedRefs.has(obj._ref)) {
130
+ return {
131
+ ...obj,
132
+ _ref: translatedRefs.get(obj._ref),
133
+ };
134
+ }
135
+
136
+ if (Array.isArray(obj)) {
137
+ return obj.map((item) => updateReferencesRecursive(item, translatedRefs));
138
+ }
139
+
140
+ const updated: any = {};
141
+ Object.keys(obj).forEach((key) => {
142
+ updated[key] = updateReferencesRecursive(obj[key], translatedRefs);
143
+ });
144
+
145
+ return updated;
146
+ }
@@ -0,0 +1,138 @@
1
+ // Adapted from https://github.com/sanity-io/sanity-naive-html-serializer
2
+
3
+ import { Merger } from './types';
4
+ import { SanityDocument } from 'sanity';
5
+ import { extractWithPath, arrayToJSONMatchPath } from '@sanity/mutator';
6
+
7
+ const reconcileArray = (origArray: any[], translatedArray: any[]): any[] => {
8
+ //arrays of strings don't have keys, so just replace the array and return
9
+ if (translatedArray && translatedArray.some((el) => typeof el === 'string')) {
10
+ return translatedArray;
11
+ }
12
+
13
+ //deep copy needed for field level patching
14
+ const combined = JSON.parse(JSON.stringify(origArray));
15
+
16
+ translatedArray.forEach((block) => {
17
+ if (!block._key) {
18
+ return;
19
+ }
20
+ const foundBlockIdx = origArray.findIndex(
21
+ (origBlock) => origBlock._key === block._key
22
+ );
23
+ if (foundBlockIdx < 0) {
24
+ //eslint-disable-next-line no-console
25
+ console.warn(
26
+ `This block no longer exists on the original document. Was it removed? ${JSON.stringify(
27
+ block
28
+ )}`
29
+ );
30
+ } else if (
31
+ origArray[foundBlockIdx]._type === 'block' ||
32
+ origArray[foundBlockIdx]._type === 'span'
33
+ ) {
34
+ combined[foundBlockIdx] = block;
35
+ } else if (Array.isArray(origArray[foundBlockIdx])) {
36
+ combined[foundBlockIdx] = reconcileArray(origArray[foundBlockIdx], block);
37
+ } else {
38
+ combined[foundBlockIdx] = reconcileObject(
39
+ origArray[foundBlockIdx],
40
+ block
41
+ );
42
+ }
43
+ });
44
+ return combined;
45
+ };
46
+
47
+ const reconcileObject = (
48
+ origObject: Record<string, any>,
49
+ translatedObject: Record<string, any>
50
+ ): Record<string, any> => {
51
+ if (
52
+ typeof translatedObject !== 'object' ||
53
+ !Object.keys(translatedObject).length
54
+ ) {
55
+ return origObject;
56
+ }
57
+
58
+ const updatedObj = JSON.parse(JSON.stringify(origObject));
59
+ Object.entries(translatedObject).forEach(([key, value]) => {
60
+ if (!value || key[0] === '_') {
61
+ return;
62
+ }
63
+ if (typeof value === 'string') {
64
+ updatedObj[key] = value;
65
+ } else if (Array.isArray(value)) {
66
+ updatedObj[key] = reconcileArray(origObject[key] ?? [], value);
67
+ } else {
68
+ updatedObj[key] = reconcileObject(origObject[key] ?? {}, value);
69
+ }
70
+ });
71
+ return updatedObj;
72
+ };
73
+
74
+ const fieldLevelMerge = (
75
+ translatedFields: Record<string, any>,
76
+ //should be fetched according to the revision and id of the translated obj above
77
+ baseDoc: SanityDocument,
78
+ localeId: string,
79
+ baseLang: string = 'en'
80
+ ): Record<string, any> => {
81
+ const merged: Record<string, any> = {};
82
+ const metaKeys = ['_rev', '_id', '_type'];
83
+ metaKeys.forEach((metaKey) => {
84
+ if (translatedFields[metaKey]) {
85
+ merged[metaKey] = translatedFields[metaKey];
86
+ }
87
+ });
88
+
89
+ //get any field that matches the base language, because it's been translated
90
+ const originPaths = extractWithPath(`..${baseLang}`, translatedFields);
91
+ originPaths.forEach((match) => {
92
+ const origVal = extractWithPath(
93
+ arrayToJSONMatchPath(match.path),
94
+ baseDoc
95
+ )[0].value;
96
+ const translatedVal = extractWithPath(
97
+ arrayToJSONMatchPath(match.path),
98
+ translatedFields
99
+ )[0].value;
100
+ let valToPatch;
101
+ if (typeof translatedVal === 'string') {
102
+ valToPatch = translatedVal;
103
+ } else if (Array.isArray(translatedVal) && translatedVal.length) {
104
+ valToPatch = reconcileArray((origVal as Array<any>) ?? [], translatedVal);
105
+ } else if (
106
+ typeof translatedVal === 'object' &&
107
+ Object.keys(translatedVal as Record<string, any>).length
108
+ ) {
109
+ valToPatch = reconcileObject(
110
+ origVal ?? {},
111
+ translatedVal as Record<string, any>
112
+ );
113
+ }
114
+ const destinationPath = [
115
+ ...match.path.slice(0, match.path.length - 1), //cut off the "en"
116
+ localeId.replace('-', '_'), // replace it with our locale
117
+ ];
118
+
119
+ merged[arrayToJSONMatchPath(destinationPath)] = valToPatch;
120
+ });
121
+
122
+ return merged;
123
+ };
124
+
125
+ const documentLevelMerge = (
126
+ translatedFields: Record<string, any>,
127
+ //should be fetched according to the revision and id of the translated obj above
128
+ baseDoc: SanityDocument
129
+ ): Record<string, any> => {
130
+ return reconcileObject(baseDoc, translatedFields);
131
+ };
132
+
133
+ export const BaseDocumentMerger: Merger = {
134
+ fieldLevelMerge,
135
+ documentLevelMerge,
136
+ reconcileArray,
137
+ reconcileObject,
138
+ };
@@ -0,0 +1,220 @@
1
+ // Adapted from https://github.com/sanity-io/sanity-naive-html-serializer
2
+
3
+ import { PortableTextBlockStyle } from '@portabletext/types';
4
+
5
+ import {
6
+ PortableTextBlockComponent,
7
+ PortableTextListComponent,
8
+ PortableTextListItemComponent,
9
+ PortableTextMarkComponent,
10
+ PortableTextHtmlComponents,
11
+ } from '@portabletext/to-html';
12
+
13
+ import { htmlToBlocks } from '@portabletext/block-tools';
14
+ import { blockContentType } from './deserialize/helpers';
15
+ import { PortableTextObject, PortableTextTextBlock, TypedObject } from 'sanity';
16
+ import { attachGTData, detachGTData } from './data';
17
+
18
+ export const defaultStopTypes = [
19
+ 'reference',
20
+ 'date',
21
+ 'datetime',
22
+ 'file',
23
+ 'geopoint',
24
+ 'image',
25
+ 'number',
26
+ 'crop',
27
+ 'hotspot',
28
+ 'boolean',
29
+ 'url',
30
+ 'color',
31
+ 'code',
32
+ ];
33
+
34
+ export const defaultMarks: Record<string, PortableTextMarkComponent> = {};
35
+
36
+ export const defaultPortableTextBlockStyles: Record<
37
+ PortableTextBlockStyle,
38
+ PortableTextBlockComponent | undefined
39
+ > = {
40
+ normal: ({ value, children }) => `<p id="${value._key}">${children}</p>`,
41
+ blockquote: ({ value, children }) =>
42
+ `<blockquote id="${value._key}">${children}</blockquote>`,
43
+ h1: ({ value, children }) => `<h1 id="${value._key}">${children}</h1>`,
44
+ h2: ({ value, children }) => `<h2 id="${value._key}">${children}</h2>`,
45
+ h3: ({ value, children }) => `<h3 id="${value._key}">${children}</h3>`,
46
+ h4: ({ value, children }) => `<h4 id="${value._key}">${children}</h4>`,
47
+ h5: ({ value, children }) => `<h5 id="${value._key}">${children}</h5>`,
48
+ h6: ({ value, children }) => `<h6 id="${value._key}">${children}</h6>`,
49
+ };
50
+
51
+ const defaultLists: Record<'number' | 'bullet', PortableTextListComponent> = {
52
+ number: ({ value, children }) =>
53
+ `<ol id="${value._key.replace('-parent', '')}">${children}</ol>`,
54
+ bullet: ({ value, children }) =>
55
+ `<ul id="${value._key.replace('-parent', '')}">${children}</ul>`,
56
+ };
57
+
58
+ const defaultListItem: PortableTextListItemComponent = ({
59
+ value,
60
+ children,
61
+ }) => {
62
+ const { _key, level } = value;
63
+ return `<li id="${(_key || '').replace('-parent', '')}" data-level="${level}">${children}</li>`;
64
+ };
65
+
66
+ const unknownBlockFunc: PortableTextBlockComponent = ({ value, children }) =>
67
+ `<p id="${value._key}" data-type="unknown-block-style" data-style="${value.style}">${children}</p>`;
68
+
69
+ export const customSerializers: Partial<PortableTextHtmlComponents> = {
70
+ unknownType: ({ value }: { value: Record<string, any> }) =>
71
+ `<div class="${value._type}"></div>`,
72
+ types: {},
73
+ marks: defaultMarks,
74
+ block: defaultPortableTextBlockStyles,
75
+ list: defaultLists,
76
+ listItem: defaultListItem,
77
+ unknownBlockStyle: unknownBlockFunc,
78
+ };
79
+
80
+ export const customDeserializers: Record<string, any> = { types: {} };
81
+
82
+ export const customBlockDeserializers: Array<any> = [
83
+ // handle marks with data-gt-internal
84
+ {
85
+ deserialize(
86
+ el: HTMLParagraphElement,
87
+ next: (
88
+ elements: Node | Node[] | NodeList
89
+ ) => TypedObject | TypedObject[] | undefined
90
+ ): PortableTextTextBlock | TypedObject | undefined {
91
+ if (!el.hasChildNodes()) {
92
+ return undefined;
93
+ }
94
+
95
+ if (!el.getAttribute('data-gt-internal')) {
96
+ return undefined;
97
+ }
98
+
99
+ const { html, data } = detachGTData(el.outerHTML);
100
+ const block = htmlToBlocks(html, blockContentType)[0];
101
+
102
+ const children = next(el.childNodes);
103
+
104
+ let markDefs: PortableTextObject[] = [];
105
+ if ('markDefs' in block) {
106
+ markDefs = (block.markDefs as PortableTextObject[]) ?? [];
107
+ }
108
+ if (data?.markDef) {
109
+ markDefs.push(data.markDef as PortableTextObject);
110
+ }
111
+ if (children) {
112
+ (children as any).forEach((child: any) => {
113
+ child.marks = data?.markDef?._key
114
+ ? [...(child.marks || []), data.markDef._key]
115
+ : [...(child.marks || [])];
116
+ });
117
+ }
118
+ // Resolve marks in the child nodes
119
+ const output = {
120
+ ...block,
121
+ markDefs,
122
+ children,
123
+ };
124
+ return output;
125
+ },
126
+ },
127
+ //handle undeclared styles
128
+ {
129
+ deserialize(
130
+ el: HTMLParagraphElement,
131
+ next: (
132
+ elements: Node | Node[] | NodeList
133
+ ) => TypedObject | TypedObject[] | undefined
134
+ ): PortableTextTextBlock | TypedObject | undefined {
135
+ if (!el.hasChildNodes()) {
136
+ return undefined;
137
+ }
138
+
139
+ if (el.getAttribute('data-type') !== 'unknown-block-style') {
140
+ return undefined;
141
+ }
142
+
143
+ const style = el.getAttribute('data-style') ?? '';
144
+ const block = htmlToBlocks(el.outerHTML, blockContentType)[0];
145
+
146
+ return {
147
+ ...block,
148
+ style,
149
+ children: next(el.childNodes),
150
+ };
151
+ },
152
+ },
153
+ //handle list items
154
+ {
155
+ deserialize(
156
+ el: HTMLParagraphElement,
157
+ next: (
158
+ elements: Node | Node[] | NodeList
159
+ ) => TypedObject | TypedObject[] | undefined
160
+ ): PortableTextTextBlock | TypedObject | undefined {
161
+ if (!el.hasChildNodes()) {
162
+ return undefined;
163
+ }
164
+
165
+ if (el.tagName.toLowerCase() !== 'li') {
166
+ return undefined;
167
+ }
168
+
169
+ const tagsToStyle: Record<string, string> = {
170
+ ul: 'bullet',
171
+ ol: 'number',
172
+ };
173
+
174
+ const parent = el.parentNode as HTMLUListElement | HTMLOListElement;
175
+ if (!parent || !parent.tagName) {
176
+ return undefined;
177
+ }
178
+
179
+ const listItem = tagsToStyle[parent.tagName.toLowerCase()];
180
+ if (!listItem) {
181
+ return undefined;
182
+ }
183
+
184
+ const level =
185
+ el.getAttribute('data-level') &&
186
+ parseInt(el.getAttribute('data-level') || '0', 10);
187
+ const _key = el.id;
188
+ let block = htmlToBlocks(parent.outerHTML, blockContentType)[0];
189
+ const customStyle = el.children?.[0]?.getAttribute('data-style');
190
+
191
+ //check if the object inside is also serialized -- that means it has a style
192
+ //or custom annotation and we should use childNode serialization
193
+ const regex = new RegExp(/<("[^"]*"|'[^']*'|[^'">])*>/);
194
+ if (regex.test(el.innerHTML)) {
195
+ const newBlock = htmlToBlocks(el.innerHTML, blockContentType)[0];
196
+ if (newBlock) {
197
+ block = {
198
+ ...block,
199
+ ...newBlock,
200
+ // @ts-ignore
201
+ style: customStyle ?? (newBlock as PortableTextTextBlock).style,
202
+ };
203
+
204
+ //next(childNodes) plays poorly with custom styles, issue to be filed.
205
+ if (customStyle) {
206
+ return block as PortableTextTextBlock;
207
+ }
208
+ }
209
+ }
210
+
211
+ return {
212
+ ...block,
213
+ level,
214
+ _key,
215
+ listItem,
216
+ children: next(el.childNodes),
217
+ };
218
+ },
219
+ },
220
+ ];