diff --git a/shared/src/book/design/design-templated-framing.ts b/shared/src/book/design/design-templated-framing.ts index 29f49265c..310b43d22 100644 --- a/shared/src/book/design/design-templated-framing.ts +++ b/shared/src/book/design/design-templated-framing.ts @@ -13,7 +13,7 @@ import { import type { JourneyStats } from '../journey-stats.schema'; import { applyDecoratedTemplate } from '../templates/template-apply'; import type { BookDecoratedTemplate } from '../templates/template-format'; -import { bookTemplateWords } from '../templates/template-words'; +import { bookTemplateWords, type BookTemplateWords } from '../templates/template-words'; import { themedElement } from '../theme/book-theme-apply'; import { bookTravelLooks, BOOK_TRAVEL_COUNTRY_ACCENT, type BookTravelLooks } from '../travel/book-travel-looks'; import { frameOf, liveBox, photoEl, scaled, textEl, type ComposeCtx } from './design-compose'; @@ -79,14 +79,18 @@ function coverSeed(c: ComposeCtx, words: CoverWords, material: CoverMaterial, sl return { id: c.newId('sp'), role: 'cover', background: null, elements: els, parked: [], entryId: null }; } -/** A page laid on a decorated template in the book's theme, its empty roles taking the journey's words. */ +/** + * A page laid on a decorated template in the book's theme, its empty roles + * taking the journey's words, or the words `given` for a role by name. + */ export function pourTemplate( c: ComposeCtx, seed: BookSpread, tpl: BookDecoratedTemplate, entries: readonly AutoEntry[], + given: BookTemplateWords = {}, ) { - const words = bookTemplateWords(seed, { bookTitle: '', locale: c.locale, entries }); + const words = { ...bookTemplateWords(seed, { bookTitle: '', locale: c.locale, entries }), ...given }; return applyDecoratedTemplate(seed, tpl, c.page, { theme: c.look.theme, travel: c.travel, diff --git a/shared/src/book/design/design-templated.parity.json b/shared/src/book/design/design-templated.parity.json index 0fde3fbb1..7a89b3ee1 100644 --- a/shared/src/book/design/design-templated.parity.json +++ b/shared/src/book/design/design-templated.parity.json @@ -1,115 +1,121 @@ { "A/fabric-classic/head": "74bb0439e4575ed7", - "A/fabric-classic/plan": "2e813091b20151b7", - "A/fabric-classic/spread/0": "8f7e1afe57460ae6", + "A/fabric-classic/plan": "95b55b052b9ff07b", + "A/fabric-classic/spread/0": "a4d090166287cbe5", "A/fabric-classic/spread/1": "aa637b0de2518bf5", - "A/fabric-classic/spread/10": "e69ba5b8577eaccd", - "A/fabric-classic/spread/11": "bc296929478ee85f", - "A/fabric-classic/spread/12": "3053f53078476b2a", - "A/fabric-classic/spread/13": "e636be9ac1bfac5c", - "A/fabric-classic/spread/14": "9c9506b1446385fa", - "A/fabric-classic/spread/15": "f9c0ddb3332943e1", - "A/fabric-classic/spread/16": "a63442848952fded", - "A/fabric-classic/spread/17": "31aff9e6d65d8da4", - "A/fabric-classic/spread/18": "bf897175ec7245e3", - "A/fabric-classic/spread/19": "bf8169ba3d96440e", + "A/fabric-classic/spread/10": "febf8fa0b4cd3aef", + "A/fabric-classic/spread/11": "26253da4f1f688a0", + "A/fabric-classic/spread/12": "4dbf29b2c63f9231", + "A/fabric-classic/spread/13": "5deb8ece6f27e514", + "A/fabric-classic/spread/14": "db4f2ede14bb6023", + "A/fabric-classic/spread/15": "7d011356ea8fc70f", + "A/fabric-classic/spread/16": "205c5f0f86f90cb4", + "A/fabric-classic/spread/17": "de849a3986a487c1", + "A/fabric-classic/spread/18": "22b9cb0c78e0ff66", + "A/fabric-classic/spread/19": "9df44ff280b76f64", "A/fabric-classic/spread/2": "8278d3c2507527f9", - "A/fabric-classic/spread/20": "7233b4dfd372d1e1", - "A/fabric-classic/spread/21": "5bb02502fc0ad047", - "A/fabric-classic/spread/22": "61f06e7b8b03a23e", + "A/fabric-classic/spread/20": "972976260c457cd3", + "A/fabric-classic/spread/21": "9102a5fa147693fe", + "A/fabric-classic/spread/22": "0bc9c54f6ee8b3e9", "A/fabric-classic/spread/23": "b11b5b3fb34a34eb", "A/fabric-classic/spread/24": "a55189e7bfd3934d", "A/fabric-classic/spread/3": "2ceb4890ba95c249", - "A/fabric-classic/spread/4": "7f5a904449283690", - "A/fabric-classic/spread/5": "61024db654103dd4", - "A/fabric-classic/spread/6": "8117ccbdc6dc7f1e", - "A/fabric-classic/spread/7": "da36e9e35e753415", - "A/fabric-classic/spread/8": "198e5a3ff4c85366", - "A/fabric-classic/spread/9": "516fc6e34b8a200e", + "A/fabric-classic/spread/4": "7743a5784df1d3e5", + "A/fabric-classic/spread/5": "dbbbfb7bedd90da2", + "A/fabric-classic/spread/6": "c36b6c75a4254fad", + "A/fabric-classic/spread/7": "b7694f72f7cbe2bb", + "A/fabric-classic/spread/8": "bdafe534fcbe56f0", + "A/fabric-classic/spread/9": "c8a03b3fbdb60114", "A/midnight/head": "ce0d82281cbc3e1f", - "A/midnight/plan": "59eb38db863e4b47", - "A/midnight/spread/0": "eb607a0fc5b592ac", + "A/midnight/plan": "7fde812131c9b570", + "A/midnight/spread/0": "409beb5fbb1accce", "A/midnight/spread/1": "d9f5d78659332b45", - "A/midnight/spread/10": "691dd9203db87133", - "A/midnight/spread/11": "4cc728884c5e85f9", - "A/midnight/spread/12": "f2e0ed48e8d5b738", - "A/midnight/spread/13": "a05050ef05c8a754", - "A/midnight/spread/14": "b9c738456d6cbf49", - "A/midnight/spread/15": "aceb06a5507b46c0", - "A/midnight/spread/16": "f2761e7e09ff254b", - "A/midnight/spread/17": "e4d8bf042e9f24d2", - "A/midnight/spread/18": "3daa8f2074ce601b", - "A/midnight/spread/19": "e23937f6deffb3b2", + "A/midnight/spread/10": "53f13a2ec3a3c5f4", + "A/midnight/spread/11": "888c0c66730d32dc", + "A/midnight/spread/12": "8d6c62bc4ee5ed1d", + "A/midnight/spread/13": "fc0673a56f5b4d4b", + "A/midnight/spread/14": "33db0be88fd469a7", + "A/midnight/spread/15": "72105c668088bc4c", + "A/midnight/spread/16": "bd11f4dc18e03108", + "A/midnight/spread/17": "2e7a0f09822c3556", + "A/midnight/spread/18": "f4264c3ee121b8d1", + "A/midnight/spread/19": "4f27a48a457d95bb", "A/midnight/spread/2": "b00839cd0dc1451b", - "A/midnight/spread/20": "bd539efa35e36bfc", - "A/midnight/spread/21": "38d76ca81db84db0", + "A/midnight/spread/20": "dff6e523732c8481", + "A/midnight/spread/21": "26d666dde7be6641", "A/midnight/spread/22": "eba329fd185958b3", "A/midnight/spread/23": "9664fbd24505a632", "A/midnight/spread/3": "742dfb4105263101", - "A/midnight/spread/4": "98f7916fe80a1243", - "A/midnight/spread/5": "7b262b39dd640935", - "A/midnight/spread/6": "ed8bd8657c310997", - "A/midnight/spread/7": "4eb6a338f0d7fc68", - "A/midnight/spread/8": "e2d2c7791a83e1a1", - "A/midnight/spread/9": "05c31075e8a1f631", + "A/midnight/spread/4": "ea9613c918951560", + "A/midnight/spread/5": "8aa571b4d51a5234", + "A/midnight/spread/6": "2275359a3c03f457", + "A/midnight/spread/7": "3ef3c265b38598e2", + "A/midnight/spread/8": "3a77eec4f6c9f860", + "A/midnight/spread/9": "5bdff7027bea103c", "A/riso/head": "7d43d0900fbe0f57", - "A/riso/plan": "7e50f5d2a0b80e0e", - "A/riso/spread/0": "657be2c47bd8ad61", + "A/riso/plan": "f802ad819f1fb262", + "A/riso/spread/0": "1f4502d29eca9017", "A/riso/spread/1": "8de92788de29277b", - "A/riso/spread/10": "8f8039b3c8548358", - "A/riso/spread/11": "5dc1c12abcf36f29", - "A/riso/spread/12": "84b6af0e7a851a60", - "A/riso/spread/13": "c0aff5dc75e09532", - "A/riso/spread/14": "3d8e67e0c536334c", - "A/riso/spread/15": "29101a4941a4e9c5", + "A/riso/spread/10": "2a92b79192575cd0", + "A/riso/spread/11": "5785a9cc59ddd735", + "A/riso/spread/12": "cd4f6004153fe694", + "A/riso/spread/13": "b1f87a92cb7f775c", + "A/riso/spread/14": "873bf5f7a8404fe4", + "A/riso/spread/15": "c1aab0214328ef3b", "A/riso/spread/16": "d58f70c381d9f6ae", "A/riso/spread/17": "00caa407cb2a0677", - "A/riso/spread/18": "ed1fbeca6e577132", - "A/riso/spread/19": "323e6d081b913240", + "A/riso/spread/18": "ed41aca064456614", + "A/riso/spread/19": "36647f06c594e715", "A/riso/spread/2": "5c9a93de9e6467d6", - "A/riso/spread/20": "84f564cff0148bc3", - "A/riso/spread/21": "cc4b2d6f3ad38662", - "A/riso/spread/22": "f696e1aad9a5e0f6", + "A/riso/spread/20": "8518ca61c110fd8a", + "A/riso/spread/21": "d4ba78b2ffd5ae04", + "A/riso/spread/22": "de5116307e12ce61", "A/riso/spread/23": "a748ada7a028818e", "A/riso/spread/24": "9abf456403732463", "A/riso/spread/3": "20a199a47535dea7", - "A/riso/spread/4": "45bfc268e430c87d", - "A/riso/spread/5": "d15be16f42113e11", - "A/riso/spread/6": "3056ef6c2b6ebdd1", - "A/riso/spread/7": "3f58daf471a89d43", - "A/riso/spread/8": "ce87c24b384a8bf0", - "A/riso/spread/9": "6e02b9784f9d5791", - "B/fabric-classic/head": "b7377b9e48255d7b", - "B/fabric-classic/plan": "121194e82b626f79", + "A/riso/spread/4": "93b84be6e48097b5", + "A/riso/spread/5": "6d5ae8e80c2f0d21", + "A/riso/spread/6": "e7f5c928785be48d", + "A/riso/spread/7": "b672fd04b61ea7b9", + "A/riso/spread/8": "d9e84bbad744e450", + "A/riso/spread/9": "017d695be27dc988", + "B/fabric-classic/head": "6af0d11dd09eae78", + "B/fabric-classic/plan": "cae85517611abbca", "B/fabric-classic/spread/0": "87325aa216fe9c6a", "B/fabric-classic/spread/1": "b3f8e9c5043f8302", "B/fabric-classic/spread/2": "5951c4e8d72b4b3f", - "B/fabric-classic/spread/3": "3c1131d279c96432", + "B/fabric-classic/spread/3": "110c2e11d8079468", "B/fabric-classic/spread/4": "6f02c19ed62524d1", - "B/fabric-classic/spread/5": "25cfae1fead90097", - "B/fabric-classic/spread/6": "b11b5b3fb34a34eb", - "B/fabric-classic/spread/7": "3194c096e5c7d16f", - "B/midnight/head": "1d04fe6e78b208c4", - "B/midnight/plan": "3168bafe49ffe4d6", - "B/midnight/spread/0": "487b2440a33b4990", + "B/fabric-classic/spread/5": "5b0cb3424c4b894c", + "B/fabric-classic/spread/6": "28fec4ed0f8fd482", + "B/fabric-classic/spread/7": "9d68ef6e7d8b5f5a", + "B/fabric-classic/spread/8": "b11b5b3fb34a34eb", + "B/fabric-classic/spread/9": "3194c096e5c7d16f", + "B/midnight/head": "cfd309bdf9e41811", + "B/midnight/plan": "b3e4cb03c2d620a4", + "B/midnight/spread/0": "7fdd20dcda48881f", "B/midnight/spread/1": "d9f5d78659332b45", + "B/midnight/spread/10": "db97ebe8475e0371", "B/midnight/spread/2": "a094503b82019808", - "B/midnight/spread/3": "54de2d4a4b77b772", - "B/midnight/spread/4": "a480b41d506dfa5c", - "B/midnight/spread/5": "db7f87888a66afc9", - "B/midnight/spread/6": "5e937726a85c5b42", - "B/midnight/spread/7": "eba329fd185958b3", - "B/midnight/spread/8": "db97ebe8475e0371", - "B/riso/head": "30cd1c15dcaf7def", - "B/riso/plan": "357271fb7149e401", - "B/riso/spread/0": "6bf3e8b0649cf3c5", + "B/midnight/spread/3": "6e3e57121d8b8683", + "B/midnight/spread/4": "468100c3bebad30d", + "B/midnight/spread/5": "ca669ba76ca4d9bd", + "B/midnight/spread/6": "60b4cb974e35c55d", + "B/midnight/spread/7": "46fe0841298e4f7b", + "B/midnight/spread/8": "25e4510c5fb81817", + "B/midnight/spread/9": "eba329fd185958b3", + "B/riso/head": "69a6c3112de2a047", + "B/riso/plan": "d8906a53b3532db8", + "B/riso/spread/0": "2bbd6e6b4e6d1b4d", "B/riso/spread/1": "8de92788de29277b", "B/riso/spread/2": "40f8d2b093d0fc8d", - "B/riso/spread/3": "f79b25f04949963a", - "B/riso/spread/4": "0c5ae515506ee299", - "B/riso/spread/5": "0855b6391406b76d", - "B/riso/spread/6": "a748ada7a028818e", - "B/riso/spread/7": "877657cfa95e3cac", + "B/riso/spread/3": "1fc8aedc7486cbeb", + "B/riso/spread/4": "311c9fcde888d578", + "B/riso/spread/5": "51562d8f08f46011", + "B/riso/spread/6": "c114a720b5c5beff", + "B/riso/spread/7": "6f4c43c7b5f1dbc3", + "B/riso/spread/8": "a748ada7a028818e", + "B/riso/spread/9": "877657cfa95e3cac", "C/fabric-classic/head": "ddd21691c506a600", "C/fabric-classic/plan": "c31dcaa9b3603acc", "C/fabric-classic/spread/0": "92d21ac9d8e866df", diff --git a/shared/src/book/design/design-templated.spec.ts b/shared/src/book/design/design-templated.spec.ts index e41c0475f..3ddd49269 100644 --- a/shared/src/book/design/design-templated.spec.ts +++ b/shared/src/book/design/design-templated.spec.ts @@ -9,12 +9,13 @@ import { } from '../book.schema'; import { pageFindings } from '../describe-spread'; import { findDecoratedTemplate } from '../templates/template-catalogue'; +import { templatePhotoSlots } from '../templates/template-format'; import { matchDecoratedTemplate } from '../templates/template-match'; import { bookThemeColor, bookThemeOf } from '../theme/book-theme'; import { applyBookTheme } from '../theme/book-theme-apply'; import { designBook, type BookDesignInput, type BookDesignResult } from './design-book'; import type { BookDesignDirectivesInput } from './design-directives.schema'; -import { THEME_ARCHETYPE } from './design-template-pick'; +import { ARCHETYPE_KIN, THEME_ARCHETYPE } from './design-template-pick'; import { createHash } from 'node:crypto'; import { readFileSync } from 'node:fs'; @@ -331,6 +332,32 @@ describe('the book on the designed templates', () => { ); }); + it("AUTODSN-016: bare stops are listed on a words-only template of the book's styles, every name in its story", () => { + const base = parity.A as Fixture; + const entries = base.input.entries.map((e) => ({ ...e, story: null, photos: [] })); + const input = { ...base.input, entries, coverPhotoId: null, candidates: [], photos: [] }; + for (const theme of THEMES) { + const { plan, doc } = laid(input, theme); + const lists = plan.filter((r) => r.beat === 'stations'); + expect(lists.length, theme).toBeGreaterThan(0); + const words: string[] = []; + for (const r of lists) { + const tpl = findDecoratedTemplate(r.template ?? ''); + expect(tpl && templatePhotoSlots(tpl), `${theme} ${r.template}`).toBe(0); + expect([THEME_ARCHETYPE[theme], ...ARCHETYPE_KIN[THEME_ARCHETYPE[theme]]]).toContain(tpl?.archetype); + const s = doc.spreads.find((x) => x.id === r.spreadId) as BookSpread; + const texts = s.elements.flatMap((e) => (e.kind === 'text' ? [e.text] : [])); + expect(texts, theme).toContain(input.stationsLabel); + words.push(...texts); + } + const listed = words.join(' '); + for (const e of entries) { + const name = e.title || e.location || ''; + if (name) expect(listed, `${theme} ${name}`).toContain(name); + } + } + }); + it('AUTODSN-020: lays the parity journeys exactly as pinned', () => { const actual = digests(); const differ = [...actual].filter(([k, v]) => expected[k] !== v).map(([k]) => k); diff --git a/shared/src/book/design/design-templated.ts b/shared/src/book/design/design-templated.ts index 369e85eed..f9a91fa7c 100644 --- a/shared/src/book/design/design-templated.ts +++ b/shared/src/book/design/design-templated.ts @@ -1,9 +1,11 @@ import type { AutoEntry, AutoPhoto } from '../auto/auto-types'; import type { BookCoverArchetype } from '../book-archetypes'; import { DESIGN_TEXT_HEADROOM, textHeightNeeded } from '../book-findings'; +import { formatBookDate } from '../book-text'; import type { BookElement, BookSpread, BookTextElement } from '../book.schema'; import { decoratedTemplatesFor } from '../templates/template-catalogue'; -import { templatePhotoSlots, type BookDecoratedTemplate } from '../templates/template-format'; +import { templatePhotoSlots, templateRoles, type BookDecoratedTemplate } from '../templates/template-format'; +import type { BookTemplateWord } from '../templates/template-words'; import type { BookDesignInput } from './design-book'; import { cutWords, liveBox, photoEl, textEl, type ComposeCtx } from './design-compose'; import { composeStations } from './design-framing'; @@ -308,10 +310,71 @@ function chapterPages(l: Laying, chapter: DesignChapter, notes: string[]) { } } -/** The pages of a list of bare stops: as many stations spreads as the stops need. */ +/** The most stops one page of the list takes, so a page of them still reads as a list. */ +const STOPS_PER_PAGE = 10; + +/** Bare stops as a list: under each date the names of the stops of that day, a blank line between days. */ +function stopList(c: ComposeCtx, stops: readonly AutoEntry[], dated: boolean): string { + const days: { date: string; names: string[] }[] = []; + for (const e of stops) { + const date = e.date && dated ? formatBookDate(e.date, c.locale) : ''; + const name = (e.title || e.location || '').trim(); + if (!name) continue; + const day = days[days.length - 1]; + if (day && day.date === date) day.names.push(name); + else days.push({ date, names: [name] }); + } + return days.map((d) => [d.date, ...d.names].filter(Boolean).join('\n')).join('\n\n'); +} + +/** The one day all these stops were on, bound to the first of them, as a date role takes it; null without one. */ +function oneDay(c: ComposeCtx, stops: readonly AutoEntry[]): BookTemplateWord | null { + const days = new Set(stops.map((e) => e.date ?? '')); + const [day] = days; + const first = stops[0]; + if (days.size !== 1 || !day || !first) return null; + return { text: formatBookDate(day, c.locale), binding: { source: 'entry.date', entryId: first.id, value: day } }; +} + +/** + * One page of bare stops on a words-only template of the book's styles: the + * list's label as its title, the stops as its story (given by role, since a + * short list that opens on a date would read as a date line), as many as its + * story frame holds. Stops of one day set that day in the template's date + * and only their names in the list. Undefined when no template takes a list. + */ +function stationsPage(l: Laying, stops: readonly AutoEntry[], label: string) { + for (let n = Math.min(stops.length, STOPS_PER_PAGE); n > 0; n--) { + const some = stops.slice(0, n); + const key = `stations:${l.out.length}`; + const pick = (list: string) => + pickInnerTemplate({ photos: [], story: list, mappable: false }, { ...l.base, history: l.history, key }); + const tpl = pick(stopList(l.c, some, true)); + if (!tpl) return undefined; + const day = templateRoles(tpl).includes('date') ? oneDay(l.c, some) : null; + const list = stopList(l.c, some, !day); + if (bodyCapacity(tpl, l.c.page, l.c.look.theme) < list.length && n > 1) continue; + const words = { + title: { text: label }, + body: { text: list }, + ...(day ? { date: day } : {}), + }; + return { spread: tidied(pourTemplate(l.c, innerSpread(l.c, [], null), tpl, l.entries, words)), tpl, placed: n }; + } + return undefined; +} + +/** The pages of a list of bare stops: as many as the stops need, each on a words-only template where one takes it. */ function stationsPages(l: Laying, chapter: DesignChapter, input: BookDesignInput) { let rest = chapter.entries; while (rest.length) { + const page = stationsPage(l, rest, input.stationsLabel); + if (page) { + l.history.push(page.tpl.id); + l.out.push({ spread: page.spread, beat: 'stations', chapter: null, findings: [], template: page.tpl.id }); + rest = rest.slice(page.placed); + continue; + } const set = composeStations(l.c, rest, input.stationsLabel); if (!set.placed) break; l.out.push({ spread: innerSpread(l.c, set.elements, null), beat: 'stations', chapter: null, findings: [] });