distribea-mcp 1.3.0 → 1.3.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/index.mjs +82 -8
- package/package.json +1 -1
package/index.mjs
CHANGED
|
@@ -229,7 +229,75 @@ const SKIP_DIRS = new Set([
|
|
|
229
229
|
const PLACEHOLDER_SRC_RE =
|
|
230
230
|
/placehold\.co|via\.placeholder\.com|placekitten|picsum\.photos|dummyimage\.com|loremflickr\.com|placeimg\.com|fakeimg\.pl|images\.unsplash\.com|source\.unsplash\.com|images\.pexels\.com|cdn\.pixabay\.com|placeholder/i;
|
|
231
231
|
const IMG_TAG_RE = /<(?:img|Image)\b[\s\S]*?>/g;
|
|
232
|
-
const
|
|
232
|
+
const HEADING_SCAN_RE = /<h[1-4][^>]*>([\s\S]*?)<\/h[1-4]>/gi;
|
|
233
|
+
const NEXT_HEADING_RE = /<h[1-4][^>]*>/i;
|
|
234
|
+
// Frontières de bloc : on ne relie JAMAIS une image au titre d'une autre section.
|
|
235
|
+
const BLOCK_BOUNDARY_RE =
|
|
236
|
+
/<\/?(?:section|article|header|footer|main|nav|aside)\b[^>]*>/gi;
|
|
237
|
+
|
|
238
|
+
// Le « bloc » qui contient l'image : entre la frontière de section juste avant
|
|
239
|
+
// et juste après. Page mal codée (aucune balise de section) → fenêtre bornée
|
|
240
|
+
// pour ne pas aller chercher un titre à l'autre bout du fichier.
|
|
241
|
+
function sectionBoundsForImg(content, imgStart, imgEnd) {
|
|
242
|
+
let lo = 0;
|
|
243
|
+
let hi = content.length;
|
|
244
|
+
for (const b of content.matchAll(BLOCK_BOUNDARY_RE)) {
|
|
245
|
+
if (b.index < imgStart) {
|
|
246
|
+
lo = b.index + b[0].length;
|
|
247
|
+
} else if (b.index >= imgEnd) {
|
|
248
|
+
hi = b.index;
|
|
249
|
+
break;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
if (lo === 0 && hi === content.length) {
|
|
253
|
+
lo = Math.max(0, imgStart - 1200);
|
|
254
|
+
hi = Math.min(content.length, imgEnd + 1200);
|
|
255
|
+
}
|
|
256
|
+
return { lo, hi };
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Titre + description du MÊME bloc que l'image : on prend le titre le plus
|
|
260
|
+
// proche, qu'il soit AU-DESSUS ou EN DESSOUS (cartes image-en-haut/titre-dessous,
|
|
261
|
+
// blocs 2 colonnes…), sans déborder sur la carte voisine. Aucun titre trouvé →
|
|
262
|
+
// on retombe sur le texte juste avant l'image.
|
|
263
|
+
function headingAndContextForImg(content, imgStart, imgEnd) {
|
|
264
|
+
const { lo, hi } = sectionBoundsForImg(content, imgStart, imgEnd);
|
|
265
|
+
const scope = content.slice(lo, hi);
|
|
266
|
+
const relStart = imgStart - lo;
|
|
267
|
+
const relEnd = imgEnd - lo;
|
|
268
|
+
let best = null;
|
|
269
|
+
let bestDist = Number.POSITIVE_INFINITY;
|
|
270
|
+
for (const h of scope.matchAll(HEADING_SCAN_RE)) {
|
|
271
|
+
const hStart = h.index;
|
|
272
|
+
const hEnd = h.index + h[0].length;
|
|
273
|
+
let dist = 0;
|
|
274
|
+
if (hEnd <= relStart) {
|
|
275
|
+
dist = relStart - hEnd;
|
|
276
|
+
} else if (hStart >= relEnd) {
|
|
277
|
+
dist = hStart - relEnd;
|
|
278
|
+
}
|
|
279
|
+
if (dist < bestDist) {
|
|
280
|
+
bestDist = dist;
|
|
281
|
+
best = { text: stripMarkup(h[1]), hEnd };
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
if (!best) {
|
|
285
|
+
return {
|
|
286
|
+
heading: "",
|
|
287
|
+
context: stripMarkup(
|
|
288
|
+
content.slice(Math.max(0, imgStart - 300), imgStart)
|
|
289
|
+
).slice(-250),
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
// Description = le texte qui suit CE titre, borné au titre suivant (pour ne
|
|
293
|
+
// pas avaler la carte d'après).
|
|
294
|
+
const afterHead = scope.slice(best.hEnd);
|
|
295
|
+
const nextHead = afterHead.search(NEXT_HEADING_RE);
|
|
296
|
+
const desc = stripMarkup(
|
|
297
|
+
nextHead >= 0 ? afterHead.slice(0, nextHead) : afterHead
|
|
298
|
+
).slice(0, 280);
|
|
299
|
+
return { heading: best.text, context: desc };
|
|
300
|
+
}
|
|
233
301
|
|
|
234
302
|
function walkFiles(dir, out = []) {
|
|
235
303
|
for (const name of readdirSync(dir)) {
|
|
@@ -309,15 +377,18 @@ function scanFileForSlots(file, content) {
|
|
|
309
377
|
h = Number(dimM[2]);
|
|
310
378
|
}
|
|
311
379
|
}
|
|
312
|
-
const
|
|
313
|
-
|
|
380
|
+
const { heading, context } = headingAndContextForImg(
|
|
381
|
+
content,
|
|
382
|
+
m.index,
|
|
383
|
+
m.index + tag.length
|
|
384
|
+
);
|
|
314
385
|
slots.push({
|
|
315
386
|
file,
|
|
316
387
|
tag,
|
|
317
388
|
src: srcM[2],
|
|
318
389
|
alt: altM?.[2] ?? "",
|
|
319
|
-
heading
|
|
320
|
-
context
|
|
390
|
+
heading,
|
|
391
|
+
context,
|
|
321
392
|
// Le prénom de l'auteur d'un avis est presque toujours SOUS sa photo.
|
|
322
393
|
after: stripMarkup(
|
|
323
394
|
content.slice(m.index + tag.length, m.index + tag.length + 500)
|
|
@@ -1573,13 +1644,16 @@ function scanRebrandCandidates(projectDir) {
|
|
|
1573
1644
|
continue;
|
|
1574
1645
|
}
|
|
1575
1646
|
const altM = tag.match(/\balt\s*=\s*\{?\s*(["'])([\s\S]*?)\1/);
|
|
1576
|
-
const
|
|
1577
|
-
|
|
1647
|
+
const { heading } = headingAndContextForImg(
|
|
1648
|
+
content,
|
|
1649
|
+
m.index,
|
|
1650
|
+
m.index + tag.length
|
|
1651
|
+
);
|
|
1578
1652
|
const entry = seen.get(resolved) ?? {
|
|
1579
1653
|
path: resolved,
|
|
1580
1654
|
usedIn: new Set(),
|
|
1581
1655
|
alt: altM?.[2] ?? "",
|
|
1582
|
-
heading
|
|
1656
|
+
heading,
|
|
1583
1657
|
};
|
|
1584
1658
|
entry.usedIn.add(relative(projectDir, file));
|
|
1585
1659
|
seen.set(resolved, entry);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "distribea-mcp",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.1",
|
|
4
4
|
"description": "Distribea MCP, on-brand website imagery (style-locked, recurring characters, UGC review avatars, blog covers) generated by the hosted Distribea engine. Requires a Distribea subscription key.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|