From 3e3d51082ef3cc91f38345943b1a1eb66c6bc0ff Mon Sep 17 00:00:00 2001 From: Maurice Date: Sat, 24 Oct 2026 09:27:52 +0200 Subject: [PATCH] refactor(pdf): the markdown rows and styles in a module of their own; the shared stop's markdown test beside its component --- client/scripts/size-baseline.json | 2 +- client/src/components/PDF/TripPDF.tsx | 20 +++-------- client/src/components/PDF/pdfMarkdown.ts | 30 ++++++++++++++++ client/src/pages/SharedTripPage.test.tsx | 25 ------------- .../sharedTrip/SharedPlaceDetails.test.tsx | 36 +++++++++++++++++++ 5 files changed, 71 insertions(+), 42 deletions(-) create mode 100644 client/src/components/PDF/pdfMarkdown.ts create mode 100644 client/src/pages/sharedTrip/SharedPlaceDetails.test.tsx diff --git a/client/scripts/size-baseline.json b/client/scripts/size-baseline.json index 20ecc0adc..7ae23cdc4 100644 --- a/client/scripts/size-baseline.json +++ b/client/scripts/size-baseline.json @@ -12,7 +12,7 @@ "src/components/PDF/TripPDF.tsx": 1082, "src/components/Planner/DayDetailPanel.test.tsx": 2333, "src/components/Planner/DayPlanSidebar.test.tsx": 5349, - "src/components/Planner/DayPlanSidebar.tsx": 2953, + "src/components/Planner/DayPlanSidebar.tsx": 2936, "src/components/Planner/PlaceFormModal.test.tsx": 2836, "src/components/Planner/PlaceFormModal.tsx": 1157, "src/components/Planner/PlaceInspector.tsx": 1290, diff --git a/client/src/components/PDF/TripPDF.tsx b/client/src/components/PDF/TripPDF.tsx index 251142fbf..9f70f374e 100644 --- a/client/src/components/PDF/TripPDF.tsx +++ b/client/src/components/PDF/TripPDF.tsx @@ -24,7 +24,7 @@ import { fetchExchangeRates } from '../../hooks/useExchangeRates' import { getFlightLegs, getTrainLegs, usesStationRoute } from '../../utils/flightLegs' import { isServiceStopType } from '../Roadtrip/roadtripModel' import { onlyMyPlan } from './pdfScope' -import { createMarkdownHtml } from '../shared/markdownHtml' +import { createPdfMarkdown, PDF_MARKDOWN_CSS } from './pdfMarkdown' /** * Every day starts a new page by default. On a trip of short days that prints @@ -232,10 +232,8 @@ export async function downloadTripPDF({ trip, days, places, assignments: allStor let hasTransportNotes = false const loc = _locale || undefined const tr = _t || (k => k) - // A place's description and notes are markdown in the app, so the export renders - // them the same way instead of printing the raw syntax (#2692). - const markdown = createMarkdownHtml() - const mdBlock = (text: string) => `
${markdown.html(text)}
` + const markdown = createPdfMarkdown() + const mdBlock = markdown.row // The store read is the fallback, not the source: a caller that forgets the // prop still prints the reader's own format instead of silently reverting. const is12h = (_timeFormat || useSettingsStore.getState().settings.time_format || '24h') === '12h' @@ -841,17 +839,7 @@ export async function downloadTripPDF({ trip, days, places, assignments: allStor .info-spacer { width: 13px; flex-shrink: 0; } .info-text { font-size: 9px; color: #64748b; line-height: 1.5; } .info-text.muted { color: #94a3b8; } - .info-text.md { min-width: 0; overflow-wrap: anywhere; } - .md > :first-child { margin-top: 0; } - .md > :last-child { margin-bottom: 0; } - .md p, .md ul, .md ol, .md table, .md blockquote, .md pre { margin: 0 0 4px; } - .md ul, .md ol { padding-inline-start: 14px; } - .md h1, .md h2, .md h3, .md h4, .md h5, .md h6 { font-size: 1.1em; font-weight: 700; margin: 4px 0 2px; } - .md table { border-collapse: collapse; } - .md th, .md td { border: 1px solid #e2e8f0; padding: 2px 5px; text-align: start; } - .md code { font-family: ui-monospace, monospace; font-size: 8.5px; } - .md a { color: inherit; text-decoration: underline; } - .md blockquote { border-inline-start: 2px solid #e2e8f0; padding-inline-start: 6px; } + ${PDF_MARKDOWN_CSS} .chips { display: flex; flex-wrap: wrap; gap: 4px; padding-left: 21px; margin-top: 4px; } .chip { display: inline-flex; align-items: center; gap: 3px; font-size: 8px; font-weight: 600; background: #f1f5f9; color: #374151; border-radius: 99px; padding: 2px 7px; white-space: nowrap; } diff --git a/client/src/components/PDF/pdfMarkdown.ts b/client/src/components/PDF/pdfMarkdown.ts new file mode 100644 index 000000000..c4e834395 --- /dev/null +++ b/client/src/components/PDF/pdfMarkdown.ts @@ -0,0 +1,30 @@ +import { createMarkdownHtml } from '../shared/markdownHtml'; + +/** + * A place's description and notes are markdown in the app, so the export + * renders them the same way instead of printing the raw syntax (#2692): one + * info row per text, the HTML from the app's own sanitized pipeline. + */ +export function createPdfMarkdown(): { row: (text: string) => string; dispose: () => void } { + const markdown = createMarkdownHtml(); + return { + row: (text) => + `
${markdown.html(text)}
`, + dispose: markdown.dispose, + }; +} + +/** The markdown's own spacing, lists, tables and links at the export's small size. */ +export const PDF_MARKDOWN_CSS = ` + .info-text.md { min-width: 0; overflow-wrap: anywhere; } + .md > :first-child { margin-top: 0; } + .md > :last-child { margin-bottom: 0; } + .md p, .md ul, .md ol, .md table, .md blockquote, .md pre { margin: 0 0 4px; } + .md ul, .md ol { padding-inline-start: 14px; } + .md h1, .md h2, .md h3, .md h4, .md h5, .md h6 { font-size: 1.1em; font-weight: 700; margin: 4px 0 2px; } + .md table { border-collapse: collapse; } + .md th, .md td { border: 1px solid #e2e8f0; padding: 2px 5px; text-align: start; } + .md code { font-family: ui-monospace, monospace; font-size: 8.5px; } + .md a { color: inherit; text-decoration: underline; } + .md blockquote { border-inline-start: 2px solid #e2e8f0; padding-inline-start: 6px; } +`; diff --git a/client/src/pages/SharedTripPage.test.tsx b/client/src/pages/SharedTripPage.test.tsx index 1e9d88dc5..aaf6691e6 100644 --- a/client/src/pages/SharedTripPage.test.tsx +++ b/client/src/pages/SharedTripPage.test.tsx @@ -1657,31 +1657,6 @@ describe('SharedTripPage', () => { await waitFor(() => expect(screen.getByText('Somewhere 1')).toBeInTheDocument()); expect(screen.queryByText('1 h')).toBeNull(); }); - - // #2692: the description and notes are markdown in the planner, so the shared page renders them too. - it('renders markdown in the description and notes, and keeps raw HTML as text', async () => { - const marked = { - 21: [{ - id: 304, day_id: 21, order_index: 0, notes: '**Bring** cash', - place: { - id: 504, name: 'Market', - description: ['## Opening', '', '| Day | Hours |', '| --- | --- |', '| Sat | 8 to 14 |', '', ''].join('\n'), - address: 'Market Square 1', lat: null, lng: null, duration_minutes: 0, notes: ['- fruit', '- bread'].join('\n'), - website: null, phone: null, category_id: null, price: null, place_time: null, end_time: null, - image_url: null, transport_mode: 'walking', category: null, tags: [], - }, - }], - }; - await open('markdown-token', payload({ days, assignments: marked })); - await waitFor(() => expect(screen.getByText('Market Square 1')).toBeInTheDocument()); - - expect(screen.getByRole('heading', { name: 'Opening' })).toBeInTheDocument(); - expect(screen.getByRole('cell', { name: '8 to 14' })).toBeInTheDocument(); - expect(screen.getByText('Bring').tagName).toBe('STRONG'); - expect(screen.getByText('fruit').tagName).toBe('LI'); - expect(document.querySelector('img[src="x"]')).toBeNull(); - expect(screen.queryByText('## Opening')).toBeNull(); - }); }); describe('FE-PAGE-SHARED-039: a booking shows its note and its link, and nothing that is not http(s) (#2320)', () => { diff --git a/client/src/pages/sharedTrip/SharedPlaceDetails.test.tsx b/client/src/pages/sharedTrip/SharedPlaceDetails.test.tsx new file mode 100644 index 000000000..faebddb2e --- /dev/null +++ b/client/src/pages/sharedTrip/SharedPlaceDetails.test.tsx @@ -0,0 +1,36 @@ +import { describe, expect, it } from 'vitest'; + +import { render, screen } from '../../../tests/helpers/render'; +import { SharedPlaceDetails } from './SharedPlaceDetails'; + +/** FE-PAGE-SHARED-PLACE-001: a stop's description and notes on a shared trip, read as the planner shows them (#2692). */ + +describe('SharedPlaceDetails', () => { + it('FE-PAGE-SHARED-PLACE-001: renders markdown in the description and notes, and keeps raw HTML as text', () => { + render( + ', + ].join('\n'), + notes: ['- fruit', '- bread'].join('\n'), + }} + assignmentNotes="**Bring** cash" + /> + ); + expect(screen.getByRole('heading', { name: 'Opening' })).toBeInTheDocument(); + expect(screen.getByRole('cell', { name: '8 to 14' })).toBeInTheDocument(); + expect(screen.getByText('Bring').tagName).toBe('STRONG'); + expect(screen.getByText('fruit').tagName).toBe('LI'); + expect(document.querySelector('img[src="x"]')).toBeNull(); + expect(screen.queryByText('## Opening')).toBeNull(); + }); +});