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.
Files changed (2) hide show
  1. package/index.mjs +82 -8
  2. 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 HEADING_RE = /<h[1-3][^>]*>([\s\S]*?)<\/h[1-3]>/g;
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 before = content.slice(Math.max(0, m.index - 700), m.index);
313
- const headM = [...before.matchAll(HEADING_RE)].pop();
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: headM ? stripMarkup(headM[1]) : "",
320
- context: stripMarkup(before).slice(-250),
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 before = content.slice(Math.max(0, m.index - 700), m.index);
1577
- const headM = [...before.matchAll(HEADING_RE)].pop();
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: headM ? stripMarkup(headM[1]) : "",
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.0",
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": {