includio-cms 0.7.2 → 0.13.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/CHANGELOG.md +128 -0
- package/ROADMAP.md +54 -2
- package/dist/admin/api/generate-styles.d.ts +2 -0
- package/dist/admin/api/generate-styles.js +32 -0
- package/dist/admin/api/handler.js +33 -0
- package/dist/admin/api/media-gc.js +10 -4
- package/dist/admin/api/rest/handler.js +17 -0
- package/dist/admin/api/rest/routes/collections.js +25 -13
- package/dist/admin/api/rest/routes/entries.d.ts +1 -1
- package/dist/admin/api/rest/routes/entries.js +10 -10
- package/dist/admin/api/rest/routes/media.d.ts +2 -0
- package/dist/admin/api/rest/routes/media.js +9 -0
- package/dist/admin/api/rest/routes/schema.d.ts +5 -0
- package/dist/admin/api/rest/routes/schema.js +152 -0
- package/dist/admin/api/rest/routes/singletons.d.ts +1 -1
- package/dist/admin/api/rest/routes/singletons.js +8 -7
- package/dist/admin/api/rest/routes/upload.d.ts +2 -0
- package/dist/admin/api/rest/routes/upload.js +28 -0
- package/dist/admin/api/upload.js +13 -0
- package/dist/admin/client/collection/collection-entries.svelte +35 -13
- package/dist/admin/client/entry/entry.svelte +21 -23
- package/dist/admin/client/entry/header/a11y-validator.js +2 -2
- package/dist/admin/client/entry/header/publish-panel.svelte +33 -85
- package/dist/admin/client/entry/header/status-badge.svelte +2 -2
- package/dist/admin/client/entry/header/version-history-sheet.svelte +9 -9
- package/dist/admin/client/entry/header/visibility.svelte +16 -10
- package/dist/admin/client/entry/utils.d.ts +3 -0
- package/dist/admin/client/entry/utils.js +22 -4
- package/dist/admin/client/form/form-submission/form-submission-page.svelte +4 -1
- package/dist/admin/client/form/form-submission/submission-field.svelte +10 -0
- package/dist/admin/client/index.d.ts +1 -0
- package/dist/admin/client/index.js +1 -0
- package/dist/admin/client/maintenance/maintenance-page.svelte +146 -2
- package/dist/admin/client/users/users-page.svelte +5 -6
- package/dist/admin/client/users/users-page.svelte.d.ts +1 -4
- package/dist/admin/components/fields/block-picker-modal.svelte +13 -4
- package/dist/admin/components/fields/blocks-field.svelte +40 -19
- package/dist/admin/components/fields/field-renderer.svelte +4 -8
- package/dist/admin/components/fields/object-field.svelte +7 -12
- package/dist/admin/components/fields/select-field.svelte +8 -2
- package/dist/admin/components/fields/seo-field.svelte +40 -93
- package/dist/admin/components/fields/simple-array-field.svelte +27 -16
- package/dist/admin/components/fields/text-field-wrapper.svelte +52 -197
- package/dist/admin/components/fields/text-field-wrapper.svelte.d.ts +2 -2
- package/dist/admin/components/fields/url-field-wrapper.svelte +15 -25
- package/dist/admin/components/fields/url-field.svelte +61 -72
- package/dist/admin/components/layout/layout-renderer.svelte +10 -4
- package/dist/admin/components/media/file-preview.svelte +10 -1
- package/dist/admin/components/media/file-upload.svelte +5 -1
- package/dist/admin/components/media/file-upload.svelte.d.ts +1 -0
- package/dist/admin/components/media/files-list.svelte +12 -3
- package/dist/admin/components/media/media-library.svelte +109 -37
- package/dist/admin/components/media/media-selector.svelte +90 -16
- package/dist/admin/components/media/tag-sidebar.svelte +10 -6
- package/dist/admin/components/media/tag-sidebar.svelte.d.ts +7 -2
- package/dist/admin/components/tiptap/FigureNodeView.svelte +15 -10
- package/dist/admin/components/tiptap/InlineBlockNodeView.svelte +53 -94
- package/dist/admin/components/tiptap/SlashCommandPopup.svelte +8 -3
- package/dist/admin/components/tiptap/editor-toolbar.svelte +28 -23
- package/dist/admin/components/tiptap/image-dialog.svelte +12 -7
- package/dist/admin/components/tiptap/inline-block-node.js +6 -5
- package/dist/admin/components/tiptap/lang.d.ts +77 -0
- package/dist/admin/components/tiptap/lang.js +170 -0
- package/dist/admin/components/tiptap/link-dialog.svelte +31 -28
- package/dist/admin/components/tiptap/slash-command.js +27 -23
- package/dist/admin/components/tiptap/table-dialog.svelte +9 -4
- package/dist/admin/components/tiptap/video-dialog.svelte +6 -1
- package/dist/admin/remote/email.remote.d.ts +1 -0
- package/dist/admin/remote/email.remote.js +5 -0
- package/dist/admin/remote/entry.remote.d.ts +2 -5
- package/dist/admin/remote/entry.remote.js +23 -28
- package/dist/admin/remote/index.d.ts +1 -0
- package/dist/admin/remote/index.js +1 -0
- package/dist/admin/remote/media.remote.d.ts +15 -0
- package/dist/admin/remote/media.remote.js +18 -2
- package/dist/admin/remote/preview.remote.js +3 -1
- package/dist/admin/utils/entryLabel.js +9 -6
- package/dist/admin/utils/translationStatus.js +1 -2
- package/dist/cli/scaffold/admin.js +34 -2
- package/dist/cms/runtime/api.d.ts +16 -12
- package/dist/cms/runtime/api.js +7 -6
- package/dist/cms/runtime/remote.js +2 -2
- package/dist/cms/runtime/schemas.d.ts +1 -1
- package/dist/cms/runtime/schemas.js +1 -1
- package/dist/cms/runtime/types.d.ts +118 -112
- package/dist/cms/runtime/types.js +0 -12
- package/dist/core/cms.d.ts +3 -1
- package/dist/core/cms.js +30 -0
- package/dist/core/fields/fieldSchemaToTs.js +9 -15
- package/dist/core/fields/formFieldSchemaToTs.js +7 -0
- package/dist/core/server/entries/operations/create.js +10 -4
- package/dist/core/server/entries/operations/get.d.ts +1 -0
- package/dist/core/server/entries/operations/get.js +186 -191
- package/dist/core/server/entries/operations/update.d.ts +6 -7
- package/dist/core/server/entries/operations/update.js +20 -38
- package/dist/core/server/fields/populateEntry.js +16 -52
- package/dist/core/server/fields/resolveImageFields.js +69 -120
- package/dist/core/server/fields/resolveRelationFields.js +30 -51
- package/dist/core/server/fields/resolveRichtextLinks.js +46 -100
- package/dist/core/server/fields/resolveTypographyOrphans.bench.d.ts +1 -0
- package/dist/core/server/fields/resolveTypographyOrphans.bench.js +87 -0
- package/dist/core/server/fields/resolveTypographyOrphans.d.ts +3 -0
- package/dist/core/server/fields/resolveTypographyOrphans.js +128 -0
- package/dist/core/server/fields/resolveUrlFields.js +47 -56
- package/dist/core/server/fields/utils/fixOrphans.d.ts +5 -0
- package/dist/core/server/fields/utils/fixOrphans.js +12 -0
- package/dist/core/server/fields/utils/imageStyles.d.ts +4 -2
- package/dist/core/server/fields/utils/imageStyles.js +41 -25
- package/dist/core/server/fields/utils/resolveMedia.js +1 -6
- package/dist/core/server/forms/submissions/operations/delete.js +26 -2
- package/dist/core/server/forms/submissions/utils/parseMultipart.d.ts +2 -0
- package/dist/core/server/forms/submissions/utils/parseMultipart.js +75 -0
- package/dist/core/server/generator/fields.d.ts +6 -0
- package/dist/core/server/generator/fields.js +43 -5
- package/dist/core/server/generator/formFieldSchemaToString.js +10 -0
- package/dist/core/server/generator/formFields.js +1 -0
- package/dist/core/server/generator/generator.js +98 -30
- package/dist/core/server/media/operations/getFiles.d.ts +5 -0
- package/dist/core/server/media/operations/getFiles.js +6 -0
- package/dist/core/server/media/operations/uploadPrivateFile.d.ts +4 -0
- package/dist/core/server/media/operations/uploadPrivateFile.js +8 -0
- package/dist/core/server/media/styles/operations/batchGenerateStyles.d.ts +16 -0
- package/dist/core/server/media/styles/operations/batchGenerateStyles.js +144 -0
- package/dist/db-postgres/index.js +303 -37
- package/dist/db-postgres/schema/entry.d.ts +0 -94
- package/dist/db-postgres/schema/entry.js +0 -6
- package/dist/db-postgres/schema/entryVersion.d.ts +17 -0
- package/dist/db-postgres/schema/entryVersion.js +1 -0
- package/dist/entity/index.d.ts +9 -4
- package/dist/entity/index.js +24 -24
- package/dist/files-local/index.js +43 -0
- package/dist/paraglide/messages/_index.d.ts +36 -3
- package/dist/paraglide/messages/_index.js +71 -3
- package/dist/paraglide/messages/en.d.ts +5 -0
- package/dist/paraglide/messages/en.js +14 -0
- package/dist/paraglide/messages/pl.d.ts +5 -0
- package/dist/paraglide/messages/pl.js +14 -0
- package/dist/sveltekit/components/preview.svelte +2 -326
- package/dist/sveltekit/components/preview.svelte.d.ts +5 -16
- package/dist/sveltekit/server/index.d.ts +2 -1
- package/dist/sveltekit/server/index.js +2 -1
- package/dist/sveltekit/server/preview.js +4 -7
- package/dist/types/adapters/db.d.ts +15 -1
- package/dist/types/adapters/files.d.ts +6 -0
- package/dist/types/cms.d.ts +5 -0
- package/dist/types/entries.d.ts +54 -18
- package/dist/types/fields.d.ts +14 -24
- package/dist/types/formFields.d.ts +7 -2
- package/dist/types/index.d.ts +2 -2
- package/dist/types/layout.d.ts +0 -1
- package/dist/types/structured-content.d.ts +5 -0
- package/dist/updates/0.10.0/index.d.ts +2 -0
- package/dist/updates/0.10.0/index.js +15 -0
- package/dist/updates/0.11.0/index.d.ts +2 -0
- package/dist/updates/0.11.0/index.js +12 -0
- package/dist/updates/0.12.0/index.d.ts +2 -0
- package/dist/updates/0.12.0/index.js +12 -0
- package/dist/updates/0.13.0/index.d.ts +2 -0
- package/dist/updates/0.13.0/index.js +10 -0
- package/dist/updates/0.13.1/index.d.ts +2 -0
- package/dist/updates/0.13.1/index.js +20 -0
- package/dist/updates/0.7.3/index.d.ts +2 -0
- package/dist/updates/0.7.3/index.js +10 -0
- package/dist/updates/0.8.0/index.d.ts +2 -0
- package/dist/updates/0.8.0/index.js +18 -0
- package/dist/updates/0.8.0/migrate.d.ts +2 -0
- package/dist/updates/0.8.0/migrate.js +101 -0
- package/dist/updates/0.9.0/index.d.ts +2 -0
- package/dist/updates/0.9.0/index.js +38 -0
- package/dist/updates/index.js +9 -1
- package/package.json +7 -6
- package/dist/admin/components/fields/image-field.svelte +0 -198
- package/dist/admin/components/fields/image-field.svelte.d.ts +0 -8
- package/dist/admin/components/fields/richtext-field.svelte +0 -13
- package/dist/admin/components/fields/richtext-field.svelte.d.ts +0 -8
- package/dist/admin/components/tiptap.svelte +0 -11
- package/dist/admin/components/tiptap.svelte.d.ts +0 -6
- package/dist/core/server/entries/utils/getEntryTranslation.d.ts +0 -1
- package/dist/core/server/entries/utils/getEntryTranslation.js +0 -18
- package/dist/paraglide/messages/hello_world.d.ts +0 -5
- package/dist/paraglide/messages/hello_world.js +0 -33
- package/dist/paraglide/messages/login_hello.d.ts +0 -16
- package/dist/paraglide/messages/login_hello.js +0 -34
- package/dist/paraglide/messages/login_please_login.d.ts +0 -16
- package/dist/paraglide/messages/login_please_login.js +0 -34
|
@@ -1,22 +1,6 @@
|
|
|
1
1
|
import { getCMS } from '../../cms.js';
|
|
2
|
-
import { extractEntryIds as extractEntryIdsFromDoc,
|
|
2
|
+
import { extractEntryIds as extractEntryIdsFromDoc, cloneDoc, walkLinkMarks, walkInlineBlockNodes } from '../../../admin/components/tiptap/structured-content-utils.js';
|
|
3
3
|
import { getEntrySlugPath, getSlugFromEntryData, getEntryPath } from './slugResolver.js';
|
|
4
|
-
const ENTRY_ID_RE = /data-entry-id="([0-9a-f-]{36})"/g;
|
|
5
|
-
function extractEntryIds(html) {
|
|
6
|
-
const ids = [];
|
|
7
|
-
for (const match of html.matchAll(ENTRY_ID_RE)) {
|
|
8
|
-
ids.push(match[1]);
|
|
9
|
-
}
|
|
10
|
-
return ids;
|
|
11
|
-
}
|
|
12
|
-
function resolveHtml(html, slugMap) {
|
|
13
|
-
return html.replace(/<a\s([^>]*data-entry-id="([0-9a-f-]{36})"[^>]*)>/g, (match, _attrs, id) => {
|
|
14
|
-
const slug = slugMap[id];
|
|
15
|
-
if (!slug)
|
|
16
|
-
return match;
|
|
17
|
-
return match.replace(/href="[^"]*"/, `href="${slug}"`);
|
|
18
|
-
});
|
|
19
|
-
}
|
|
20
4
|
/** Extract entry IDs from inlineBlock blockData relation fields. */
|
|
21
5
|
function extractEntryIdsFromInlineBlocks(doc) {
|
|
22
6
|
const ids = [];
|
|
@@ -59,41 +43,23 @@ export async function resolveRichtextLinks(data, fields, language) {
|
|
|
59
43
|
if (val == null)
|
|
60
44
|
continue;
|
|
61
45
|
switch (field.type) {
|
|
62
|
-
case 'richtext': {
|
|
63
|
-
// Before translateObject, richtext is { lang: "html", ... }
|
|
64
|
-
if (typeof val === 'object') {
|
|
65
|
-
for (const html of Object.values(val)) {
|
|
66
|
-
if (typeof html === 'string') {
|
|
67
|
-
entriesIds.push(...extractEntryIds(html));
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
else if (typeof val === 'string') {
|
|
72
|
-
entriesIds.push(...extractEntryIds(val));
|
|
73
|
-
}
|
|
74
|
-
break;
|
|
75
|
-
}
|
|
76
46
|
case 'content': {
|
|
77
|
-
// Content
|
|
78
|
-
if (typeof val === 'object' && val
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
entriesIds.push(...extractEntryIdsFromDoc(doc));
|
|
82
|
-
entriesIds.push(...extractEntryIdsFromInlineBlocks(doc));
|
|
83
|
-
}
|
|
84
|
-
}
|
|
47
|
+
// Content is now a single doc, not Record<lang, doc>
|
|
48
|
+
if (val && typeof val === 'object' && val.type === 'doc') {
|
|
49
|
+
entriesIds.push(...extractEntryIdsFromDoc(val));
|
|
50
|
+
entriesIds.push(...extractEntryIdsFromInlineBlocks(val));
|
|
85
51
|
}
|
|
86
52
|
break;
|
|
87
53
|
}
|
|
88
54
|
case 'object':
|
|
89
|
-
collectIds(val
|
|
55
|
+
collectIds(val, field.fields);
|
|
90
56
|
break;
|
|
91
57
|
case 'blocks':
|
|
92
58
|
if (Array.isArray(val)) {
|
|
93
59
|
val.forEach((item) => {
|
|
94
|
-
const objectDef = field.of.find((objDef) => objDef.slug === item.
|
|
60
|
+
const objectDef = field.of.find((objDef) => objDef.slug === item._slug);
|
|
95
61
|
if (objectDef) {
|
|
96
|
-
collectIds(item
|
|
62
|
+
collectIds(item, objectDef.fields);
|
|
97
63
|
}
|
|
98
64
|
});
|
|
99
65
|
}
|
|
@@ -107,16 +73,28 @@ export async function resolveRichtextLinks(data, fields, language) {
|
|
|
107
73
|
if (uniqueIds.length === 0)
|
|
108
74
|
return data;
|
|
109
75
|
const db = getCMS().databaseAdapter;
|
|
110
|
-
// Get entries
|
|
76
|
+
// Get entries and find their published versions for the target language
|
|
111
77
|
const entries = await db.getEntries({ ids: uniqueIds });
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
.map((e) => e.publishedVersionId);
|
|
115
|
-
if (publishedVersionIds.length === 0)
|
|
78
|
+
const entryIds = entries.map((e) => e.id);
|
|
79
|
+
if (entryIds.length === 0)
|
|
116
80
|
return data;
|
|
117
81
|
const versions = await db.getEntryVersions({
|
|
118
|
-
|
|
82
|
+
entryIds,
|
|
83
|
+
lang: language
|
|
119
84
|
});
|
|
85
|
+
const now = new Date();
|
|
86
|
+
// Pick latest published version per entry
|
|
87
|
+
const publishedByEntry = new Map();
|
|
88
|
+
for (const v of versions) {
|
|
89
|
+
if (v.publishedAt != null && v.publishedAt <= now) {
|
|
90
|
+
const existing = publishedByEntry.get(v.entryId);
|
|
91
|
+
if (!existing || v.versionNumber > existing.versionNumber) {
|
|
92
|
+
publishedByEntry.set(v.entryId, v);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (publishedByEntry.size === 0)
|
|
97
|
+
return data;
|
|
120
98
|
// Build map: entryId → collectionSlug for config lookup
|
|
121
99
|
const entryConfigSlugMap = {};
|
|
122
100
|
for (const entry of entries) {
|
|
@@ -124,14 +102,13 @@ export async function resolveRichtextLinks(data, fields, language) {
|
|
|
124
102
|
}
|
|
125
103
|
// Build map: entryId → resolved slug string
|
|
126
104
|
const slugMap = {};
|
|
127
|
-
for (const version of
|
|
105
|
+
for (const [entryId, version] of publishedByEntry) {
|
|
128
106
|
const rawData = version.data;
|
|
129
|
-
const configSlug = entryConfigSlugMap[
|
|
107
|
+
const configSlug = entryConfigSlugMap[entryId];
|
|
130
108
|
const slugPath = configSlug ? getEntrySlugPath(configSlug) : 'seo.slug';
|
|
131
109
|
const slug = getSlugFromEntryData(rawData, slugPath, language);
|
|
132
110
|
if (slug) {
|
|
133
|
-
|
|
134
|
-
slugMap[version.entryId] = configSlug ? getEntryPath(configSlug, slug) : slug;
|
|
111
|
+
slugMap[entryId] = configSlug ? getEntryPath(configSlug, slug) : slug;
|
|
135
112
|
}
|
|
136
113
|
}
|
|
137
114
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
@@ -145,66 +122,35 @@ export async function resolveRichtextLinks(data, fields, language) {
|
|
|
145
122
|
continue;
|
|
146
123
|
}
|
|
147
124
|
switch (field.type) {
|
|
148
|
-
case 'richtext': {
|
|
149
|
-
if (typeof val === 'object') {
|
|
150
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
151
|
-
const resolved = {};
|
|
152
|
-
for (const [lang, html] of Object.entries(val)) {
|
|
153
|
-
resolved[lang] =
|
|
154
|
-
typeof html === 'string' ? resolveHtml(html, slugMap) : html;
|
|
155
|
-
}
|
|
156
|
-
result[field.slug] = resolved;
|
|
157
|
-
}
|
|
158
|
-
else if (typeof val === 'string') {
|
|
159
|
-
result[field.slug] = resolveHtml(val, slugMap);
|
|
160
|
-
}
|
|
161
|
-
break;
|
|
162
|
-
}
|
|
163
125
|
case 'content': {
|
|
164
126
|
const contentField = field;
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
if (!def)
|
|
179
|
-
return;
|
|
180
|
-
node.attrs.blockData = resolveValues(bd, def.fields);
|
|
181
|
-
});
|
|
182
|
-
}
|
|
183
|
-
resolved[lang] = cloned;
|
|
184
|
-
}
|
|
185
|
-
else {
|
|
186
|
-
resolved[lang] = doc;
|
|
187
|
-
}
|
|
127
|
+
// Content is now a single doc
|
|
128
|
+
if (val && typeof val === 'object' && val.type === 'doc') {
|
|
129
|
+
const cloned = resolveContentDoc(val, slugMap);
|
|
130
|
+
if (contentField.inlineBlocks?.length) {
|
|
131
|
+
walkInlineBlockNodes(cloned, (node) => {
|
|
132
|
+
const bd = node.attrs?.blockData;
|
|
133
|
+
if (!bd || typeof bd !== 'object')
|
|
134
|
+
return;
|
|
135
|
+
const def = contentField.inlineBlocks.find((b) => b.slug === node.attrs?.blockType);
|
|
136
|
+
if (!def)
|
|
137
|
+
return;
|
|
138
|
+
node.attrs.blockData = resolveValues(bd, def.fields);
|
|
139
|
+
});
|
|
188
140
|
}
|
|
189
|
-
result[field.slug] =
|
|
141
|
+
result[field.slug] = cloned;
|
|
190
142
|
}
|
|
191
143
|
break;
|
|
192
144
|
}
|
|
193
145
|
case 'object':
|
|
194
|
-
result[field.slug] =
|
|
195
|
-
...val,
|
|
196
|
-
data: resolveValues(val.data, field.fields)
|
|
197
|
-
};
|
|
146
|
+
result[field.slug] = resolveValues(val, field.fields);
|
|
198
147
|
break;
|
|
199
148
|
case 'blocks':
|
|
200
149
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
201
150
|
result[field.slug] = val.map((item) => {
|
|
202
|
-
const objectDef = field.of.find((objDef) => objDef.slug === item.
|
|
151
|
+
const objectDef = field.of.find((objDef) => objDef.slug === item._slug);
|
|
203
152
|
if (objectDef) {
|
|
204
|
-
return
|
|
205
|
-
...item,
|
|
206
|
-
data: resolveValues(item.data, objectDef.fields)
|
|
207
|
-
};
|
|
153
|
+
return resolveValues(item, objectDef.fields);
|
|
208
154
|
}
|
|
209
155
|
return item;
|
|
210
156
|
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { bench, describe } from 'vitest';
|
|
2
|
+
import { resolveTypographyOrphans } from './resolveTypographyOrphans.js';
|
|
3
|
+
function makeParagraph(text) {
|
|
4
|
+
return {
|
|
5
|
+
type: 'paragraph',
|
|
6
|
+
content: [{ type: 'text', text }]
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
function makeContentDoc(paragraphCount) {
|
|
10
|
+
const content = [];
|
|
11
|
+
for (let i = 0; i < paragraphCount; i++) {
|
|
12
|
+
content.push(makeParagraph(`To jest paragraf numer ${i} i zawiera tekst o kotach w domu u Ali`));
|
|
13
|
+
}
|
|
14
|
+
return { type: 'doc', content };
|
|
15
|
+
}
|
|
16
|
+
// Build a large entry: 50 text fields, 5 content fields (100 paragraphs each), seo, url, nested object/blocks
|
|
17
|
+
function buildLargeEntry() {
|
|
18
|
+
const data = {};
|
|
19
|
+
const fields = [];
|
|
20
|
+
// 50 text fields
|
|
21
|
+
for (let i = 0; i < 50; i++) {
|
|
22
|
+
const slug = `text_${i}`;
|
|
23
|
+
data[slug] = `To jest pole tekstowe numer ${i} i zawiera polskie spójniki w tekście o kotach`;
|
|
24
|
+
fields.push({ type: 'text', slug });
|
|
25
|
+
}
|
|
26
|
+
// 5 content fields with 100 paragraphs each
|
|
27
|
+
for (let i = 0; i < 5; i++) {
|
|
28
|
+
const slug = `content_${i}`;
|
|
29
|
+
data[slug] = makeContentDoc(100);
|
|
30
|
+
fields.push({ type: 'content', slug });
|
|
31
|
+
}
|
|
32
|
+
// SEO field
|
|
33
|
+
data.seo = {
|
|
34
|
+
slug: 'test-page',
|
|
35
|
+
title: 'Strona o kotach i psach w domu u Ali',
|
|
36
|
+
description: 'Dowiedz się o kotach i psach w ogrodzie u naszych znajomych'
|
|
37
|
+
};
|
|
38
|
+
fields.push({ type: 'seo', slug: 'seo' });
|
|
39
|
+
// URL field
|
|
40
|
+
data.link = { url: '/test', text: 'Kliknij i zobacz' };
|
|
41
|
+
fields.push({ type: 'url', slug: 'link' });
|
|
42
|
+
// Nested object
|
|
43
|
+
data.hero = {
|
|
44
|
+
heading: 'Witaj w naszym domu i ogrodzie',
|
|
45
|
+
subheading: 'Zapraszamy do zwiedzania i odpoczynku'
|
|
46
|
+
};
|
|
47
|
+
fields.push({
|
|
48
|
+
type: 'object',
|
|
49
|
+
slug: 'hero',
|
|
50
|
+
fields: [
|
|
51
|
+
{ type: 'text', slug: 'heading' },
|
|
52
|
+
{ type: 'text', slug: 'subheading' }
|
|
53
|
+
]
|
|
54
|
+
});
|
|
55
|
+
// Blocks
|
|
56
|
+
const blockItems = [];
|
|
57
|
+
for (let i = 0; i < 10; i++) {
|
|
58
|
+
blockItems.push({
|
|
59
|
+
_slug: 'text-block',
|
|
60
|
+
body: `Blok numer ${i} z tekstem o kotach i psach w domu`
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
data.sections = blockItems;
|
|
64
|
+
fields.push({
|
|
65
|
+
type: 'blocks',
|
|
66
|
+
slug: 'sections',
|
|
67
|
+
of: [
|
|
68
|
+
{
|
|
69
|
+
type: 'object',
|
|
70
|
+
slug: 'text-block',
|
|
71
|
+
fields: [{ type: 'text', slug: 'body' }]
|
|
72
|
+
}
|
|
73
|
+
]
|
|
74
|
+
});
|
|
75
|
+
return { data, fields };
|
|
76
|
+
}
|
|
77
|
+
const { data, fields } = buildLargeEntry();
|
|
78
|
+
describe('resolveTypographyOrphans benchmark', () => {
|
|
79
|
+
bench('with orphan fix', () => {
|
|
80
|
+
resolveTypographyOrphans(data, fields);
|
|
81
|
+
});
|
|
82
|
+
bench('baseline (no-op — skip resolver)', () => {
|
|
83
|
+
// Just spread to simulate similar allocation without regex
|
|
84
|
+
const result = { ...data };
|
|
85
|
+
void result;
|
|
86
|
+
});
|
|
87
|
+
});
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { walkNodes, cloneDoc } from '../../../admin/components/tiptap/structured-content-utils.js';
|
|
2
|
+
import { fixOrphans } from './utils/fixOrphans.js';
|
|
3
|
+
function fixContentDoc(doc) {
|
|
4
|
+
const cloned = cloneDoc(doc);
|
|
5
|
+
walkNodes(cloned.content, (node) => {
|
|
6
|
+
if (node.type === 'text' && node.text) {
|
|
7
|
+
node.text = fixOrphans(node.text);
|
|
8
|
+
}
|
|
9
|
+
if (node.type === 'figure' && node.attrs) {
|
|
10
|
+
if (typeof node.attrs.alt === 'string') {
|
|
11
|
+
node.attrs.alt = fixOrphans(node.attrs.alt);
|
|
12
|
+
}
|
|
13
|
+
if (typeof node.attrs.caption === 'string') {
|
|
14
|
+
node.attrs.caption = fixOrphans(node.attrs.caption);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
return cloned;
|
|
19
|
+
}
|
|
20
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
21
|
+
function resolveValues(value, fields) {
|
|
22
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
23
|
+
const result = { ...value };
|
|
24
|
+
for (const field of fields) {
|
|
25
|
+
const val = value?.[field.slug];
|
|
26
|
+
if (val == null)
|
|
27
|
+
continue;
|
|
28
|
+
switch (field.type) {
|
|
29
|
+
case 'text':
|
|
30
|
+
if (typeof val === 'string') {
|
|
31
|
+
result[field.slug] = fixOrphans(val);
|
|
32
|
+
}
|
|
33
|
+
break;
|
|
34
|
+
case 'content':
|
|
35
|
+
if (val && typeof val === 'object' && val.type === 'doc') {
|
|
36
|
+
const fixed = fixContentDoc(val);
|
|
37
|
+
// Also fix inline block text fields
|
|
38
|
+
const cf = field;
|
|
39
|
+
if (cf.inlineBlocks?.length) {
|
|
40
|
+
walkNodes(fixed.content, (node) => {
|
|
41
|
+
if (node.type !== 'inlineBlock')
|
|
42
|
+
return;
|
|
43
|
+
const bd = node.attrs?.blockData;
|
|
44
|
+
if (!bd || typeof bd !== 'object')
|
|
45
|
+
return;
|
|
46
|
+
const def = cf.inlineBlocks.find((b) => b.slug === node.attrs?.blockType);
|
|
47
|
+
if (def) {
|
|
48
|
+
node.attrs.blockData = resolveValues(bd, def.fields);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
result[field.slug] = fixed;
|
|
53
|
+
}
|
|
54
|
+
break;
|
|
55
|
+
case 'seo':
|
|
56
|
+
if (val && typeof val === 'object') {
|
|
57
|
+
const seo = { ...val };
|
|
58
|
+
if (seo.title)
|
|
59
|
+
seo.title = fixOrphans(seo.title);
|
|
60
|
+
if (seo.description)
|
|
61
|
+
seo.description = fixOrphans(seo.description);
|
|
62
|
+
result[field.slug] = seo;
|
|
63
|
+
}
|
|
64
|
+
break;
|
|
65
|
+
case 'url':
|
|
66
|
+
if (val && typeof val === 'object') {
|
|
67
|
+
// After resolveUrlFields, text may be a flat string or Record<lang, string>
|
|
68
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
69
|
+
const url = { ...val };
|
|
70
|
+
if (typeof url.text === 'string') {
|
|
71
|
+
url.text = fixOrphans(url.text);
|
|
72
|
+
}
|
|
73
|
+
else if (url.text && typeof url.text === 'object') {
|
|
74
|
+
const fixedText = {};
|
|
75
|
+
for (const [lang, text] of Object.entries(url.text)) {
|
|
76
|
+
fixedText[lang] = fixOrphans(text);
|
|
77
|
+
}
|
|
78
|
+
url.text = fixedText;
|
|
79
|
+
}
|
|
80
|
+
result[field.slug] = url;
|
|
81
|
+
}
|
|
82
|
+
break;
|
|
83
|
+
case 'object':
|
|
84
|
+
result[field.slug] = resolveValues(val, field.fields);
|
|
85
|
+
break;
|
|
86
|
+
case 'blocks':
|
|
87
|
+
if (Array.isArray(val)) {
|
|
88
|
+
result[field.slug] = val.map((item) => {
|
|
89
|
+
const blockDef = field.of.find((d) => d.slug === item._slug);
|
|
90
|
+
if (blockDef) {
|
|
91
|
+
return resolveValues(item, blockDef.fields);
|
|
92
|
+
}
|
|
93
|
+
return item;
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
break;
|
|
97
|
+
case 'array':
|
|
98
|
+
if (field.of === 'text' && Array.isArray(val)) {
|
|
99
|
+
result[field.slug] = val.map((item) => typeof item === 'string' ? fixOrphans(item) : item);
|
|
100
|
+
}
|
|
101
|
+
else if (field.of === 'url' && Array.isArray(val)) {
|
|
102
|
+
result[field.slug] = val.map((item) => {
|
|
103
|
+
if (item && typeof item === 'object') {
|
|
104
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
105
|
+
const url = { ...item };
|
|
106
|
+
if (typeof url.text === 'string') {
|
|
107
|
+
url.text = fixOrphans(url.text);
|
|
108
|
+
}
|
|
109
|
+
else if (url.text && typeof url.text === 'object') {
|
|
110
|
+
const fixedText = {};
|
|
111
|
+
for (const [lang, text] of Object.entries(url.text)) {
|
|
112
|
+
fixedText[lang] = fixOrphans(text);
|
|
113
|
+
}
|
|
114
|
+
url.text = fixedText;
|
|
115
|
+
}
|
|
116
|
+
return url;
|
|
117
|
+
}
|
|
118
|
+
return item;
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return result;
|
|
125
|
+
}
|
|
126
|
+
export function resolveTypographyOrphans(data, fields) {
|
|
127
|
+
return resolveValues(data, fields);
|
|
128
|
+
}
|
|
@@ -59,34 +59,30 @@ export async function resolveUrlFields(data, fields, language) {
|
|
|
59
59
|
break;
|
|
60
60
|
}
|
|
61
61
|
case 'object':
|
|
62
|
-
collectIds(val
|
|
62
|
+
collectIds(val, field.fields);
|
|
63
63
|
break;
|
|
64
64
|
case 'blocks':
|
|
65
65
|
if (Array.isArray(val)) {
|
|
66
66
|
val.forEach((item) => {
|
|
67
|
-
|
|
68
|
-
const objectDef = field.of.find((objDef) => objDef.slug === item.slug);
|
|
67
|
+
const objectDef = field.of.find((objDef) => objDef.slug === item._slug);
|
|
69
68
|
if (objectDef) {
|
|
70
|
-
collectIds(item
|
|
69
|
+
collectIds(item, objectDef.fields);
|
|
71
70
|
}
|
|
72
71
|
});
|
|
73
72
|
}
|
|
74
73
|
break;
|
|
75
74
|
case 'content': {
|
|
76
75
|
const cf = field;
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
});
|
|
88
|
-
}
|
|
89
|
-
}
|
|
76
|
+
// Content is now a single doc, not Record<lang, doc>
|
|
77
|
+
if (val && typeof val === 'object' && val.type === 'doc' && cf.inlineBlocks?.length) {
|
|
78
|
+
walkInlineBlockNodes(val, (node) => {
|
|
79
|
+
const bd = node.attrs?.blockData;
|
|
80
|
+
if (!bd || typeof bd !== 'object')
|
|
81
|
+
return;
|
|
82
|
+
const def = cf.inlineBlocks.find((b) => b.slug === node.attrs?.blockType);
|
|
83
|
+
if (def)
|
|
84
|
+
collectIds(bd, def.fields);
|
|
85
|
+
});
|
|
90
86
|
}
|
|
91
87
|
break;
|
|
92
88
|
}
|
|
@@ -108,12 +104,24 @@ export async function resolveUrlFields(data, fields, language) {
|
|
|
108
104
|
if (entriesIds.length > 0) {
|
|
109
105
|
// Use raw DB calls to avoid recursive populateEntryData → resolveUrlFields loop
|
|
110
106
|
const dbEntries = await getDbEntries({ ids: entriesIds });
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
107
|
+
const entryIds = dbEntries.map((e) => e.id);
|
|
108
|
+
if (entryIds.length > 0) {
|
|
109
|
+
// Get published versions for the target language
|
|
110
|
+
const versions = await getDbEntryVersions({
|
|
111
|
+
entryIds,
|
|
112
|
+
lang: language
|
|
113
|
+
});
|
|
114
|
+
const now = new Date();
|
|
115
|
+
// Group by entry, pick latest published
|
|
116
|
+
const versionByEntryId = new Map();
|
|
117
|
+
for (const v of versions) {
|
|
118
|
+
if (v.publishedAt != null && v.publishedAt <= now) {
|
|
119
|
+
const existing = versionByEntryId.get(v.entryId);
|
|
120
|
+
if (!existing || v.versionNumber > existing.versionNumber) {
|
|
121
|
+
versionByEntryId.set(v.entryId, v);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
117
125
|
for (const entry of dbEntries) {
|
|
118
126
|
const version = versionByEntryId.get(entry.id);
|
|
119
127
|
if (!version)
|
|
@@ -178,50 +186,33 @@ export async function resolveUrlFields(data, fields, language) {
|
|
|
178
186
|
break;
|
|
179
187
|
}
|
|
180
188
|
case 'object':
|
|
181
|
-
result[field.slug] =
|
|
182
|
-
...val,
|
|
183
|
-
data: resolveValues(val.data, field.fields)
|
|
184
|
-
};
|
|
189
|
+
result[field.slug] = resolveValues(val, field.fields);
|
|
185
190
|
break;
|
|
186
191
|
case 'blocks':
|
|
187
192
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
188
193
|
result[field.slug] = val.map((item) => {
|
|
189
|
-
|
|
190
|
-
const objectDef = field.of.find((objDef) => objDef.slug === item.slug);
|
|
194
|
+
const objectDef = field.of.find((objDef) => objDef.slug === item._slug);
|
|
191
195
|
if (objectDef) {
|
|
192
|
-
return
|
|
193
|
-
...item,
|
|
194
|
-
data: resolveValues(item.data, objectDef.fields)
|
|
195
|
-
};
|
|
196
|
+
return resolveValues(item, objectDef.fields);
|
|
196
197
|
}
|
|
197
|
-
// If no matching object definition found, return item unchanged
|
|
198
198
|
return item;
|
|
199
199
|
});
|
|
200
200
|
break;
|
|
201
201
|
case 'content': {
|
|
202
202
|
const cf = field;
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
node.attrs.blockData = resolveValues(bd, def.fields);
|
|
217
|
-
});
|
|
218
|
-
resolved[lang] = cloned;
|
|
219
|
-
}
|
|
220
|
-
else {
|
|
221
|
-
resolved[lang] = doc;
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
result[field.slug] = resolved;
|
|
203
|
+
// Content is now a single doc, not Record<lang, doc>
|
|
204
|
+
if (val && typeof val === 'object' && val.type === 'doc' && cf.inlineBlocks?.length) {
|
|
205
|
+
const cloned = cloneDoc(val);
|
|
206
|
+
walkInlineBlockNodes(cloned, (node) => {
|
|
207
|
+
const bd = node.attrs?.blockData;
|
|
208
|
+
if (!bd || typeof bd !== 'object')
|
|
209
|
+
return;
|
|
210
|
+
const def = cf.inlineBlocks.find((b) => b.slug === node.attrs?.blockType);
|
|
211
|
+
if (!def)
|
|
212
|
+
return;
|
|
213
|
+
node.attrs.blockData = resolveValues(bd, def.fields);
|
|
214
|
+
});
|
|
215
|
+
result[field.slug] = cloned;
|
|
225
216
|
}
|
|
226
217
|
break;
|
|
227
218
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Polish orphan conjunctions that should not appear at line end.
|
|
3
|
+
* Lowercase + uppercase variants of single-letter words.
|
|
4
|
+
*/
|
|
5
|
+
const ORPHAN_REGEX = /(?<=\s|^)([iIwWzZoOuUaAeE]) (?=\S)/g;
|
|
6
|
+
/**
|
|
7
|
+
* Replace regular space after single-letter Polish conjunctions with non-breaking space.
|
|
8
|
+
* Idempotent — already-fixed text (with \u00A0) is not modified.
|
|
9
|
+
*/
|
|
10
|
+
export function fixOrphans(text) {
|
|
11
|
+
return text.replace(ORPHAN_REGEX, '$1\u00A0');
|
|
12
|
+
}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ImageFieldStyle } from '../../../../types/fields.js';
|
|
2
2
|
import type { ImageStyle, MediaFile } from '../../../../types/media.js';
|
|
3
3
|
export declare const defaultStyles: ImageFieldStyle[];
|
|
4
4
|
export declare function getOriginalFormat(val: MediaFile): string;
|
|
5
5
|
export declare function expandStyleFormats(styles: ImageFieldStyle[], originalFormat?: string): ImageFieldStyle[];
|
|
6
6
|
export declare function isProcessableImage(val: MediaFile): boolean;
|
|
7
|
-
export declare function getImageStyles(field:
|
|
7
|
+
export declare function getImageStyles(field: {
|
|
8
|
+
styles?: ImageFieldStyle[];
|
|
9
|
+
}, val: MediaFile): Promise<{
|
|
8
10
|
styles: Record<string, ImageStyle>;
|
|
9
11
|
blurDataUrl: string | null;
|
|
10
12
|
}>;
|