gt-sanity 0.0.6 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +1 -1
- package/dist/index.d.mts +95 -73
- package/dist/index.d.ts +95 -73
- package/dist/index.js +6233 -1162
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +6292 -1193
- package/dist/index.mjs.map +1 -1
- package/package.json +9 -4
- package/src/adapter/core.ts +41 -4
- package/src/adapter/getLocales.ts +2 -2
- package/src/components/TranslationsProvider.tsx +942 -0
- package/src/components/page/BatchProgress.tsx +27 -0
- package/src/components/page/ImportAllDialog.tsx +51 -0
- package/src/components/page/ImportMissingDialog.tsx +55 -0
- package/src/components/page/TranslateAllDialog.tsx +55 -0
- package/src/components/page/TranslationsTable.tsx +81 -0
- package/src/components/page/TranslationsTool.tsx +299 -837
- package/src/components/shared/BaseTranslationWrapper.tsx +82 -0
- package/src/components/shared/LocaleCheckbox.tsx +47 -0
- package/src/components/shared/SingleDocumentView.tsx +108 -0
- package/src/components/tab/TranslationView.tsx +379 -0
- package/src/components/tab/TranslationsTab.tsx +25 -0
- package/src/configuration/baseDocumentLevelConfig/documentLevelPatch.ts +6 -9
- package/src/configuration/baseDocumentLevelConfig/helpers/createI18nDocAndPatchMetadata.ts +5 -24
- package/src/configuration/baseDocumentLevelConfig/helpers/patchI18nDoc.ts +3 -23
- package/src/configuration/baseDocumentLevelConfig/index.ts +16 -68
- package/src/configuration/baseFieldLevelConfig.ts +15 -50
- package/src/index.ts +29 -43
- package/src/sanity-api/findDocuments.ts +44 -0
- package/src/sanity-api/publishDocuments.ts +49 -0
- package/src/sanity-api/resolveRefs.ts +146 -0
- package/src/serialization/BaseDocumentMerger.ts +138 -0
- package/src/serialization/BaseSerializationConfig.ts +220 -0
- package/src/serialization/__tests__/BaseDocumentDeserializer/__snapshots__/documentLevelDeserialization.test.ts.snap +189 -0
- package/src/serialization/__tests__/BaseDocumentDeserializer/__snapshots__/fieldLevelDeserialization.test.ts.snap +107 -0
- package/src/serialization/__tests__/BaseDocumentDeserializer/baseDeserialization.test.ts +397 -0
- package/src/serialization/__tests__/BaseDocumentDeserializer/documentLevelDeserialization.test.ts +107 -0
- package/src/serialization/__tests__/BaseDocumentDeserializer/fieldLevelDeserialization.test.ts +107 -0
- package/src/serialization/__tests__/BaseDocumentMerger/__snapshots__/documentLevelMerge.test.ts.snap +193 -0
- package/src/serialization/__tests__/BaseDocumentMerger/__snapshots__/fieldLevelMerge.test.ts.snap +97 -0
- package/src/serialization/__tests__/BaseDocumentMerger/baseMerge.test.ts +36 -0
- package/src/serialization/__tests__/BaseDocumentMerger/documentLevelMerge.test.ts +96 -0
- package/src/serialization/__tests__/BaseDocumentMerger/fieldLevelMerge.test.ts +142 -0
- package/src/serialization/__tests__/BaseDocumentMerger/utils.ts +52 -0
- package/src/serialization/__tests__/BaseDocumentSerializer/__snapshots__/documentInlineMarks.test.ts.snap +39 -0
- package/src/serialization/__tests__/BaseDocumentSerializer/__snapshots__/documentLevelSerialization.test.ts.snap +8 -0
- package/src/serialization/__tests__/BaseDocumentSerializer/__snapshots__/fieldLevelSerialization.test.ts.snap +8 -0
- package/src/serialization/__tests__/BaseDocumentSerializer/baseSerialization.test.ts +345 -0
- package/src/serialization/__tests__/BaseDocumentSerializer/documentInlineMarks.test.ts +53 -0
- package/src/serialization/__tests__/BaseDocumentSerializer/documentLevelSerialization.test.ts +120 -0
- package/src/serialization/__tests__/BaseDocumentSerializer/fieldLevelSerialization.test.ts +153 -0
- package/src/serialization/__tests__/BaseDocumentSerializer/utils.ts +27 -0
- package/src/serialization/__tests__/README +2 -0
- package/src/serialization/__tests__/__fixtures__/annotationAndInlineBlocks.json +140 -0
- package/src/serialization/__tests__/__fixtures__/customStyles.json +62 -0
- package/src/serialization/__tests__/__fixtures__/documentInlineMarks.json +70 -0
- package/src/serialization/__tests__/__fixtures__/documentLevelArticle.json +185 -0
- package/src/serialization/__tests__/__fixtures__/fieldLevelArticle.json +107 -0
- package/src/serialization/__tests__/__fixtures__/inlineDocumentLevelArticle.json +134 -0
- package/src/serialization/__tests__/__fixtures__/inlineSchema.ts +270 -0
- package/src/serialization/__tests__/__fixtures__/messy-html.html +26 -0
- package/src/serialization/__tests__/__fixtures__/nestedLanguageFields.json +54 -0
- package/src/serialization/__tests__/__fixtures__/schema.ts +310 -0
- package/src/serialization/__tests__/global.setup.ts +40 -0
- package/src/serialization/__tests__/helpers.ts +132 -0
- package/src/serialization/data.ts +82 -0
- package/src/serialization/deserialize/BaseDocumentDeserializer.ts +171 -0
- package/src/serialization/deserialize/helpers.ts +42 -0
- package/src/serialization/helpers.ts +18 -0
- package/src/serialization/index.ts +11 -0
- package/src/serialization/serialize/fieldFilters.ts +124 -0
- package/src/serialization/serialize/index.ts +284 -0
- package/src/serialization/types.ts +41 -0
- package/src/translation/importDocument.ts +4 -5
- package/src/translation/uploadFiles.ts +1 -1
- package/src/types.ts +3 -19
- package/src/utils/batchProcessor.ts +111 -0
- package/src/utils/importUtils.ts +95 -0
- package/src/utils/serialize.ts +25 -5
- package/src/utils/shared.ts +1 -1
- package/src/adapter/index.ts +0 -13
- package/src/components/NewTask.tsx +0 -251
- package/src/components/TaskView.tsx +0 -257
- package/src/components/TranslationContext.tsx +0 -24
- package/src/components/TranslationView.tsx +0 -114
- package/src/components/TranslationsTab.tsx +0 -181
- /package/src/components/{LanguageStatus.tsx → shared/LanguageStatus.tsx} +0 -0
- /package/src/components/{ProgressBar.tsx → shared/ProgressBar.tsx} +0 -0
|
@@ -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
|
+
];
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
|
+
|
|
3
|
+
exports[`Global test of working doc-level functionality and snapshot match 1`] = `
|
|
4
|
+
{
|
|
5
|
+
"_id": "drafts.d8ffc675-ce86-4f60-9ac8-da164cde3b0a",
|
|
6
|
+
"_rev": "fxcnqj-uew-gr7-v6r-z965tfm69",
|
|
7
|
+
"_type": "documentLevelArticle",
|
|
8
|
+
"config": {
|
|
9
|
+
"_type": "objectField",
|
|
10
|
+
"nestedArrayField": [
|
|
11
|
+
{
|
|
12
|
+
"_key": "4a58adc7c507",
|
|
13
|
+
"_type": "block",
|
|
14
|
+
"children": [
|
|
15
|
+
{
|
|
16
|
+
"_key": "randomKey-16",
|
|
17
|
+
"_type": "span",
|
|
18
|
+
"marks": [],
|
|
19
|
+
"text": "This is block text 1 level deep",
|
|
20
|
+
},
|
|
21
|
+
],
|
|
22
|
+
"markDefs": [],
|
|
23
|
+
"style": "normal",
|
|
24
|
+
},
|
|
25
|
+
],
|
|
26
|
+
"objectAsField": {
|
|
27
|
+
"_type": "childObjectField",
|
|
28
|
+
"content": [
|
|
29
|
+
{
|
|
30
|
+
"_key": "1aa228ac848b",
|
|
31
|
+
"_type": "block",
|
|
32
|
+
"children": [
|
|
33
|
+
{
|
|
34
|
+
"_key": "randomKey-14",
|
|
35
|
+
"_type": "span",
|
|
36
|
+
"marks": [],
|
|
37
|
+
"text": "This is a block text 2 levels deep",
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
"markDefs": [],
|
|
41
|
+
"style": "normal",
|
|
42
|
+
},
|
|
43
|
+
],
|
|
44
|
+
"title": "This is one level deeper",
|
|
45
|
+
},
|
|
46
|
+
"title": "This is an object nested in a document",
|
|
47
|
+
},
|
|
48
|
+
"content": [
|
|
49
|
+
{
|
|
50
|
+
"_key": "a4d177e92666",
|
|
51
|
+
"_type": "block",
|
|
52
|
+
"children": [
|
|
53
|
+
{
|
|
54
|
+
"_key": "randomKey-18",
|
|
55
|
+
"_type": "span",
|
|
56
|
+
"marks": [],
|
|
57
|
+
"text": "This is block text at the top level.",
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
"markDefs": [],
|
|
61
|
+
"style": "normal",
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
"_key": "c0313627775e",
|
|
65
|
+
"_type": "objectField",
|
|
66
|
+
"objectAsField": {
|
|
67
|
+
"_type": "childObjectField",
|
|
68
|
+
"content": [
|
|
69
|
+
{
|
|
70
|
+
"_key": "4f24f6fbfae7",
|
|
71
|
+
"_type": "block",
|
|
72
|
+
"children": [
|
|
73
|
+
{
|
|
74
|
+
"_key": "randomKey-24",
|
|
75
|
+
"_type": "span",
|
|
76
|
+
"marks": [],
|
|
77
|
+
"text": "This is block text in a nested object in an object in top-level block text.",
|
|
78
|
+
},
|
|
79
|
+
],
|
|
80
|
+
"markDefs": [],
|
|
81
|
+
"style": "normal",
|
|
82
|
+
},
|
|
83
|
+
],
|
|
84
|
+
"title": "This is a nested object in an object in top-level block text.",
|
|
85
|
+
},
|
|
86
|
+
"title": "This is an object in top-level block text.",
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
"_key": "9e2ab13c6d63",
|
|
90
|
+
"_type": "block",
|
|
91
|
+
"children": [
|
|
92
|
+
{
|
|
93
|
+
"_key": "randomKey-26",
|
|
94
|
+
"_type": "span",
|
|
95
|
+
"marks": [],
|
|
96
|
+
"text": "This is h1 text",
|
|
97
|
+
},
|
|
98
|
+
],
|
|
99
|
+
"markDefs": [],
|
|
100
|
+
"style": "h1",
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
"_key": "297142fbaf21",
|
|
104
|
+
"_type": "block",
|
|
105
|
+
"children": [
|
|
106
|
+
{
|
|
107
|
+
"_key": "randomKey-28",
|
|
108
|
+
"_type": "span",
|
|
109
|
+
"marks": [],
|
|
110
|
+
"text": "This is h2 text",
|
|
111
|
+
},
|
|
112
|
+
],
|
|
113
|
+
"markDefs": [],
|
|
114
|
+
"style": "h2",
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
"_key": "76e648fc3845",
|
|
118
|
+
"_type": "block",
|
|
119
|
+
"children": [
|
|
120
|
+
{
|
|
121
|
+
"_key": "randomKey-32",
|
|
122
|
+
"_type": "span",
|
|
123
|
+
"marks": [],
|
|
124
|
+
"text": "Bullet 1",
|
|
125
|
+
},
|
|
126
|
+
],
|
|
127
|
+
"level": 1,
|
|
128
|
+
"listItem": "bullet",
|
|
129
|
+
"markDefs": [],
|
|
130
|
+
"style": "normal",
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
"_key": "d090cd8b27d2",
|
|
134
|
+
"_type": "block",
|
|
135
|
+
"children": [
|
|
136
|
+
{
|
|
137
|
+
"_key": "randomKey-36",
|
|
138
|
+
"_type": "span",
|
|
139
|
+
"marks": [],
|
|
140
|
+
"text": "nested bullet a",
|
|
141
|
+
},
|
|
142
|
+
],
|
|
143
|
+
"level": 2,
|
|
144
|
+
"listItem": "bullet",
|
|
145
|
+
"markDefs": [],
|
|
146
|
+
"style": "normal",
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
"_key": "1cdffa5d50f5",
|
|
150
|
+
"_type": "block",
|
|
151
|
+
"children": [
|
|
152
|
+
{
|
|
153
|
+
"_key": "randomKey-42",
|
|
154
|
+
"_type": "span",
|
|
155
|
+
"marks": [],
|
|
156
|
+
"text": "Styled bullet 2",
|
|
157
|
+
},
|
|
158
|
+
],
|
|
159
|
+
"level": 1,
|
|
160
|
+
"listItem": "bullet",
|
|
161
|
+
"markDefs": [],
|
|
162
|
+
"style": "h2",
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
"_key": "da29f5063059",
|
|
166
|
+
"_type": "block",
|
|
167
|
+
"children": [
|
|
168
|
+
{
|
|
169
|
+
"_key": "randomKey-48",
|
|
170
|
+
"_type": "span",
|
|
171
|
+
"marks": [],
|
|
172
|
+
"text": "Number 1",
|
|
173
|
+
},
|
|
174
|
+
],
|
|
175
|
+
"level": 1,
|
|
176
|
+
"listItem": "number",
|
|
177
|
+
"markDefs": [],
|
|
178
|
+
"style": "h3",
|
|
179
|
+
},
|
|
180
|
+
],
|
|
181
|
+
"snippet": "This is text in my text field.",
|
|
182
|
+
"tags": [
|
|
183
|
+
"tag 1",
|
|
184
|
+
"tag 2",
|
|
185
|
+
"tag 3",
|
|
186
|
+
],
|
|
187
|
+
"title": "My Document-Level Article",
|
|
188
|
+
}
|
|
189
|
+
`;
|