@uniweb/build 1.8.2 → 1.8.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniweb/build",
3
- "version": "1.8.2",
3
+ "version": "1.8.4",
4
4
  "description": "Build tooling for the Uniweb Component Web Platform",
5
5
  "type": "module",
6
6
  "exports": {
@@ -56,15 +56,15 @@
56
56
  "js-yaml": "^4.1.0",
57
57
  "sharp": "^0.35.3",
58
58
  "yaml": "^2.5.0",
59
- "@uniweb/content-reader": "~1.5.4",
60
- "@uniweb/projections": "~1.1.7",
59
+ "@uniweb/content-reader": "~1.5.6",
60
+ "@uniweb/semantic-parser": "~1.13.0",
61
61
  "@uniweb/schemas": "~1.6.5",
62
- "@uniweb/semantic-parser": "~1.12.2",
63
- "@uniweb/theming": "~1.2.1",
64
- "@uniweb/content-writer": "~1.3.0"
62
+ "@uniweb/content-writer": "~1.3.0",
63
+ "@uniweb/projections": "~1.1.7",
64
+ "@uniweb/theming": "~1.2.1"
65
65
  },
66
66
  "optionalDependencies": {
67
- "@uniweb/runtime": "~1.1.16"
67
+ "@uniweb/runtime": "~1.1.18"
68
68
  },
69
69
  "peerDependencies": {
70
70
  "vite": "^5.0.0 || ^6.0.0 || ^7.0.0",
@@ -73,8 +73,8 @@
73
73
  "@tailwindcss/vite": "^4.0.0",
74
74
  "@vitejs/plugin-react": "^4.0.0 || ^5.0.0",
75
75
  "vite-plugin-svgr": "^4.0.0",
76
- "@uniweb/core": "~1.4.1",
77
- "@uniweb/runtime": "~1.1.16"
76
+ "@uniweb/runtime": "~1.1.18",
77
+ "@uniweb/core": "~1.4.3"
78
78
  },
