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,171 @@
1
+ // Adapted from https://github.com/sanity-io/sanity-naive-html-serializer
2
+ import { htmlToBlocks } from '@portabletext/block-tools';
3
+ import {
4
+ customDeserializers,
5
+ customBlockDeserializers,
6
+ } from '../BaseSerializationConfig';
7
+ import { Deserializer } from '../types';
8
+ import { blockContentType, preprocess } from './helpers';
9
+ import { mergeBlocks } from '../helpers';
10
+
11
+ export const deserializeArray = (
12
+ arrayHTML: Element,
13
+ deserializers: Record<string, any> = customDeserializers,
14
+ blockDeserializers = customBlockDeserializers
15
+ ) => {
16
+ const output: any[] = [];
17
+ const children = Array.from(arrayHTML.children);
18
+ children.forEach((child) => {
19
+ let deserializedObject: any;
20
+ try {
21
+ if (child.tagName?.toLowerCase() === 'span') {
22
+ deserializedObject = preprocess(child.innerHTML);
23
+ }
24
+ //has specific class name or data type, so it's an obj
25
+ else if (
26
+ child.className ||
27
+ child.getAttribute('data-type') === 'object'
28
+ ) {
29
+ deserializedObject = deserializeObject(
30
+ child,
31
+ deserializers,
32
+ blockDeserializers
33
+ );
34
+ deserializedObject._key = child.id;
35
+ } else {
36
+ deserializedObject = htmlToBlocks(child.outerHTML, blockContentType, {
37
+ rules: blockDeserializers,
38
+ });
39
+ deserializedObject = mergeBlocks(deserializedObject);
40
+ deserializedObject._key = child.id;
41
+ }
42
+ } catch (e) {
43
+ //eslint-disable-next-line no-console
44
+ console.debug(
45
+ `Tried to deserialize block: ${child.outerHTML} in an array but failed to identify it! Error: ${e}`
46
+ );
47
+ }
48
+ output.push(deserializedObject);
49
+ });
50
+ return output;
51
+ };
52
+
53
+ export const deserializeObject = (
54
+ objectHTML: Element,
55
+ deserializers: Record<string, any> = customDeserializers,
56
+ blockDeserializers = customBlockDeserializers
57
+ ) => {
58
+ const deserialize = deserializers.types[objectHTML.className];
59
+ if (deserialize) {
60
+ return deserialize(objectHTML);
61
+ }
62
+
63
+ const output: Record<string, any> = {};
64
+ //account for anonymous inline objects
65
+ if (objectHTML.className) {
66
+ output._type = objectHTML.className;
67
+ }
68
+ const children = Array.from(objectHTML.children);
69
+
70
+ children.forEach((child) => {
71
+ //string field
72
+ if (child.tagName?.toLowerCase() === 'span') {
73
+ output[child.className] = preprocess(child.innerHTML);
74
+ }
75
+ //richer field, either object or array
76
+ else if (child.getAttribute('data-level') === 'field') {
77
+ const deserialized = deserializeHTML(
78
+ child.outerHTML,
79
+ deserializers,
80
+ blockDeserializers
81
+ );
82
+ if (deserialized && Object.keys(deserialized).length) {
83
+ output[child.className] = deserialized;
84
+ } else {
85
+ //eslint-disable-next-line no-console
86
+ console.debug(
87
+ `Deserializer: Skipping empty or unreadable HTML: ${child.outerHTML}`
88
+ );
89
+ }
90
+ } else if (child.getAttribute('data-type') === 'array') {
91
+ output[child.className] = deserializeArray(
92
+ child,
93
+ deserializers,
94
+ blockDeserializers
95
+ );
96
+ }
97
+ });
98
+ return output;
99
+ };
100
+
101
+ export const deserializeHTML = (
102
+ html: string,
103
+ deserializers: Record<string, any>,
104
+ blockDeserializers: Array<any>
105
+ ): Record<string, any> | any[] => {
106
+ //parent node is always div with classname of field -- get its child
107
+ let HTMLnode = new DOMParser().parseFromString(html, 'text/html').body
108
+ .children[0];
109
+
110
+ //catch embedded object as a field
111
+ if (HTMLnode?.getAttribute('data-level') === 'field') {
112
+ HTMLnode = HTMLnode.children[0];
113
+ }
114
+
115
+ if (!HTMLnode) {
116
+ return {};
117
+ }
118
+
119
+ let output: Record<string, any> | any[];
120
+
121
+ //prioritize custom deserialization
122
+ const deserialize = deserializers.types[HTMLnode.className];
123
+ if (deserialize) {
124
+ output = deserialize(HTMLnode);
125
+ } else if (HTMLnode.getAttribute('data-type') === 'object') {
126
+ output = deserializeObject(HTMLnode, deserializers, blockDeserializers);
127
+ } else if (HTMLnode.getAttribute('data-type') === 'array') {
128
+ output = deserializeArray(HTMLnode, deserializers, blockDeserializers);
129
+ } else {
130
+ output = {};
131
+ //eslint-disable-next-line no-console
132
+ console.debug(
133
+ `Tried to deserialize block ${HTMLnode.outerHTML} but failed to identify it!`
134
+ );
135
+ }
136
+
137
+ return output;
138
+ };
139
+
140
+ export const deserializeDocument = (
141
+ serializedDoc: string,
142
+ deserializers: Record<string, any> = customDeserializers,
143
+ blockDeserializers = customBlockDeserializers
144
+ ): Record<string, any> => {
145
+ const metadata: Record<string, any> = {};
146
+ const head = new DOMParser().parseFromString(serializedDoc, 'text/html').head;
147
+
148
+ Array.from(head.children).forEach((metaTag) => {
149
+ const validTags = ['_id', '_rev', '_type'];
150
+ const metaName = metaTag.getAttribute('name');
151
+ if (metaName && validTags.includes(metaName)) {
152
+ metadata[metaName] = metaTag.getAttribute('content');
153
+ }
154
+ });
155
+
156
+ const content: Record<string, any> = deserializeHTML(
157
+ serializedDoc,
158
+ deserializers,
159
+ blockDeserializers
160
+ );
161
+
162
+ return {
163
+ ...content,
164
+ ...metadata,
165
+ };
166
+ };
167
+
168
+ export const BaseDocumentDeserializer: Deserializer = {
169
+ deserializeDocument,
170
+ deserializeHTML,
171
+ };
@@ -0,0 +1,42 @@
1
+ // Adapted from https://github.com/sanity-io/sanity-naive-html-serializer
2
+ import { htmlToBlocks } from '@portabletext/block-tools';
3
+ import { Schema } from '@sanity/schema';
4
+ import { ObjectField, PortableTextSpan, PortableTextTextBlock } from 'sanity';
5
+
6
+ const defaultSchema = Schema.compile({
7
+ name: 'default',
8
+ types: [
9
+ {
10
+ type: 'object',
11
+ name: 'default',
12
+ fields: [
13
+ {
14
+ name: 'block',
15
+ type: 'array',
16
+ of: [{ type: 'block' }],
17
+ },
18
+ ],
19
+ },
20
+ ],
21
+ });
22
+
23
+ export const blockContentType = defaultSchema
24
+ .get('default')
25
+ .fields.find((field: ObjectField) => field.name === 'block').type;
26
+
27
+ export const noSchemaWarning = (obj: Element): string =>
28
+ `WARNING: Unfortunately the deserializer may have issues with this field or object: ${obj.className}.
29
+ If it's a specific type, you may need to declare at the top level, or write a custom deserializer.`;
30
+
31
+ //helper to handle messy input -- take advantage
32
+ //of blockTools' sanitizing behavior for single strings
33
+ export const preprocess = (html: string): string => {
34
+ const intermediateBlocks = htmlToBlocks(
35
+ `<p>${html}</p>`,
36
+ blockContentType
37
+ ) as PortableTextTextBlock<PortableTextSpan>[];
38
+ if (!intermediateBlocks.length) {
39
+ throw new Error(`Error parsing string '${html}'`);
40
+ }
41
+ return intermediateBlocks[0].children[0].text;
42
+ };
@@ -0,0 +1,18 @@
1
+ import { PortableTextTextBlock } from 'sanity';
2
+
3
+ // Helper function to merge multiple blocks
4
+ // Prioritize blocks[0]
5
+ export function mergeBlocks(blocks: PortableTextTextBlock[]) {
6
+ const mergedBlock = { ...blocks[0] };
7
+ mergedBlock.markDefs = mergedBlock.markDefs ?? [];
8
+ for (const [idx, block] of blocks.entries()) {
9
+ if (idx === 0) {
10
+ continue;
11
+ }
12
+ mergedBlock.children.push(...block.children);
13
+ mergedBlock.markDefs.push(...(block.markDefs ?? []));
14
+ }
15
+ mergedBlock._type = 'block';
16
+
17
+ return mergedBlock;
18
+ }
@@ -0,0 +1,11 @@
1
+ export { BaseDocumentMerger } from './BaseDocumentMerger';
2
+ export { BaseDocumentSerializer } from './serialize';
3
+ export { BaseDocumentDeserializer } from './deserialize/BaseDocumentDeserializer';
4
+ export {
5
+ defaultStopTypes,
6
+ customSerializers,
7
+ customBlockDeserializers,
8
+ } from './BaseSerializationConfig';
9
+
10
+ export type { SerializedDocument, Deserializer, Merger } from './types';
11
+ export { attachGTData, detachGTData } from './data';
@@ -0,0 +1,124 @@
1
+ // Adapted from https://github.com/sanity-io/sanity-naive-html-serializer
2
+
3
+ import { ObjectField, TypedObject } from 'sanity';
4
+
5
+ const META_FIELDS = ['_key', '_type', '_id'];
6
+
7
+ /*
8
+ * Helper. If field-level translation pattern used, only sends over
9
+ * content from the base language. Works recursively, so if users
10
+ * use this pattern several layers deep, base language fields will still be found.
11
+ */
12
+ export const languageObjectFieldFilter = (
13
+ obj: Record<string, any>,
14
+ baseLang: string
15
+ ): Record<string, any> => {
16
+ const filterToLangField = (childObj: Record<string, any>) => {
17
+ const filteredObj: Record<string, any> = {};
18
+ filteredObj[baseLang] = childObj[baseLang];
19
+ META_FIELDS.forEach((field) => {
20
+ if (childObj[field]) {
21
+ filteredObj[field] = childObj[field];
22
+ }
23
+ });
24
+ return filteredObj;
25
+ };
26
+
27
+ const findBaseLang = (childObj: Record<string, any>): Record<string, any> => {
28
+ const filteredObj: Record<string, any> = {};
29
+ META_FIELDS.forEach((field) => {
30
+ if (childObj[field]) {
31
+ filteredObj[field] = childObj[field];
32
+ }
33
+ });
34
+
35
+ for (const key in childObj) {
36
+ if (childObj.hasOwnProperty(key)) {
37
+ const value: any = childObj[key];
38
+ //we've reached a base language field, add it to
39
+ //what we want to send to translation
40
+ if (value.hasOwnProperty(baseLang)) {
41
+ filteredObj[key] = filterToLangField(value);
42
+ }
43
+ //we have an array that may have language fields in its objects
44
+ else if (
45
+ Array.isArray(value) &&
46
+ value.length &&
47
+ typeof value[0] === 'object'
48
+ ) {
49
+ //recursively find and filter for any objects that have the base language
50
+ const validLangObjects = value.reduce((validArr, objInArray) => {
51
+ if (objInArray._type === 'block') {
52
+ validArr.push(objInArray);
53
+ } else if (objInArray.hasOwnProperty(baseLang)) {
54
+ validArr.push(filterToLangField(objInArray));
55
+ } else {
56
+ const filtered = findBaseLang(objInArray);
57
+ const nonMetaFields = Object.keys(filtered).filter(
58
+ (objInArrayKey) => META_FIELDS.indexOf(objInArrayKey) === -1
59
+ );
60
+ if (nonMetaFields.length) {
61
+ validArr.push(filtered);
62
+ }
63
+ }
64
+ return validArr;
65
+ }, []);
66
+ if (validLangObjects.length) {
67
+ filteredObj[key] = validLangObjects;
68
+ }
69
+ }
70
+ //we have an object nested in an object
71
+ //recurse down the tree
72
+ else if (typeof value === 'object') {
73
+ const nestedLangObj = findBaseLang(value);
74
+ const nonMetaFields = Object.keys(nestedLangObj).filter(
75
+ (nestedObjKey) => META_FIELDS.indexOf(nestedObjKey) === -1
76
+ );
77
+ if (nonMetaFields.length) {
78
+ filteredObj[key] = nestedLangObj;
79
+ }
80
+ }
81
+ }
82
+ }
83
+ return filteredObj;
84
+ };
85
+
86
+ //send top level object into recursive function
87
+ return findBaseLang(obj);
88
+ };
89
+
90
+ /*
91
+ * Eliminates stop-types and non-localizable fields
92
+ * for document-level translation.
93
+ */
94
+ export const fieldFilter = (
95
+ obj: Record<string, any>,
96
+ objFields: ObjectField[],
97
+ stopTypes: string[]
98
+ ): TypedObject => {
99
+ const filteredObj: TypedObject = { _type: obj._type };
100
+
101
+ const fieldFilterFunc = (field: Record<string, any>) => {
102
+ if (field.localize === false) {
103
+ return false;
104
+ } else if (field.type === 'string' || field.type === 'text') {
105
+ return true;
106
+ } else if (Array.isArray(obj[field.name])) {
107
+ return true;
108
+ } else if (!stopTypes.includes(field.type)) {
109
+ return true;
110
+ }
111
+ return false;
112
+ };
113
+
114
+ const validFields = [
115
+ ...META_FIELDS,
116
+ ...objFields?.filter(fieldFilterFunc)?.map((field) => field.name),
117
+ ];
118
+ validFields.forEach((field) => {
119
+ if (obj[field]) {
120
+ filteredObj[field] = obj[field];
121
+ }
122
+ });
123
+ return filteredObj;
124
+ };
@@ -0,0 +1,284 @@
1
+ // Adapted from https://github.com/sanity-io/sanity-naive-html-serializer
2
+
3
+ import {
4
+ defaultStopTypes,
5
+ customSerializers,
6
+ } from '../BaseSerializationConfig';
7
+ import { SanityDocument, TypedObject, Schema } from 'sanity';
8
+ import { TranslationLevel } from '../types';
9
+ import { fieldFilter, languageObjectFieldFilter } from './fieldFilters';
10
+ import { PortableTextTypeComponent, toHTML } from '@portabletext/to-html';
11
+
12
+ const META_FIELDS = ['_key', '_type', '_id', '_weak'];
13
+
14
+ export const BaseDocumentSerializer = (schemas: Schema) => {
15
+ /*
16
+ * Helper function that allows us to get metadata (like `localize: false`) from schema fields.
17
+ */
18
+ const getSchema = (name: string) =>
19
+ schemas?._original?.types.find((s) => s.name === name) as any;
20
+
21
+ const serializeObject = (
22
+ obj: TypedObject,
23
+ stopTypes: string[],
24
+ serializers: Record<string, any>
25
+ ) => {
26
+ if (stopTypes.includes(obj._type)) {
27
+ return '';
28
+ }
29
+
30
+ //if user has declared a custom serializer, use that
31
+ //instead of this method
32
+ const hasSerializer =
33
+ serializers.types && Object.keys(serializers.types).includes(obj._type);
34
+ if (hasSerializer) {
35
+ return toHTML([obj], { components: serializers });
36
+ }
37
+
38
+ //we don't need to worry about PT types
39
+ if (obj._type === 'span' || obj._type === 'block') {
40
+ return toHTML(obj, { components: serializers });
41
+ }
42
+
43
+ //if schema is available, encode values in the order they're declared in the schema,
44
+ //since this will likely be more intuitive for a translator.
45
+ let fieldNames = Object.keys(obj).filter((key) => key !== '_type');
46
+ const schema = getSchema(obj._type);
47
+ if (schema && schema.fields) {
48
+ fieldNames = schema.fields
49
+ .map((field: Record<string, any>) => field.name)
50
+ .filter((schemaKey: string) => Object.keys(obj).includes(schemaKey));
51
+ }
52
+
53
+ //account for anonymous inline objects
54
+ if (typeof obj === 'object' && !obj._type) {
55
+ obj._type = '';
56
+ }
57
+
58
+ //in some cases, we might recurse through many objects of the same type
59
+ //we should take all methods necessary to ensure state does not persist
60
+ //otherwise we risk using old serialization methods on new items
61
+ const newSerializationMethods: Record<string, PortableTextTypeComponent> =
62
+ {};
63
+ const tempType = `${obj._type}__temp_type__${Math.random().toString(36).substring(7)}`;
64
+ const objToSerialize: TypedObject = { _type: tempType };
65
+ //for our default serialization method, we only need to
66
+ //capture metadata. the rest will be recursively turned into strings.
67
+ META_FIELDS.filter((f) => f !== '_type').forEach((field) => {
68
+ objToSerialize[field] = obj[field];
69
+ });
70
+
71
+ let innerHTML = '';
72
+
73
+ //if it's a custom object, iterate through its keys to find and serialize translatable content
74
+ fieldNames.forEach((fieldName) => {
75
+ let htmlField = '';
76
+
77
+ if (!META_FIELDS.includes(fieldName)) {
78
+ const value = obj[fieldName];
79
+ //strings are either string fields or have recursively been turned
80
+ //into HTML because they were a nested object or array
81
+ if (typeof value === 'string') {
82
+ const htmlRegex = new RegExp(/<("[^"]*"|'[^']*'|[^'">])*>/);
83
+ if (htmlRegex.test(value)) {
84
+ htmlField = value;
85
+ } else {
86
+ htmlField = `<span class="${fieldName}">${value}</span>`;
87
+ }
88
+ }
89
+
90
+ //array fields get filtered and its children serialized
91
+ else if (Array.isArray(value)) {
92
+ htmlField = serializeArray(value, fieldName, stopTypes, {
93
+ ...serializers,
94
+ types: { ...serializers.types },
95
+ });
96
+ }
97
+
98
+ //this is an object in an object, serialize it first
99
+ else {
100
+ const embeddedObject = value as TypedObject;
101
+ const embeddedObjectSchema = getSchema(embeddedObject._type);
102
+ let toTranslate = embeddedObject;
103
+ if (embeddedObjectSchema && embeddedObjectSchema.fields) {
104
+ toTranslate = fieldFilter(
105
+ toTranslate,
106
+ embeddedObjectSchema.fields,
107
+ stopTypes
108
+ );
109
+ }
110
+ const objHTML = serializeObject(toTranslate, stopTypes, {
111
+ ...serializers,
112
+ types: { ...serializers.types },
113
+ });
114
+ htmlField = `<div class="${fieldName}" data-level="field">${objHTML}</div>`;
115
+ }
116
+
117
+ innerHTML += htmlField;
118
+ }
119
+ });
120
+
121
+ if (!innerHTML) {
122
+ return '';
123
+ }
124
+
125
+ newSerializationMethods[tempType] = ({ value }: { value: TypedObject }) => {
126
+ let div = `<div class="${value._type.split('__temp_type__')[0]}"`;
127
+ if (value._key || value._id) {
128
+ div += `id="${value._key ?? value._id}"`;
129
+ }
130
+
131
+ return [div, ` data-type="object">${innerHTML}</div>`].join('');
132
+ };
133
+
134
+ let serializedBlock = '';
135
+ try {
136
+ serializedBlock = toHTML(objToSerialize, {
137
+ components: {
138
+ ...serializers,
139
+ types: {
140
+ ...serializers.types,
141
+ ...newSerializationMethods,
142
+ },
143
+ },
144
+ });
145
+ } catch (err) {
146
+ //eslint-disable-next-line no-console -- this is a warning
147
+ console.warn(
148
+ `Had issues serializing block of type "${obj._type}". Please specify a serialization method for this block in your serialization config. Received error: ${err}`
149
+ );
150
+ }
151
+
152
+ return serializedBlock;
153
+ };
154
+
155
+ const serializeArray = (
156
+ fieldContent: Record<string, any>[],
157
+ fieldName: string,
158
+ stopTypes: string[],
159
+ serializers: Record<string, any>
160
+ ) => {
161
+ //filter for any blocks that user has indicated
162
+ //should not be sent for translation
163
+ const validBlocks = fieldContent.filter(
164
+ (block) => !stopTypes.includes(block._type)
165
+ );
166
+
167
+ //take out any fields in these blocks that should
168
+ //not be sent to translation
169
+ const filteredBlocks = validBlocks.map((block) => {
170
+ const schema = getSchema(block._type);
171
+ if (schema && schema.fields) {
172
+ return fieldFilter(block, schema.fields, stopTypes);
173
+ }
174
+ return block;
175
+ });
176
+
177
+ const output = filteredBlocks.map((obj) => {
178
+ //if object in array is just a string, just return it
179
+ if (typeof obj === 'string') {
180
+ return `<span>${obj}</span>`;
181
+ }
182
+ //send to serialization method
183
+ return serializeObject(obj as TypedObject, stopTypes, serializers);
184
+ });
185
+
186
+ //encode this with data-level field
187
+ return `<div class="${fieldName}" data-type="array">${output.join('')}</div>`;
188
+ };
189
+
190
+ /*
191
+ * Main parent function: finds fields to translate, and feeds them to appropriate child serialization
192
+ * methods.
193
+ */
194
+ const serializeDocument = (
195
+ doc: SanityDocument,
196
+ translationLevel: TranslationLevel = 'document',
197
+ baseLang = 'en',
198
+ stopTypes = defaultStopTypes,
199
+ serializers = customSerializers
200
+ ) => {
201
+ const schema = getSchema(doc._type);
202
+ let filteredObj: Record<string, any> = {};
203
+
204
+ //field level translations explicitly send over any fields that
205
+ //match the base language, regardless of depth
206
+ if (translationLevel === 'field') {
207
+ filteredObj = languageObjectFieldFilter(doc, baseLang);
208
+ }
209
+ //otherwise, we can refer to the schema and a list of stop types
210
+ //to determine what should not be sent
211
+ else {
212
+ filteredObj = fieldFilter(doc, schema.fields, stopTypes);
213
+ }
214
+
215
+ const serializedFields: Record<string, any> = {};
216
+
217
+ for (const key in filteredObj) {
218
+ if (filteredObj.hasOwnProperty(key) === false) continue;
219
+ const value: Record<string, any> | Array<any> | string = filteredObj[key];
220
+
221
+ if (typeof value === 'string') {
222
+ serializedFields[key] = value;
223
+ } else if (Array.isArray(value)) {
224
+ serializedFields[key] = serializeArray(
225
+ value,
226
+ key,
227
+ stopTypes,
228
+ serializers
229
+ );
230
+ } else if (
231
+ value &&
232
+ !stopTypes.find((stopType) => stopType == value?._type)
233
+ ) {
234
+ const serialized = serializeObject(
235
+ value as TypedObject,
236
+ stopTypes,
237
+ serializers
238
+ );
239
+ serializedFields[key] =
240
+ `<div class="${key}" data-level='field'>${serialized}</div>`;
241
+ }
242
+ }
243
+
244
+ //create a valid HTML file
245
+ const rawHTMLBody = document.createElement('body');
246
+ rawHTMLBody.innerHTML = serializeObject(
247
+ serializedFields as TypedObject,
248
+ stopTypes,
249
+ serializers
250
+ );
251
+
252
+ const rawHTMLHead = document.createElement('head');
253
+ const metaFields = ['_id', '_type', '_rev'];
254
+ //save our metadata as meta tags so we can use them later on
255
+ metaFields.forEach((field) => {
256
+ const metaEl = document.createElement('meta');
257
+ metaEl.setAttribute('name', field);
258
+ metaEl.setAttribute('content', doc[field] as string);
259
+ rawHTMLHead.appendChild(metaEl);
260
+ });
261
+ //encode version so we can use the correct deserialization methods
262
+ const versionMeta = document.createElement('meta');
263
+ versionMeta.setAttribute('name', 'version');
264
+ versionMeta.setAttribute('content', '3');
265
+ rawHTMLHead.appendChild(versionMeta);
266
+
267
+ const rawHTML = document.createElement('html');
268
+ rawHTML.appendChild(rawHTMLHead);
269
+ rawHTML.appendChild(rawHTMLBody);
270
+
271
+ return {
272
+ name: doc._id,
273
+ content: rawHTML.outerHTML,
274
+ };
275
+ };
276
+
277
+ return {
278
+ serializeDocument,
279
+ fieldFilter,
280
+ languageObjectFieldFilter,
281
+ serializeArray,
282
+ serializeObject,
283
+ };
284
+ };
@@ -0,0 +1,41 @@
1
+ // Adapted from https://github.com/sanity-io/sanity-naive-html-serializer
2
+
3
+ import { ObjectField, SanityDocument, TypedObject, Schema } from 'sanity';
4
+
5
+ export type SerializedDocument = {
6
+ name: string;
7
+ content: string;
8
+ };
9
+
10
+ export type TranslationLevel = 'document' | 'field';
11
+
12
+ export interface Deserializer {
13
+ deserializeDocument: (
14
+ serializedDoc: string,
15
+ deserializers?: Record<string, any>,
16
+ blockDeserializers?: Array<any>
17
+ ) => Record<string, any>;
18
+ deserializeHTML: (
19
+ html: string,
20
+ deserializers: Record<string, any>,
21
+ blockDeserializers: Array<any>
22
+ ) => Record<string, any> | any[];
23
+ }
24
+
25
+ export interface Merger {
26
+ fieldLevelMerge: (
27
+ translatedFields: Record<string, any>,
28
+ baseDoc: SanityDocument,
29
+ localeId: string,
30
+ baseLang: string
31
+ ) => Record<string, any>;
32
+ documentLevelMerge: (
33
+ translatedFields: Record<string, any>,
34
+ baseDoc: SanityDocument
35
+ ) => Record<string, any>;
36
+ reconcileArray: (origArray: any[], translatedArray: any[]) => any[];
37
+ reconcileObject: (
38
+ origObject: Record<string, any>,
39
+ translatedObject: Record<string, any>
40
+ ) => Record<string, any>;
41
+ }