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.
Files changed (185) hide show
  1. package/CHANGELOG.md +128 -0
  2. package/ROADMAP.md +54 -2
  3. package/dist/admin/api/generate-styles.d.ts +2 -0
  4. package/dist/admin/api/generate-styles.js +32 -0
  5. package/dist/admin/api/handler.js +33 -0
  6. package/dist/admin/api/media-gc.js +10 -4
  7. package/dist/admin/api/rest/handler.js +17 -0
  8. package/dist/admin/api/rest/routes/collections.js +25 -13
  9. package/dist/admin/api/rest/routes/entries.d.ts +1 -1
  10. package/dist/admin/api/rest/routes/entries.js +10 -10
  11. package/dist/admin/api/rest/routes/media.d.ts +2 -0
  12. package/dist/admin/api/rest/routes/media.js +9 -0
  13. package/dist/admin/api/rest/routes/schema.d.ts +5 -0
  14. package/dist/admin/api/rest/routes/schema.js +152 -0
  15. package/dist/admin/api/rest/routes/singletons.d.ts +1 -1
  16. package/dist/admin/api/rest/routes/singletons.js +8 -7
  17. package/dist/admin/api/rest/routes/upload.d.ts +2 -0
  18. package/dist/admin/api/rest/routes/upload.js +28 -0
  19. package/dist/admin/api/upload.js +13 -0
  20. package/dist/admin/client/collection/collection-entries.svelte +35 -13
  21. package/dist/admin/client/entry/entry.svelte +21 -23
  22. package/dist/admin/client/entry/header/a11y-validator.js +2 -2
  23. package/dist/admin/client/entry/header/publish-panel.svelte +33 -85
  24. package/dist/admin/client/entry/header/status-badge.svelte +2 -2
  25. package/dist/admin/client/entry/header/version-history-sheet.svelte +9 -9
  26. package/dist/admin/client/entry/header/visibility.svelte +16 -10
  27. package/dist/admin/client/entry/utils.d.ts +3 -0
  28. package/dist/admin/client/entry/utils.js +22 -4
  29. package/dist/admin/client/form/form-submission/form-submission-page.svelte +4 -1
  30. package/dist/admin/client/form/form-submission/submission-field.svelte +10 -0
  31. package/dist/admin/client/index.d.ts +1 -0
  32. package/dist/admin/client/index.js +1 -0
  33. package/dist/admin/client/maintenance/maintenance-page.svelte +146 -2
  34. package/dist/admin/client/users/users-page.svelte +5 -6
  35. package/dist/admin/client/users/users-page.svelte.d.ts +1 -4
  36. package/dist/admin/components/fields/block-picker-modal.svelte +13 -4
  37. package/dist/admin/components/fields/blocks-field.svelte +40 -19
  38. package/dist/admin/components/fields/field-renderer.svelte +4 -8
  39. package/dist/admin/components/fields/object-field.svelte +7 -12
  40. package/dist/admin/components/fields/select-field.svelte +8 -2
  41. package/dist/admin/components/fields/seo-field.svelte +40 -93
  42. package/dist/admin/components/fields/simple-array-field.svelte +27 -16
  43. package/dist/admin/components/fields/text-field-wrapper.svelte +52 -197
  44. package/dist/admin/components/fields/text-field-wrapper.svelte.d.ts +2 -2
  45. package/dist/admin/components/fields/url-field-wrapper.svelte +15 -25
  46. package/dist/admin/components/fields/url-field.svelte +61 -72
  47. package/dist/admin/components/layout/layout-renderer.svelte +10 -4
  48. package/dist/admin/components/media/file-preview.svelte +10 -1
  49. package/dist/admin/components/media/file-upload.svelte +5 -1
  50. package/dist/admin/components/media/file-upload.svelte.d.ts +1 -0
  51. package/dist/admin/components/media/files-list.svelte +12 -3
  52. package/dist/admin/components/media/media-library.svelte +109 -37
  53. package/dist/admin/components/media/media-selector.svelte +90 -16
  54. package/dist/admin/components/media/tag-sidebar.svelte +10 -6
  55. package/dist/admin/components/media/tag-sidebar.svelte.d.ts +7 -2
  56. package/dist/admin/components/tiptap/FigureNodeView.svelte +15 -10
  57. package/dist/admin/components/tiptap/InlineBlockNodeView.svelte +53 -94
  58. package/dist/admin/components/tiptap/SlashCommandPopup.svelte +8 -3
  59. package/dist/admin/components/tiptap/editor-toolbar.svelte +28 -23
  60. package/dist/admin/components/tiptap/image-dialog.svelte +12 -7
  61. package/dist/admin/components/tiptap/inline-block-node.js +6 -5
  62. package/dist/admin/components/tiptap/lang.d.ts +77 -0
  63. package/dist/admin/components/tiptap/lang.js +170 -0
  64. package/dist/admin/components/tiptap/link-dialog.svelte +31 -28
  65. package/dist/admin/components/tiptap/slash-command.js +27 -23
  66. package/dist/admin/components/tiptap/table-dialog.svelte +9 -4
  67. package/dist/admin/components/tiptap/video-dialog.svelte +6 -1
  68. package/dist/admin/remote/email.remote.d.ts +1 -0
  69. package/dist/admin/remote/email.remote.js +5 -0
  70. package/dist/admin/remote/entry.remote.d.ts +2 -5
  71. package/dist/admin/remote/entry.remote.js +23 -28
  72. package/dist/admin/remote/index.d.ts +1 -0
  73. package/dist/admin/remote/index.js +1 -0
  74. package/dist/admin/remote/media.remote.d.ts +15 -0
  75. package/dist/admin/remote/media.remote.js +18 -2
  76. package/dist/admin/remote/preview.remote.js +3 -1
  77. package/dist/admin/utils/entryLabel.js +9 -6
  78. package/dist/admin/utils/translationStatus.js +1 -2
  79. package/dist/cli/scaffold/admin.js +34 -2
  80. package/dist/cms/runtime/api.d.ts +16 -12
  81. package/dist/cms/runtime/api.js +7 -6
  82. package/dist/cms/runtime/remote.js +2 -2
  83. package/dist/cms/runtime/schemas.d.ts +1 -1
  84. package/dist/cms/runtime/schemas.js +1 -1
  85. package/dist/cms/runtime/types.d.ts +118 -112
  86. package/dist/cms/runtime/types.js +0 -12
  87. package/dist/core/cms.d.ts +3 -1
  88. package/dist/core/cms.js +30 -0
  89. package/dist/core/fields/fieldSchemaToTs.js +9 -15
  90. package/dist/core/fields/formFieldSchemaToTs.js +7 -0
  91. package/dist/core/server/entries/operations/create.js +10 -4
  92. package/dist/core/server/entries/operations/get.d.ts +1 -0
  93. package/dist/core/server/entries/operations/get.js +186 -191
  94. package/dist/core/server/entries/operations/update.d.ts +6 -7
  95. package/dist/core/server/entries/operations/update.js +20 -38
  96. package/dist/core/server/fields/populateEntry.js +16 -52
  97. package/dist/core/server/fields/resolveImageFields.js +69 -120
  98. package/dist/core/server/fields/resolveRelationFields.js +30 -51
  99. package/dist/core/server/fields/resolveRichtextLinks.js +46 -100
  100. package/dist/core/server/fields/resolveTypographyOrphans.bench.d.ts +1 -0
  101. package/dist/core/server/fields/resolveTypographyOrphans.bench.js +87 -0
  102. package/dist/core/server/fields/resolveTypographyOrphans.d.ts +3 -0
  103. package/dist/core/server/fields/resolveTypographyOrphans.js +128 -0
  104. package/dist/core/server/fields/resolveUrlFields.js +47 -56
  105. package/dist/core/server/fields/utils/fixOrphans.d.ts +5 -0
  106. package/dist/core/server/fields/utils/fixOrphans.js +12 -0
  107. package/dist/core/server/fields/utils/imageStyles.d.ts +4 -2
  108. package/dist/core/server/fields/utils/imageStyles.js +41 -25
  109. package/dist/core/server/fields/utils/resolveMedia.js +1 -6
  110. package/dist/core/server/forms/submissions/operations/delete.js +26 -2
  111. package/dist/core/server/forms/submissions/utils/parseMultipart.d.ts +2 -0
  112. package/dist/core/server/forms/submissions/utils/parseMultipart.js +75 -0
  113. package/dist/core/server/generator/fields.d.ts +6 -0
  114. package/dist/core/server/generator/fields.js +43 -5
  115. package/dist/core/server/generator/formFieldSchemaToString.js +10 -0
  116. package/dist/core/server/generator/formFields.js +1 -0
  117. package/dist/core/server/generator/generator.js +98 -30
  118. package/dist/core/server/media/operations/getFiles.d.ts +5 -0
  119. package/dist/core/server/media/operations/getFiles.js +6 -0
  120. package/dist/core/server/media/operations/uploadPrivateFile.d.ts +4 -0
  121. package/dist/core/server/media/operations/uploadPrivateFile.js +8 -0
  122. package/dist/core/server/media/styles/operations/batchGenerateStyles.d.ts +16 -0
  123. package/dist/core/server/media/styles/operations/batchGenerateStyles.js +144 -0
  124. package/dist/db-postgres/index.js +303 -37
  125. package/dist/db-postgres/schema/entry.d.ts +0 -94
  126. package/dist/db-postgres/schema/entry.js +0 -6
  127. package/dist/db-postgres/schema/entryVersion.d.ts +17 -0
  128. package/dist/db-postgres/schema/entryVersion.js +1 -0
  129. package/dist/entity/index.d.ts +9 -4
  130. package/dist/entity/index.js +24 -24
  131. package/dist/files-local/index.js +43 -0
  132. package/dist/paraglide/messages/_index.d.ts +36 -3
  133. package/dist/paraglide/messages/_index.js +71 -3
  134. package/dist/paraglide/messages/en.d.ts +5 -0
  135. package/dist/paraglide/messages/en.js +14 -0
  136. package/dist/paraglide/messages/pl.d.ts +5 -0
  137. package/dist/paraglide/messages/pl.js +14 -0
  138. package/dist/sveltekit/components/preview.svelte +2 -326
  139. package/dist/sveltekit/components/preview.svelte.d.ts +5 -16
  140. package/dist/sveltekit/server/index.d.ts +2 -1
  141. package/dist/sveltekit/server/index.js +2 -1
  142. package/dist/sveltekit/server/preview.js +4 -7
  143. package/dist/types/adapters/db.d.ts +15 -1
  144. package/dist/types/adapters/files.d.ts +6 -0
  145. package/dist/types/cms.d.ts +5 -0
  146. package/dist/types/entries.d.ts +54 -18
  147. package/dist/types/fields.d.ts +14 -24
  148. package/dist/types/formFields.d.ts +7 -2
  149. package/dist/types/index.d.ts +2 -2
  150. package/dist/types/layout.d.ts +0 -1
  151. package/dist/types/structured-content.d.ts +5 -0
  152. package/dist/updates/0.10.0/index.d.ts +2 -0
  153. package/dist/updates/0.10.0/index.js +15 -0
  154. package/dist/updates/0.11.0/index.d.ts +2 -0
  155. package/dist/updates/0.11.0/index.js +12 -0
  156. package/dist/updates/0.12.0/index.d.ts +2 -0
  157. package/dist/updates/0.12.0/index.js +12 -0
  158. package/dist/updates/0.13.0/index.d.ts +2 -0
  159. package/dist/updates/0.13.0/index.js +10 -0
  160. package/dist/updates/0.13.1/index.d.ts +2 -0
  161. package/dist/updates/0.13.1/index.js +20 -0
  162. package/dist/updates/0.7.3/index.d.ts +2 -0
  163. package/dist/updates/0.7.3/index.js +10 -0
  164. package/dist/updates/0.8.0/index.d.ts +2 -0
  165. package/dist/updates/0.8.0/index.js +18 -0
  166. package/dist/updates/0.8.0/migrate.d.ts +2 -0
  167. package/dist/updates/0.8.0/migrate.js +101 -0
  168. package/dist/updates/0.9.0/index.d.ts +2 -0
  169. package/dist/updates/0.9.0/index.js +38 -0
  170. package/dist/updates/index.js +9 -1
  171. package/package.json +7 -6
  172. package/dist/admin/components/fields/image-field.svelte +0 -198
  173. package/dist/admin/components/fields/image-field.svelte.d.ts +0 -8
  174. package/dist/admin/components/fields/richtext-field.svelte +0 -13
  175. package/dist/admin/components/fields/richtext-field.svelte.d.ts +0 -8
  176. package/dist/admin/components/tiptap.svelte +0 -11
  177. package/dist/admin/components/tiptap.svelte.d.ts +0 -6
  178. package/dist/core/server/entries/utils/getEntryTranslation.d.ts +0 -1
  179. package/dist/core/server/entries/utils/getEntryTranslation.js +0 -18
  180. package/dist/paraglide/messages/hello_world.d.ts +0 -5
  181. package/dist/paraglide/messages/hello_world.js +0 -33
  182. package/dist/paraglide/messages/login_hello.d.ts +0 -16
  183. package/dist/paraglide/messages/login_hello.js +0 -34
  184. package/dist/paraglide/messages/login_please_login.d.ts +0 -16
  185. 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, extractMediaIds as extractMediaIdsFromDoc, cloneDoc, walkLinkMarks, walkInlineBlockNodes } from '../../../admin/components/tiptap/structured-content-utils.js';
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 field is localized: { lang: StructuredContentDoc, ... }
78
- if (typeof val === 'object' && val !== null) {
79
- for (const doc of Object.values(val)) {
80
- if (doc && typeof doc === 'object' && doc.type === 'doc') {
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.data, field.fields);
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.slug);
60
+ const objectDef = field.of.find((objDef) => objDef.slug === item._slug);
95
61
  if (objectDef) {
96
- collectIds(item.data, objectDef.fields);
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 to find their publishedVersionId
76
+ // Get entries and find their published versions for the target language
111
77
  const entries = await db.getEntries({ ids: uniqueIds });
112
- const publishedVersionIds = entries
113
- .filter((e) => e.publishedVersionId != null && e.publishedAt != null && e.publishedAt <= new Date())
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
- ids: publishedVersionIds
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 versions) {
105
+ for (const [entryId, version] of publishedByEntry) {
128
106
  const rawData = version.data;
129
- const configSlug = entryConfigSlugMap[version.entryId];
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
- const configSlug = entryConfigSlugMap[version.entryId];
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
- if (typeof val === 'object' && val !== null) {
166
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
167
- const resolved = {};
168
- for (const [lang, doc] of Object.entries(val)) {
169
- if (doc && typeof doc === 'object' && doc.type === 'doc') {
170
- const cloned = resolveContentDoc(doc, slugMap);
171
- // Resolve inline block fields
172
- if (contentField.inlineBlocks?.length) {
173
- walkInlineBlockNodes(cloned, (node) => {
174
- const bd = node.attrs?.blockData;
175
- if (!bd || typeof bd !== 'object')
176
- return;
177
- const def = contentField.inlineBlocks.find((b) => b.slug === node.attrs?.blockType);
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] = resolved;
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.slug);
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,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,3 @@
1
+ import type { PopulatedEntryData } from '../../../types/entries.js';
2
+ import type { Field } from '../../../types/fields.js';
3
+ export declare function resolveTypographyOrphans(data: PopulatedEntryData, fields: Field[]): PopulatedEntryData;
@@ -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.data, field.fields);
62
+ collectIds(val, field.fields);
63
63
  break;
64
64
  case 'blocks':
65
65
  if (Array.isArray(val)) {
66
66
  val.forEach((item) => {
67
- // Find the object definition for this item based on its slug
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.data, objectDef.fields);
69
+ collectIds(item, objectDef.fields);
71
70
  }
72
71
  });
73
72
  }
74
73
  break;
75
74
  case 'content': {
76
75
  const cf = field;
77
- if (typeof val === 'object' && val !== null && cf.inlineBlocks?.length) {
78
- for (const doc of Object.values(val)) {
79
- if (doc && typeof doc === 'object' && doc.type === 'doc') {
80
- walkInlineBlockNodes(doc, (node) => {
81
- const bd = node.attrs?.blockData;
82
- if (!bd || typeof bd !== 'object')
83
- return;
84
- const def = cf.inlineBlocks.find((b) => b.slug === node.attrs?.blockType);
85
- if (def)
86
- collectIds(bd, def.fields);
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 publishedVersionIds = dbEntries
112
- .map((e) => e.publishedVersionId)
113
- .filter((id) => id != null);
114
- if (publishedVersionIds.length > 0) {
115
- const versions = await getDbEntryVersions({ ids: publishedVersionIds });
116
- const versionByEntryId = new Map(versions.map((v) => [v.entryId, v]));
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
- // Find the object definition for this item based on its slug
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
- if (typeof val === 'object' && val !== null && cf.inlineBlocks?.length) {
204
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
205
- const resolved = {};
206
- for (const [lang, doc] of Object.entries(val)) {
207
- if (doc && typeof doc === 'object' && doc.type === 'doc') {
208
- const cloned = cloneDoc(doc);
209
- walkInlineBlockNodes(cloned, (node) => {
210
- const bd = node.attrs?.blockData;
211
- if (!bd || typeof bd !== 'object')
212
- return;
213
- const def = cf.inlineBlocks.find((b) => b.slug === node.attrs?.blockType);
214
- if (!def)
215
- return;
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,5 @@
1
+ /**
2
+ * Replace regular space after single-letter Polish conjunctions with non-breaking space.
3
+ * Idempotent — already-fixed text (with \u00A0) is not modified.
4
+ */
5
+ export declare function fixOrphans(text: string): string;
@@ -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 { ImageField, ImageFieldStyle } from '../../../../types/fields.js';
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: ImageField, val: MediaFile): Promise<{
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
  }>;