@neatico/visual-core 0.14.3 → 0.14.26

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.
@@ -5,37 +5,93 @@ exports.needsEditorialPagination = needsEditorialPagination;
5
5
  exports.ensurePresentationBookends = ensurePresentationBookends;
6
6
  const content_js_1 = require("./content.js");
7
7
  /** Reading density, not just geometric fit. CJK occupies roughly twice Latin's width. */
8
- const readingUnits = (text) => Array.from(text).reduce((n, c) => n + (/\s/.test(c) ? 0.2 : /[\u2e80-\u9fff\uac00-\ud7af\u3040-\u30ff]/u.test(c) ? 1 : 0.52), 0);
8
+ const readingUnits = (text) => Array.from(text).reduce((n, c) => n +
9
+ (/\s/.test(c)
10
+ ? 0.2
11
+ : /[\u2e80-\u9fff\uac00-\ud7af\u3040-\u30ff]/u.test(c)
12
+ ? 1
13
+ : 0.52), 0);
9
14
  exports.readingUnits = readingUnits;
10
15
  function needsEditorialPagination(page) {
11
- if (page.scene === 'brand-collection')
16
+ if (page.scene === "brand-collection")
12
17
  return false;
13
- const entries = page.blocks.flatMap(b => b.kind === 'features' || b.kind === 'steps' ? b.entries : []);
14
- if (entries.length >= 3 && entries.some(e => (0, exports.readingUnits)(e.description) > 65))
18
+ const entries = page.blocks.flatMap((b) => b.kind === "features" || b.kind === "steps" ? b.entries : []);
19
+ if (entries.length >= 3 &&
20
+ entries.some((e) => (0, exports.readingUnits)(e.description) > 65))
15
21
  return true;
16
22
  const total = page.blocks.reduce((n, b) => n + (0, content_js_1.contentBlockText)(b).reduce((m, s) => m + (0, exports.readingUnits)(s), 0), 0);
17
- return total > 250 || page.blocks.some(b => b.kind === 'paragraph' ? (0, exports.readingUnits)(b.text) > 165 : (b.kind === 'features' || b.kind === 'steps') && (b.entries.some(e => (0, exports.readingUnits)(e.description) > 135) || (b.entries.length >= 3 && b.entries.some(e => (0, exports.readingUnits)(e.description) > 65))));
23
+ return (total > 250 ||
24
+ page.blocks.some((b) => b.kind === "paragraph"
25
+ ? (0, exports.readingUnits)(b.text) > 165
26
+ : (b.kind === "features" || b.kind === "steps") &&
27
+ (b.entries.some((e) => (0, exports.readingUnits)(e.description) > 135) ||
28
+ (b.entries.length >= 3 &&
29
+ b.entries.some((e) => (0, exports.readingUnits)(e.description) > 65)))));
18
30
  }
19
- const endings = { zh: '感谢聆听', en: 'Thank you', de: 'Vielen Dank', es: 'Gracias', fr: 'Merci', ja: 'ご清聴ありがとうございました', pt: 'Obrigado' };
31
+ const endings = {
32
+ zh: "感谢聆听",
33
+ en: "Thank you",
34
+ de: "Vielen Dank",
35
+ es: "Gracias",
36
+ fr: "Merci",
37
+ ja: "ご清聴ありがとうございました",
38
+ pt: "Obrigado",
39
+ };
20
40
  /** Structural bookends contain no invented customer claims or calls to action. */