79
79
  "peerDependenciesMeta": {
80
80
  "vite": {
package/src/i18n/merge.js CHANGED
@@ -17,19 +17,16 @@ import { loadFreeformTranslation } from './freeform.js'
17
17
  import { elementText, blockElements, translationContext, SITE_META_CONTEXT } from './extract.js'
18
18
  import { visitDataBlockStrings } from './data-strings.js'
19
19
 
20
- // Inline-markdown → ProseMirror inline fragment, for resolving a whole-element
20
+ // Inline markdown → ProseMirror inline nodes, for resolving a whole-element
21
21
  // translation VALUE (which carries marks/links/icons as inline markdown). Same
22
22
  // lazy-import-with-fallback pattern as freeform.js / collection-processor.js, so
23
23
  // the synchronous merge path has the converter ready at call time.
24
- let markdownToProseMirror
24
+ let inlineMarkdownToProseMirror
25
25
  try {
26
26
  const contentReader = await import('@uniweb/content-reader')
27
- markdownToProseMirror = contentReader.markdownToProseMirror
27
+ inlineMarkdownToProseMirror = contentReader.inlineMarkdownToProseMirror
28
28
  } catch {
29
- markdownToProseMirror = (markdown) => ({
30
- type: 'doc',
31
- content: [{ type: 'paragraph', content: [{ type: 'text', text: markdown.trim() }] }]
32
- })
29
+ inlineMarkdownToProseMirror = (markdown) => [{ type: 'text', text: markdown.trim() }]
33
30
  }
34
31
 
35
32
  /**
@@ -183,9 +180,6 @@ async function mergeTranslationsAsync(siteContent, translations, options) {
183
180
  return translated
184
181
  }
185
182
 
186
- // The blocks that hold words, which `toPlainText` reads as one run each.
187
- const TEXT_BLOCKS = new Set(['paragraph', 'heading', 'codeBlock'])
188
-
189
183
  /**
190
184
  * ⭐ A TRANSLATION VALUE IS INLINE MARKDOWN, AND A PLAIN-TEXT SLOT RENDERS ITS TEXT.
191
185
  *
@@ -221,17 +215,19 @@ export function toPlainText(value) {
221
215
  return value
222
216
  }
223
217
  try {
224
- const doc = markdownToProseMirror(value)
225
- // A block's words run together; blocks are apart. ⛔ Until 2026-10-16 every level joined with
226
- // nothing, so a value of two paragraphs read as one word at the seam ("…17hFermé…").
218
+ // Each paragraph's words, read as inline markdown and never as blocks, so `1. *Introduction*`
219
+ // keeps its number; paragraphs are apart. ⛔ Until 2026-10-16 every level joined with nothing,
220
+ // so a value of two paragraphs read as one word at the seam ("…17hFermé…"). ⛔ Until 2026-10-24
221
+ // the value was read as a document, and one that began like a block lost its marker: an ordered
222
+ // list's number, a bullet, a `#`.
227
223
  const inline = (nodes) =>
228
224
  (nodes || []).map((n) => (n.type === 'text' ? n.text || '' : n.type === 'hardBreak' ? ' ' : inline(n.content))).join('')
229
- const blocks = (nodes) =>
230
- (nodes || [])
231
- .map((n) => (TEXT_BLOCKS.has(n.type) ? inline(n.content) : n.type === 'text' ? n.text || '' : blocks(n.content)))
232
- .filter((t) => t.trim())
233
- .join(' ')
234
- const text = blocks(doc?.content).trim()
225
+ const text = value
226
+ .split(/\n[ \t]*\n/)
227
+ .map((paragraph) => inline(inlineMarkdownToProseMirror(paragraph.trim())))
228
+ .filter((t) => t.trim())
229
+ .join(' ')
230
+ .trim()
235
231
  return text || value
236
232
  } catch {
237
233
  return value
@@ -639,17 +635,15 @@ export function resolveDocForLocale(sourceDoc, table, context = { page: '', sect
639
635
  return elements || data ? doc : null
640
636
  }
641
637
 
642
- // Parse an inline-markdown translation value into a ProseMirror inline fragment.
643
- // A value is expected to be one element's inline content (one paragraph once
644
- // parsed); take that paragraph's inline children. If the converter yields
645
- // several blocks, flatten their inline content rather than drop any.
638
+ // Parse an inline-markdown translation value into a ProseMirror inline fragment: one element's
639
+ // words, read as inline markdown and never as blocks (`inlineMarkdownToProseMirror`).
640
+ // ⛔ Until 2026-10-24 it was read as a document and each block's content taken, so a value that
641
+ // began like a block — `1. Informations que nous collectons`, the French of a numbered heading —
642
+ // was an ordered list, and the heading was handed its list item. It rendered empty, and a pull
643
+ // refused the site, since no file can say a heading holds a list item.
646
644
  function inlineMarkdownToFragment(value) {
647
645
  if (typeof value !== 'string' || value.trim() === '') return null
648
- const doc = markdownToProseMirror(value)
649
- const inline = []
650
- for (const block of doc?.content || []) {
651
- if (Array.isArray(block.content)) inline.push(...block.content)
652
- }
646
+ const inline = inlineMarkdownToProseMirror(value.trim())
653
647
  return inline.length ? inline : [{ type: 'text', text: value.trim() }]
654
648
  }
655
649
 
@@ -241,6 +241,22 @@ function detectVersions(folderNames) {
241
241
  return versions
242
242
  }
243
243
 
244
+ /**
245
+ * An id an author wrote — a page's `id:` in `page.yml`, a section's in its frontmatter — as the
246
+ * string it names. YAML reads `id: 404` as a number, and an id is never one: a `page:404` link
247
+ * names the string, and a store holds a stable id as text.
248
+ *
249
+ * ⛔ *Until 2026-10-24 the parsed value was kept, so a page with `id: 404` matched no `page:`
250
+ * link, and a push sent its stable id as a number, which a backend refuses (measured that day:
251
+ * HTTP 400, "item.data.stable_id must be a string").*
252
+ *
253
+ * @param {*} value
254
+ * @returns {string|null} null for an id not written, or written empty
255
+ */
256
+ export function authoredId(value) {
257
+ return value === undefined || value === null || value === '' ? null : String(value)
258
+ }
259
+
244
260
  /**
245
261
  * `query:` — the shorthand for `fetch: { query }`, at every level that declares
246
262
  * data: a section's frontmatter, `page.yml`, `folder.yml` and `site.yml`, on the
@@ -1168,7 +1184,7 @@ async function processMarkdownFile(filePath, id, siteRoot, defaultStableId = nul
1168
1184
 
1169
1185
  // Stable ID for scroll targeting: frontmatter id > filename-derived > null
1170
1186
  // This ID is stable across reordering (unlike the positional id)
1171
- const stableId = frontmatterId || defaultStableId || null
1187
+ const stableId = authoredId(frontmatterId) || defaultStableId || null
1172
1188
 
1173
1189
  const section = {
1174
1190
  id,
@@ -1434,7 +1450,7 @@ async function processPage(pagePath, pageName, siteRoot, { isIndex = false, pare
1434
1450
  page: {
1435
1451
  route,
1436
1452
  sourcePath: parentRoute === '/' ? `/${pageName}` : `${parentRoute}/${pageName}`,
1437
- id: pageConfig.id || null,
1453
+ id: authoredId(pageConfig.id),
1438
1454
  isIndex,
1439
1455
  title: resolveDisplayTitle(pageConfig.title, pageName, null),
1440
1456
  description: pageConfig.description || '',
@@ -1460,7 +1476,7 @@ async function processPage(pagePath, pageName, siteRoot, { isIndex = false, pare
1460
1476
  page: {
1461
1477
  route,
1462
1478
  sourcePath: parentRoute === '/' ? `/${pageName}` : `${parentRoute}/${pageName}`,
1463
- id: pageConfig.id || null,
1479
+ id: authoredId(pageConfig.id),
1464
1480
  isIndex,
1465
1481
  title: resolveDisplayTitle(pageConfig.title, pageName, null),
1466
1482
  description: pageConfig.description || '',
@@ -1731,7 +1747,7 @@ async function processPage(pagePath, pageName, siteRoot, { isIndex = false, pare
1731
1747
  page: {
1732
1748
  route,
1733
1749
  sourcePath, // Original folder-based path (for ancestor checking in navigation)
1734
- id: pageConfig.id || null, // Stable page ID for page: links (survives reorganization)
1750
+ id: authoredId(pageConfig.id), // Stable page ID for page: links (survives reorganization)
1735
1751
  isIndex, // Marks this page as the index for its parent route
1736
1752
  title: resolveDisplayTitle(pageConfig.title, pageName, hierarchicalSections),
1737
1753
  description: pageConfig.description || '',
package/src/uwx/site.js CHANGED
@@ -71,6 +71,7 @@ import {
71
71
  rootOrderConfig,
72
72
  composeLocalizedRoute,
73
73
  isValidSlugSegment,
74
+ authoredId,
74
75
  } from '../site/content-collector.js'
75
76
  import { refuseBinding, refuseUnder, refuseOutsideLanguage, warnDuplicateBindings } from '../site/data-fetcher.js'
76
77
  import { readLayoutFolder, layoutAreaRoute } from '../site/layout-folder.js'
@@ -312,7 +313,8 @@ function buildPageData(config, ctx) {
312
313
  // `localizeRouteSlugs`, once the whole tree is known. `mode` is the plain
313
314
  // delivery mode.
314
315
  const data = { slug: { [sourceLocale]: slug }, mode } // both required by the entity type
315
- setIf(data, 'stable_id', config.id)
316
+ // An authored id is a string (`authoredId`): `id: 404` reads as a number, which a store refuses.
317
+ setIf(data, 'stable_id', authoredId(config.id) ?? undefined)
316
318
  setIf(data, 'title', localizeScalar(config.title, sourceLocale, translations))
317
319
  setIf(data, 'description', localizeScalar(config.description, sourceLocale, translations))
318
320
  setIf(data, 'label', localizeScalar(config.label, sourceLocale, translations))