docs(BlockEditor): document mediaSlug media library link and add server helpers

- update image block schema in README table to include `mediaSlug?` and clarify fields
- add "Liaison avec la médiathèque" section documenting mediaSlug behavior, read-only alt/caption, and internal `_` fields
- document new server helpers (`normalizeImageBlocks`, `enrichBlocksWithMedia`, `syncBlockImageReferences`) with usage examples
- add `block_image` field convention to media feature README with cross-references
- implement `mediaLink.server.js` with the three server-side helpers
- store `mediaSlug` on image block at insertion time in `Image.client.js`
- persist `mediaSlug` through clipboard paste/duplicate in `clipboard.js`
- export `mediaLink` entry point in `package.json` exports map
This commit is contained in:
2026-04-26 20:22:22 -04:00
parent 8c5c3baec4
commit 31d0359163
6 changed files with 501 additions and 43 deletions
@@ -15,6 +15,16 @@ import { inlineFromText, inlineToPlainText, normalize } from './types.js';
const HEADING_RE = /^heading_([1-6])$/;
const BLOCK_TAG_RE = /^(P|H[1-6]|UL|OL|BLOCKQUOTE|PRE|HR|FIGURE|DIV|TABLE)$/;
const MEDIA_FILE_URL_RE = /^\/zen\/api\/media\/file\/([^/?#]+)$/;
// Quand une image collée pointe sur la médiathèque interne, on dérive le
// slug pour reconstituer le lien `mediaSlug` (un copier-coller ne traverse
// pas les champs cachés). Pour les URL externes, retourne null.
function imageSlugFromSrc(src) {
if (typeof src !== 'string') return null;
const m = MEDIA_FILE_URL_RE.exec(src);
return m ? m[1] : null;
}
// Block[] → HTML string. Regroupe les listes consécutives sous un seul
// <ul>/<ol>. Les blocs inconnus deviennent un <p> au texte aplati.
@@ -98,9 +108,15 @@ function blockToElement(block) {
: align === 'left' ? 'text-align:left'
: 'text-align:right');
}
// Quand le bloc est lié à un média (`mediaSlug`), alt/caption sont
// résolus côté serveur (`_resolvedAlt` / `_resolvedCaption`) ou
// captés à l'insertion (`_mediaAlt` / `_mediaCaption`). Pour les URL
// externes, on retombe sur les champs locaux du bloc.
const altText = block._resolvedAlt ?? block._mediaAlt ?? block.alt ?? '';
const captionText = block._resolvedCaption ?? block._mediaCaption ?? block.caption ?? '';
const img = document.createElement('img');
img.setAttribute('src', block.src || '');
if (block.alt) img.setAttribute('alt', block.alt);
if (altText) img.setAttribute('alt', altText);
if (align === 'full') img.setAttribute('width', '100%');
let imgHost = img;
if (block.href) {
@@ -114,9 +130,9 @@ function blockToElement(block) {
imgHost = a;
}
fig.appendChild(imgHost);
if (block.caption) {
if (captionText) {
const cap = document.createElement('figcaption');
cap.textContent = block.caption;
cap.textContent = captionText;
fig.appendChild(cap);
}
return fig;
@@ -133,7 +149,11 @@ export function blocksToPlainText(blocks) {
return blocks
.map(b => {
if (b.type === 'divider') return '---';
if (b.type === 'image') return b.alt || b.caption || '';
if (b.type === 'image') {
const alt = b._resolvedAlt ?? b._mediaAlt ?? b.alt ?? '';
const cap = b._resolvedCaption ?? b._mediaCaption ?? b.caption ?? '';
return alt || cap || '';
}
return inlineToPlainText(b.content ?? []);
})
.join('\n');
@@ -296,15 +316,21 @@ function parseChildren(node, out) {
const align = ['left', 'center', 'right', 'full'].includes(dataAlign) ? dataAlign : 'center';
const href = linkEl?.getAttribute('href') || '';
const newTab = !!href && linkEl?.getAttribute('target') === '_blank';
const src = img.getAttribute('src') || '';
const slug = imageSlugFromSrc(src);
const altText = img.getAttribute('alt') || '';
const captionText = cap?.textContent?.trim() || '';
out.push({
id: newBlockId(),
type: 'image',
src: img.getAttribute('src') || '',
alt: img.getAttribute('alt') || '',
caption: cap?.textContent?.trim() || '',
src,
mediaSlug: slug || '',
alt: slug ? '' : altText,
caption: slug ? '' : captionText,
align,
href,
newTab,
...(slug ? { _mediaAlt: altText, _mediaCaption: captionText } : null),
});
}
continue;
@@ -312,15 +338,20 @@ function parseChildren(node, out) {
if (tag === 'IMG') {
flush();
const src = child.getAttribute('src') || '';
const slug = imageSlugFromSrc(src);
const altText = child.getAttribute('alt') || '';
out.push({
id: newBlockId(),
type: 'image',
src: child.getAttribute('src') || '',
alt: child.getAttribute('alt') || '',
src,
mediaSlug: slug || '',
alt: slug ? '' : altText,
caption: '',
align: 'center',
href: '',
newTab: false,
...(slug ? { _mediaAlt: altText } : null),
});
continue;
}