21
41
  function ensurePresentationBookends(plan) {
22
- if (plan.kind !== 'presentation' || !plan.pages.length)
42
+ if (plan.kind !== "presentation" || !plan.pages.length)
23
43
  return plan;
24
- const text = [plan.title, ...plan.pages.map(p => p.title)].join(' ');
25
- const language = /[\u3040-\u30ff]/.test(text) ? 'ja' : /[\u3400-\u9fff]/.test(text) ? 'zh' : /\b(und|für|Einführung)\b/i.test(text) ? 'de' : /\b(pour|présentation|avec)\b/i.test(text) ? 'fr' : /\b(para|apresentação|com)\b/i.test(text) ? 'pt' : /\b(presentación|con|gracias)\b/i.test(text) ? 'es' : 'en';
26
- const pages = plan.pages.map(p => ({ ...p, blocks: [...p.blocks] }));
27
- const used = new Set(pages.flatMap(p => [p.id, ...p.blocks.map(b => b.id)]));
28
- const id = (prefix) => { let s = prefix, i = 0; while (used.has(s))
29
- s = `${prefix}-${++i}`; used.add(s); return s; };
30
- const bookend = (purpose) => ({ id: id(`deck-${purpose}`), title: purpose === 'cover' ? plan.title : endings[language], purpose, scene: purpose === 'cover' ? 'hero' : 'closing', evidence: pages[0].evidence, blocks: [{ id: id(`deck-${purpose}-caption`), kind: 'paragraph', text: plan.title, evidence: pages[0].evidence }] });
31
- if (pages[0].purpose !== 'cover')
32
- pages.unshift(bookend('cover'));
44
+ const text = [plan.title, ...plan.pages.map((p) => p.title)].join(" ");
45
+ const language = /[\u3040-\u30ff]/.test(text)
46
+ ? "ja"
47
+ : /[\u3400-\u9fff]/.test(text)
48
+ ? "zh"
49
+ : /\b(und|für|Einführung)\b/i.test(text)
50
+ ? "de"
51
+ : /\b(pour|présentation|avec)\b/i.test(text)
52
+ ? "fr"
53
+ : /\b(para|apresentação|com)\b/i.test(text)
54
+ ? "pt"
55
+ : /\b(presentación|con|gracias)\b/i.test(text)
56
+ ? "es"
57
+ : "en";
58
+ const pages = plan.pages.map((p) => ({ ...p, blocks: [...p.blocks] }));
59
+ const used = new Set(pages.flatMap((p) => [p.id, ...p.blocks.map((b) => b.id)]));
60
+ const id = (prefix) => {
61
+ let s = prefix, i = 0;
62
+ while (used.has(s))
63
+ s = `${prefix}-${++i}`;
64
+ used.add(s);
65
+ return s;
66
+ };
67
+ const bookend = (purpose) => ({
68
+ id: id(`deck-${purpose}`),
69
+ title: purpose === "cover" ? plan.title : endings[language],
70
+ purpose,
71
+ scene: purpose === "cover" ? "hero" : "closing",
72
+ evidence: pages[0].evidence,
73
+ blocks: [
74
+ {
75
+ id: id(`deck-${purpose}-caption`),
76
+ kind: "paragraph",
77
+ text: plan.title,
78
+ evidence: pages[0].evidence,
79
+ },
80
+ ],
81
+ });
82
+ if (pages[0].purpose !== "cover")
83
+ pages.unshift(bookend("cover"));
33
84
  const last = pages[pages.length - 1];
34
85
  // Evidence and long explanations remain content slides, never masquerade as a closing.
35
- if (last.purpose !== 'conclude' || last.scene === 'brand-collection' || last.blocks.some(b => b.kind !== 'paragraph') || last.blocks.reduce((n, b) => n + (0, content_js_1.contentBlockText)(b).join('').length, 0) > 120 || /行业认可|客户|customers|trusted by/i.test(last.title)) {
36
- if (last.purpose === 'conclude')
37
- last.purpose = 'evidence';
38
- pages.push(bookend('conclude'));
86
+ if (last.purpose !== "conclude" ||
87
+ last.scene === "brand-collection" ||
88
+ last.blocks.some((b) => b.kind !== "paragraph") ||
89
+ last.blocks.reduce((n, b) => n + (0, content_js_1.contentBlockText)(b).join("").length, 0) >
90
+ 120 ||
91
+ /行业认可|客户|customers|trusted by/i.test(last.title)) {
92
+ if (last.purpose === "conclude")
93
+ last.purpose = "evidence";
94
+ pages.push(bookend("conclude"));
39
95
  }
40
96
  return { ...plan, pages };
41
97
  }
@@ -4,39 +4,91 @@ exports.GOOGLE_FONT_CATALOG = void 0;
4
4
  exports.contentScript = contentScript;
5
5
  exports.recommendedFonts = recommendedFonts;
6
6
  exports.languageFont = languageFont;
7
- const entry = (family, category, scripts = ['latin'], weights = [400, 700]) => ({ family, category, scripts, weights });
7
+ const entry = (family, category, scripts = ["latin"], weights = [400, 700]) => ({ family, category, scripts, weights });
8
8
  /** Curated Google Fonts families; request only their declared, supported weights. */
9
9
  exports.GOOGLE_FONT_CATALOG = [
10
- ...['Inter', 'Roboto', 'Montserrat', 'Manrope', 'DM Sans', 'Lato', 'Open Sans', 'Source Sans 3', 'Work Sans', 'Nunito Sans', 'IBM Plex Sans'].map(f => entry(f, 'sans')),
11
- ...['Lora', 'Merriweather', 'Libre Baskerville', 'Source Serif 4', 'Playfair Display', 'PT Serif'].map(f => entry(f, 'serif')),
12
- ...['Space Grotesk', 'Oswald', 'Bebas Neue'].map(f => entry(f, 'display', ['latin'], f === 'Bebas Neue' ? [400] : [400, 700])),
13
- ...['JetBrains Mono', 'IBM Plex Mono', 'Fira Code', 'Source Code Pro'].map(f => entry(f, 'mono')),
14
- entry('Noto Sans SC', 'sans', ['zh']), entry('Noto Serif SC', 'serif', ['zh']), entry('ZCOOL XiaoWei', 'display', ['zh'], [400]), entry('Ma Shan Zheng', 'display', ['zh'], [400]),
15
- entry('Noto Sans TC', 'sans', ['zh-Hant']), entry('Noto Serif TC', 'serif', ['zh-Hant']),
16
- entry('Noto Sans JP', 'sans', ['ja']), entry('Noto Serif JP', 'serif', ['ja']), entry('M PLUS Rounded 1c', 'sans', ['ja']),
17
- entry('Noto Sans KR', 'sans', ['ko']), entry('Noto Serif KR', 'serif', ['ko']),
18
- entry('Noto Sans Arabic', 'sans', ['ar']), entry('Noto Sans Devanagari', 'sans', ['hi']),
10
+ ...[
11
+ "Inter",
12
+ "Roboto",
13
+ "Montserrat",
14
+ "Manrope",
15
+ "DM Sans",
16
+ "Lato",
17
+ "Open Sans",
18
+ "Source Sans 3",
19
+ "Work Sans",
20
+ "Nunito Sans",
21
+ "IBM Plex Sans",
22
+ ].map((f) => entry(f, "sans")),
23
+ ...[
24
+ "Lora",
25
+ "Merriweather",
26
+ "Libre Baskerville",
27
+ "Source Serif 4",
28
+ "Playfair Display",
29
+ "PT Serif",
30
+ ].map((f) => entry(f, "serif")),
31
+ ...["Space Grotesk", "Oswald", "Bebas Neue"].map((f) => entry(f, "display", ["latin"], f === "Bebas Neue" ? [400] : [400, 700])),
32
+ ...["JetBrains Mono", "IBM Plex Mono", "Fira Code", "Source Code Pro"].map((f) => entry(f, "mono")),
33
+ entry("Noto Sans SC", "sans", ["zh"]),
34
+ entry("Noto Serif SC", "serif", ["zh"]),
35
+ entry("ZCOOL XiaoWei", "display", ["zh"], [400]),
36
+ entry("Ma Shan Zheng", "display", ["zh"], [400]),
37
+ entry("Noto Sans TC", "sans", ["zh-Hant"]),
38
+ entry("Noto Serif TC", "serif", ["zh-Hant"]),
39
+ entry("Noto Sans JP", "sans", ["ja"]),
40
+ entry("Noto Serif JP", "serif", ["ja"]),
41
+ entry("M PLUS Rounded 1c", "sans", ["ja"]),
42
+ entry("Noto Sans KR", "sans", ["ko"]),
43
+ entry("Noto Serif KR", "serif", ["ko"]),
44
+ entry("Noto Sans Arabic", "sans", ["ar"]),
45
+ entry("Noto Sans Devanagari", "sans", ["hi"]),
19
46
  ];
20
- function contentScript(text, locale = 'en') {
47
+ function contentScript(text, locale = "en") {
21
48
  if (/[\u3040-\u30ff]/u.test(text))
22
- return 'ja';
49
+ return "ja";
23
50
  if (/[\uac00-\ud7af]/u.test(text))
24
- return 'ko';
51
+ return "ko";
25
52
  if (/[\u0600-\u06ff]/u.test(text))
26
- return 'ar';
53
+ return "ar";
27
54
  if (/[\u0900-\u097f]/u.test(text))
28
- return 'hi';
55
+ return "hi";
29
56
  if (/[\u3400-\u9fff]/u.test(text))
30
- return /TW|HK|Hant/i.test(locale) ? 'zh-Hant' : 'zh';
57
+ return /^ja/i.test(locale)
58
+ ? "ja"
59
+ : /^ko/i.test(locale)
60
+ ? "ko"
61
+ : /TW|HK|Hant/i.test(locale)
62
+ ? "zh-Hant"
63
+ : "zh";
31
64
  if (/^zh/i.test(locale))
32
- return /TW|HK|Hant/i.test(locale) ? 'zh-Hant' : 'zh';
33
- return /^(ja|ko|ar|hi)/.exec(locale)?.[1] || 'latin';
65
+ return /TW|HK|Hant/i.test(locale) ? "zh-Hant" : "zh";
66
+ return /^(ja|ko|ar|hi)/.exec(locale)?.[1] || "latin";
34
67
  }
35
- function recommendedFonts(locale, text = '') { const script = contentScript(text, locale); return [...exports.GOOGLE_FONT_CATALOG].sort((a, b) => Number(b.scripts.includes(script)) - Number(a.scripts.includes(script))); }
36
- function languageFont(text, preferred, locale = 'en') {
68
+ function recommendedFonts(locale, text = "") {
37
69
  const script = contentScript(text, locale);
38
- if (script === 'latin')
70
+ return [...exports.GOOGLE_FONT_CATALOG].sort((a, b) => Number(b.scripts.includes(script)) - Number(a.scripts.includes(script)));
71
+ }
72
+ function languageFont(text, preferred, locale = "en") {
73
+ const script = contentScript(text, locale);
74
+ if (script === "latin")
75
+ return preferred;
76
+ const selected = exports.GOOGLE_FONT_CATALOG.find((font) => font.family === preferred);
77
+ if (selected?.scripts.includes(script))
78
+ return preferred;
79
+ if (!selected &&
80
+ ![
81
+ "Arial",
82
+ "Helvetica",
83
+ "Georgia",
84
+ "Times New Roman",
85
+ "Courier New",
86
+ "sans-serif",
87
+ "serif",
88
+ "monospace",
89
+ ].includes(preferred))
39
90
  return preferred;
40
- const serif = /serif|Georgia|Lora|Baskerville|Merriweather|Playfair/i.test(preferred) && !/sans/i.test(preferred);
41
- return exports.GOOGLE_FONT_CATALOG.find(f => f.scripts.includes(script) && f.category === (serif ? 'serif' : 'sans'))?.family || preferred;
91
+ const serif = /serif|Georgia|Lora|Baskerville|Merriweather|Playfair/i.test(preferred) &&
92
+ !/sans/i.test(preferred);
93
+ return (exports.GOOGLE_FONT_CATALOG.find((f) => f.scripts.includes(script) && f.category === (serif ? "serif" : "sans"))?.family || preferred);
42
94
  }
@@ -3,25 +3,71 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.LAYOUT_CONTRACTS = void 0;
4
4
  exports.eligibleLayouts = eligibleLayouts;
5
5
  exports.layoutCandidates = layoutCandidates;
6
- const layouts_js_1 = require("./layouts.js");
7
6
  const socialLayouts_js_1 = require("./socialLayouts.js");
8
- const textOnly = new Set(['cover', 'section', 'statement', 'quote', 'closing', 'image-full', 'og-cover', 'gradient-cover', 'bold-diagonal-cover', 'split-cover', 'block-banner', 'video-cover', 'magazine-pull-quote']);
9
- const pair = new Set(['comparison', 'two-column', 'myth', 'problem-solution', 'before-after', 'split-bar-compare']);
7
+ const textOnly = new Set([
8
+ "cover",
9
+ "section",
10
+ "statement",
11
+ "quote",
12
+ "closing",
13
+ "image-full",
14
+ "og-cover",
15
+ "gradient-cover",
16
+ "bold-diagonal-cover",
17
+ "split-cover",
18
+ "block-banner",
19
+ "video-cover",
20
+ "magazine-pull-quote",
21
+ ]);
22
+ const pair = new Set([
23
+ "comparison",
24
+ "two-column",
25
+ "myth",
26
+ "problem-solution",
27
+ "before-after",
28
+ "split-bar-compare",
29
+ ]);
10
30
  /** Eligibility is a first filter, never a substitute for measured fit. */
11
31
  exports.LAYOUT_CONTRACTS = [
12
- ...layouts_js_1.PRESENTATION_LAYOUT_IDS.map(id => ({ id, kind: 'presentation', semantic: pair.has(id) ? 'comparison' : textOnly.has(id) ? 'editorial' : id, minItems: pair.has(id) ? 2 : 0, maxItems: textOnly.has(id) ? 0 : pair.has(id) ? 2 : 4, minRatio: 16 / 9, maxRatio: 16 / 9, required: ['title'] })),
13
- ...socialLayouts_js_1.SOCIAL_LAYOUT_CATALOG.map(({ id, scene }) => ({ id, kind: 'social', semantic: scene, minItems: pair.has(id) ? 2 : 0, maxItems: textOnly.has(id) ? 0 : pair.has(id) ? 2 : id === 'course-syllabus' ? 15 : 6, minRatio: .25, maxRatio: 4, required: ['headline', ...(['pricing', 'metric'].includes(scene) ? ['value'] : [])] })),
32
+ ...socialLayouts_js_1.SOCIAL_LAYOUT_CATALOG.map(({ id, scene }) => ({
33
+ id,
34
+ kind: "social",
35
+ semantic: scene,
36
+ minItems: pair.has(id) ? 2 : 0,
37
+ maxItems: textOnly.has(id) ? 0 : pair.has(id) ? 2 : 6,
38
+ minRatio: 0.25,
39
+ maxRatio: 4,
40
+ required: [
41
+ "headline",
42
+ ...(["pricing", "metric"].includes(scene) ? ["value"] : []),
43
+ ],
44
+ })),
14
45
  ];
15
46
  function eligibleLayouts(kind, content, ratio) {
16
47
  const count = Array.isArray(content.items) ? content.items.length : 0;
17
- return exports.LAYOUT_CONTRACTS.filter(layout => layout.kind === kind && ratio >= layout.minRatio - 1e-6 && ratio <= layout.maxRatio + 1e-6 && count >= layout.minItems && count <= layout.maxItems && layout.required.every(key => typeof content[key] === 'string' && String(content[key]).trim()));
48
+ return exports.LAYOUT_CONTRACTS.filter((layout) => layout.kind === kind &&
49
+ ratio >= layout.minRatio - 1e-6 &&
50
+ ratio <= layout.maxRatio + 1e-6 &&
51
+ count >= layout.minItems &&
52
+ count <= layout.maxItems &&
53
+ layout.required.every((key) => typeof content[key] === "string" && String(content[key]).trim()));
18
54
  }
19
55
  /** Requested intent, same semantic scene, then general-purpose structures. */
20
56
  function layoutCandidates(kind, requested, content, ratio, limit = 8) {
21
57
  const eligible = eligibleLayouts(kind, content, ratio);
22
- const selected = exports.LAYOUT_CONTRACTS.find(layout => layout.kind === kind && layout.id === requested);
23
- const fallback = kind === 'presentation' ? ['cards', 'two-column', 'bullets', 'table', 'agenda', 'statement'] : ['report', 'checklist', 'steps', 'bento', 'editorial', 'story'];
24
- const order = [requested, ...eligible.filter(layout => layout.semantic === (content.scene || selected?.semantic)).map(layout => layout.id), ...fallback];
25
- const allowed = new Set(eligible.map(layout => layout.id));
26
- return [...new Set(order)].filter(id => allowed.has(id)).slice(0, Math.max(1, Math.min(16, limit)));
58
+ const selected = exports.LAYOUT_CONTRACTS.find((layout) => layout.kind === kind && layout.id === requested);
59
+ const fallback = kind === "presentation"
60
+ ? ["cards", "two-column", "bullets", "table", "agenda", "statement"]
61
+ : ["report", "checklist", "steps", "bento", "editorial", "story"];
62
+ const order = [
63
+ requested,
64
+ ...eligible
65
+ .filter((layout) => layout.semantic === (content.scene || selected?.semantic))
66
+ .map((layout) => layout.id),
67
+ ...fallback,
68
+ ];
69
+ const allowed = new Set(eligible.map((layout) => layout.id));
70
+ return [...new Set(order)]
71
+ .filter((id) => allowed.has(id))
72
+ .slice(0, Math.max(1, Math.min(16, limit)));
27
73
  }
@@ -1,103 +1,16 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.PRESENTATION_LAYOUT_IDS = void 0;
4
- /** Shared stable IDs for authored presentation renderers. */
4
+ /** Content intents for structured presentations; not a separate renderer catalog. */
5
5
  exports.PRESENTATION_LAYOUT_IDS = [
6
6
  "cover",
7
- "agenda",
8
7
  "section",
9
8
  "statement",
10
- "bullets",
11
- "two-column",
12
- "comparison",
13
- "three-column",
14
- "process",
15
- "timeline",
16
- "metrics",
17
- "quote",
18
- "image-left",
19
- "image-right",
20
- "image-full",
21
- "closing",
22
9
  "cards",
23
- "checklist",
24
- "roadmap",
25
- "team",
26
- "pricing",
27
- "faq",
28
- "swot",
29
- "scorecard",
10
+ "process",
11
+ "comparison",
30
12
  "bar-chart",
31
- "funnel",
13
+ "quote",
32
14
  "table",
33
- "problem-solution",
34
- "case-study",
35
- "architecture",
36
- "risk",
37
- "action-plan",
38
- "usage-breakdown",
39
- "organization",
40
- "editorial-quote",
41
- "research-highlight",
42
- "product-showcase",
43
- "split-bar-compare",
44
- "benchmark-bars",
45
- "block-banner",
46
- "progress-roadmap",
47
- "fan-deck",
48
- "contents-index",
49
- "contents-chapters",
50
- "contents-grid",
51
- "definition-card",
52
- "topic-branches",
53
- "radial-mindmap",
54
- "tips-card",
55
- "code-snippet",
56
- "series-carousel",
57
- "reflection-card",
58
- "video-cover",
59
- "digest-card",
60
- "chat-preview",
61
- "recommendation-feed",
62
- "before-after",
63
- "course-syllabus",
64
- "concept-explainer",
65
- "persona-question",
66
- "nutrient-symptom-grid",
67
- "milestone-history",
68
- "release-notes",
69
- "product-listing",
70
- "flash-sale",
71
- "event-banner",
72
- "startup-journey",
73
- "vision-goals",
74
- "tips-grid",
75
- "experiment-report",
76
- "leaderboard",
77
- "win-journey",
78
- "device-price-promo",
79
- "mega-sale-typographic",
80
- "illustration-process",
81
- "glass-neon-sale",
82
- "free-trial-hero",
83
- "feature-chip-hero",
84
- "event-speaker-card",
85
- "corporate-hero",
86
- "release-version-hero",
87
- "og-cover",
88
- "gradient-cover",
89
- "bold-diagonal-cover",
90
- "split-cover",
91
- "article-feed-card",
92
- "article-summary-card",
93
- "paragraph-summary-card",
94
- "summary-card-stack",
95
- "article-repost-card",
96
- "article-core-digest",
97
- "product-thread-mockup",
98
- "agent-notification",
99
- "angled-ui-launch",
100
- "repo-card",
101
- "magazine-pull-quote",
102
- "question-answer-quote"
15
+ "closing",
103
16
  ];
@@ -5,7 +5,9 @@ exports.splitContent = splitContent;
5
5
  /** Split only at explicit word/sentence boundaries. Never cut identifiers,
6
6
  * decimal values, or unbroken URLs to force them onto a smaller canvas. */
7
7
  function splitCopy(copy) {
8
- const breaks = [...copy.matchAll(/\s+|[。!?;]|[.!?;](?=\s|$)/gu)].map(match => match.index + match[0].length).filter(index => index > 0 && index < copy.length);
8
+ const breaks = [...copy.matchAll(/\s+|[。!?;]|[.!?;](?=\s|$)/gu)]
9
+ .map((match) => match.index + match[0].length)
10
+ .filter((index) => index > 0 && index < copy.length);
9
11
  if (!breaks.length)
10
12
  return;
11
13
  const at = breaks.sort((a, b) => Math.abs(a - copy.length / 2) - Math.abs(b - copy.length / 2))[0];
@@ -19,18 +21,27 @@ function splitContent(source) {
19
21
  const items = source.items || [];
20
22
  if (items.length > 1) {
21
23
  const middle = Math.ceil(items.length / 2);
22
- return [{ ...base, items: items.slice(0, middle) }, { ...base, body: '', items: items.slice(middle) }];
24
+ return [
25
+ { ...base, items: items.slice(0, middle) },
26
+ { ...base, body: "", items: items.slice(middle) },
27
+ ];
23
28
  }
24
29
  if (items.length && source.body?.trim())
25
- return [{ ...base, items: [] }, { ...base, body: '', items }];
30
+ return [
31
+ { ...base, items: [] },
32
+ { ...base, body: "", items },
33
+ ];
26
34
  if (source.body) {
27
35
  const parts = splitCopy(source.body);
28
36
  if (parts)
29
- return parts.map(body => ({ ...base, body }));
37
+ return parts.map((body) => ({ ...base, body }));
30
38
  }
31
39
  if (items.length === 1) {
32
40
  const parts = splitCopy(items[0].description);
33
41
  if (parts)
34
- return parts.map(description => ({ ...base, items: [{ ...items[0], description }] }));
42
+ return parts.map((description) => ({
43
+ ...base,
44
+ items: [{ ...items[0], description }],
45
+ }));
35
46
  }
36
47
  }