diff --git a/.gitattributes b/.gitattributes index 59e602b..d23eab5 100644 --- a/.gitattributes +++ b/.gitattributes @@ -194,3 +194,13 @@ docs/assets/component-kits/** -text docs/assets/component-kits/*.pdf binary docs/assets/component-kits/*.png binary docs/assets/component-kits/*.zip binary + +examples/yew-sheet/** text eol=lf +examples/yew-sheet/fonts/*.ttf binary +examples/yew-sheet/fonts/*-OFL.txt -text -whitespace +tools/fixtures/yew-client/** text eol=lf +docs/assets/yew-starter/** -text +docs/assets/yew-starter/*.pdf binary +docs/assets/yew-starter/*.png binary +docs/assets/yew-starter/*.zip binary +docs/assets/yew-starter/preview/fonts/*-OFL.txt -whitespace diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index e9fb363..b5b3203 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -13,6 +13,43 @@ concurrency: cancel-in-progress: true jobs: + yew-starter: + name: Yew PDF and HTML starter (${{ matrix.os }}, Rust ${{ matrix.rust }}) + runs-on: ${{ matrix.os }} + timeout-minutes: 20 + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-latest + rust: '1.85.0' + - os: windows-latest + rust: '1.97.0' + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-python@v6 + with: + python-version: '3.12' + - uses: dtolnay/rust-toolchain@stable + with: + toolchain: ${{ matrix.rust }} + targets: wasm32-unknown-unknown + - run: | + python -m pip install --only-binary=:all: -r tools/browser-requirements.txt + - run: python -m playwright install --with-deps chromium + - name: Reproduce the download and check native and browser component builds + run: | + python tools/build_yew_starter.py + git diff --exit-code -- docs/assets/yew-starter/project.zip docs/assets/yew-starter/source.json + python tools/verify_yew_starter.py + - name: Check the exported HTML and PDF downloads in Chromium + run: python tools/check_yew_starter_browser.py --root target/yew-starter/documents --output target/yew-starter/browser + - uses: actions/upload-artifact@v4 + if: always() + with: + name: yew-starter-${{ matrix.os }}-${{ matrix.rust }} + path: target/yew-starter/ + retention-days: 14 component-kits: name: Component kit composition (${{ matrix.os }}) runs-on: ${{ matrix.os }} @@ -1127,7 +1164,7 @@ jobs: path: site deploy: if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request' - needs: [build, component-kits, accessible-showcase, showcase-authoring, component-notebook, page-band-notebook, complete-html, component-preview, bootstrap-preview, components, node-components, dotnet-components, aspnet, chinese, arabic, markdown, tagged-notice, rust-download, matplotlib, pandas-components, numbered-report, axum, lambda-starter, n8n-workflow, laravel-queue, laravel-platform, streamlit] + needs: [build, yew-starter, component-kits, accessible-showcase, showcase-authoring, component-notebook, page-band-notebook, complete-html, component-preview, bootstrap-preview, components, node-components, dotnet-components, aspnet, chinese, arabic, markdown, tagged-notice, rust-download, matplotlib, pandas-components, numbered-report, axum, lambda-starter, n8n-workflow, laravel-queue, laravel-platform, streamlit] runs-on: ubuntu-latest permissions: pages: write diff --git a/.gitignore b/.gitignore index 95d56a7..62f00e5 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,8 @@ docs/assets/playground/project-verification.json docs/assets/playground/node-project-verification.json examples/rust/target/ examples/rust/verification/ +examples/yew-sheet/target/ +examples/yew-sheet/output/ /output/ /target/ __pycache__/ diff --git a/docs/assets/yew-starter/document.pdf b/docs/assets/yew-starter/document.pdf new file mode 100644 index 0000000..8ab1696 Binary files /dev/null and b/docs/assets/yew-starter/document.pdf differ diff --git a/docs/assets/yew-starter/document.png b/docs/assets/yew-starter/document.png new file mode 100644 index 0000000..5a6df18 Binary files /dev/null and b/docs/assets/yew-starter/document.png differ diff --git a/docs/assets/yew-starter/preview/document.html b/docs/assets/yew-starter/preview/document.html new file mode 100644 index 0000000..35a37bf --- /dev/null +++ b/docs/assets/yew-starter/preview/document.html @@ -0,0 +1,2 @@ + +Saturday mixed-game rotation

Riverstone / Community Series

Saturday mixed-game rotation

A sample structure for an afternoon around the table.

Starting stack
20,000 chips
Table size
6 players
Registration
Through level 4

The rotation

Levels, stakes and scheduled breaks
LevelMinutesGameBlinds / betsAnte
120No-limit hold’em100 / 200200
220Pot-limit Omaha100 / 200—
320Limit hold’em300 / 600—
420Seven-card stud300 / 60075
First interval · registration closes · 15 minute break
520No-limit hold’em200 / 400400
620Pot-limit Omaha200 / 400—
720Limit hold’em600 / 1,200—
820Seven-card stud600 / 1,200150
Second interval · 10 minute break
920No-limit hold’em400 / 800800
1020Pot-limit Omaha400 / 800—
1120Limit hold’em1,200 / 2,400—
1220Seven-card stud1,200 / 2,400300

Before you begin

diff --git a/docs/assets/yew-starter/preview/fonts/DMSerifDisplay-OFL.txt b/docs/assets/yew-starter/preview/fonts/DMSerifDisplay-OFL.txt new file mode 100644 index 0000000..be384f0 --- /dev/null +++ b/docs/assets/yew-starter/preview/fonts/DMSerifDisplay-OFL.txt @@ -0,0 +1,93 @@ +Copyright 2014-2018 Adobe (http://www.adobe.com/), with Reserved Font Name 'Source'. All Rights Reserved. Source is a trademark of Adobe in the United States and/or other countries. Copyright 2019 Google LLC. + +This Font Software is licensed under the SIL Open Font License, Version 1.1. + +This license is copied below, and is also available with a FAQ at: http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/docs/assets/yew-starter/preview/fonts/DMSerifDisplay-Regular.ttf b/docs/assets/yew-starter/preview/fonts/DMSerifDisplay-Regular.ttf new file mode 100644 index 0000000..bc5ed0c Binary files /dev/null and b/docs/assets/yew-starter/preview/fonts/DMSerifDisplay-Regular.ttf differ diff --git a/docs/assets/yew-starter/preview/fonts/Inter-OFL.txt b/docs/assets/yew-starter/preview/fonts/Inter-OFL.txt new file mode 100644 index 0000000..21f6aff --- /dev/null +++ b/docs/assets/yew-starter/preview/fonts/Inter-OFL.txt @@ -0,0 +1,93 @@ +Copyright 2020 The Inter Project Authors (https://github.com/rsms/inter) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/docs/assets/yew-starter/preview/fonts/Inter-Variable.ttf b/docs/assets/yew-starter/preview/fonts/Inter-Variable.ttf new file mode 100644 index 0000000..047c92f Binary files /dev/null and b/docs/assets/yew-starter/preview/fonts/Inter-Variable.ttf differ diff --git a/docs/assets/yew-starter/preview/style.css b/docs/assets/yew-starter/preview/style.css new file mode 100644 index 0000000..537b87a --- /dev/null +++ b/docs/assets/yew-starter/preview/style.css @@ -0,0 +1,57 @@ +@font-face { font-family: Inter; src: url('fonts/Inter-Variable.ttf') format('truetype'); font-weight: 100 900; } +@font-face { font-family: 'DM Serif Display'; src: url('fonts/DMSerifDisplay-Regular.ttf') format('truetype'); } +@page { size: A4; margin: 16mm 17mm; } +* { box-sizing: border-box; } +body { margin: 0; color: #213436; font-family: Inter, sans-serif; font-size: 9pt; line-height: 1.4; } +.masthead { background: #183b3f; color: #fffaf1; padding: 7mm 8mm; border-top: 2mm solid #c47d4e; } +.eyebrow { margin: 0 0 3mm; font-size: 8pt; text-transform: uppercase; letter-spacing: 2pt; color: #edd4b0; } +h1 { margin: 0 0 2mm; font-family: 'DM Serif Display', serif; font-size: 27pt; font-weight: 400; line-height: 1.1; } +.subtitle { margin: 0; color: #e0e8df; font-size: 9pt; } +.facts { display: flex; gap: 7mm; padding: 5mm 0; margin: 0 0 3mm; border-bottom: 0.3mm solid #c9d5d0; } +.facts div { flex: 1; } +dt { font-size: 7.5pt; color: #5f7270; text-transform: uppercase; letter-spacing: 0.6pt; } +dd { margin: 1mm 0 0; font-weight: 700; font-size: 11pt; } +h2 { margin: 0 0 2mm; font-size: 13pt; font-weight: 700; } +table { width: 100%; border-collapse: collapse; font-size: 8.6pt; } +caption { text-align: left; color: #5f7270; margin-bottom: 3mm; font-size: 8pt; } +th, td { text-align: left; padding: 2.2mm 2.4mm; border-bottom: 0.2mm solid #d4dcd7; vertical-align: top; } +thead { display: table-header-group; } +thead th { background: #e7eee8; font-size: 7.5pt; color: #254b49; font-weight: 700; } +tbody th { color: #254b49; font-weight: 700; } +tbody tr { break-inside: avoid; } +.numeric { text-align: right; white-space: nowrap; } +.break th { background: #f8eee0; color: #7a4d31; font-size: 8pt; font-weight: 400; padding-top: 2mm; padding-bottom: 2mm; } +.break-label { font-weight: 700; } +.notes { margin-top: 5mm; padding-left: 4mm; border-left: 0.8mm solid #c47d4e; } +.notes h2 { font-size: 10pt; } +.notes ul { margin: 0; padding-left: 4mm; font-size: 8pt; } +.notes li { margin-bottom: 1.2mm; } +footer { margin-top: 5mm; border-top: 0.2mm solid #c9d5d0; padding-top: 2mm; color: #5f7270; font-size: 7pt; } +footer p { margin: 0; } +@media screen { + body { background: #e7e9e3; padding: 24px; font-size: 14px; } + .sheet { background: #fffdf8; max-width: 850px; margin: 0 auto; padding: 32px; } + .table-scroll { overflow-x: auto; } + table { min-width: 560px; font-size: 13px; } + th, td { padding: 10px; } + caption, .notes ul { font-size: 12px; } + footer { font-size: 11px; } +} +@media screen and (max-width: 600px) { + body { padding: 0; } + .sheet { padding: 16px; } + .masthead { padding: 24px; } + .facts { flex-wrap: wrap; gap: 16px; } + .facts div { flex-basis: 42%; } +} +@media print { + body { background: #fff; padding: 0; font-size: 9pt; } + .sheet { margin: 0; padding: 0; max-width: none; } + .table-scroll { overflow: visible; } + table { min-width: 0; font-size: 8.6pt; } + th, td { padding: 2.2mm 2.4mm; } + caption, .notes ul { font-size: 8pt; } + footer { font-size: 7pt; } + .facts { flex-wrap: nowrap; gap: 7mm; } + .facts div { flex-basis: auto; } +} diff --git a/docs/assets/yew-starter/preview/unstyled.html b/docs/assets/yew-starter/preview/unstyled.html new file mode 100644 index 0000000..56f82a4 --- /dev/null +++ b/docs/assets/yew-starter/preview/unstyled.html @@ -0,0 +1,2 @@ + +Saturday mixed-game rotation

Riverstone / Community Series

Saturday mixed-game rotation

A sample structure for an afternoon around the table.

Starting stack
20,000 chips
Table size
6 players
Registration
Through level 4

The rotation

Levels, stakes and scheduled breaks
LevelMinutesGameBlinds / betsAnte
120No-limit hold’em100 / 200200
220Pot-limit Omaha100 / 200—
320Limit hold’em300 / 600—
420Seven-card stud300 / 60075
First interval · registration closes · 15 minute break
520No-limit hold’em200 / 400400
620Pot-limit Omaha200 / 400—
720Limit hold’em600 / 1,200—
820Seven-card stud600 / 1,200150
Second interval · 10 minute break
920No-limit hold’em400 / 800800
1020Pot-limit Omaha400 / 800—
1120Limit hold’em1,200 / 2,400—
1220Seven-card stud1,200 / 2,400300

Before you begin

diff --git a/docs/assets/yew-starter/project.zip b/docs/assets/yew-starter/project.zip new file mode 100644 index 0000000..1c54647 Binary files /dev/null and b/docs/assets/yew-starter/project.zip differ diff --git a/docs/assets/yew-starter/source.json b/docs/assets/yew-starter/source.json new file mode 100644 index 0000000..d7c7453 --- /dev/null +++ b/docs/assets/yew-starter/source.json @@ -0,0 +1,73 @@ +{ + "schema": "fullbleed.yew-starter.v1", + "prefix": "fullbleed-yew-sheet/", + "packages": { + "fullbleed": "2.5.34", + "yew": "0.23.0", + "tokio": "1.54.1" + }, + "files": [ + { + "path": "Cargo.lock", + "bytes": 25365, + "sha256": "ed41ad2e25af6f936f387d41d52e02090175cf7f1c74420bffb488955e1b1183" + }, + { + "path": "Cargo.toml", + "bytes": 456, + "sha256": "d0e1b775abdd443a40c42e25dd8a1a8d4a2d91db8b6f30bc92a29d24bafccdf9" + }, + { + "path": "LICENSE", + "bytes": 1079, + "sha256": "36a2115938f6e68cf0d87eccff7a6a3e57c17017e909cbd640d51aff1fa0c5a9" + }, + { + "path": "README.md", + "bytes": 3971, + "sha256": "62f09f6ed13546f7f93d8deb5cd858ea1bf87ee66f970aa1e4b4b74a42dd3ee5" + }, + { + "path": "data.json", + "bytes": 2035, + "sha256": "7044cade66216475b5ea370426513a50a6331b3f91ec4a8c45a50728281b68b2" + }, + { + "path": "fonts/DMSerifDisplay-OFL.txt", + "bytes": 4605, + "sha256": "a3e5cdd67d4571dd0a24fcc968de0efde7ae97ef752daf0906e4767619dd7231" + }, + { + "path": "fonts/DMSerifDisplay-Regular.ttf", + "bytes": 76580, + "sha256": "8cc3643535edf039aa5d95440a8542735e9197e4f4b8d9303e980fefbf5ab616" + }, + { + "path": "fonts/Inter-OFL.txt", + "bytes": 4377, + "sha256": "5b9321a4298cfeb6b34354164a1c3afc3db114569984c502b9b35d988fd58c57" + }, + { + "path": "fonts/Inter-Variable.ttf", + "bytes": 876576, + "sha256": "29160a80ff49ddcab2c97711247e08b1fab27a484a329ce8b813d820dc559031" + }, + { + "path": "src/document.rs", + "bytes": 4425, + "sha256": "7e4a2abb11dc1145a8ed437ab1d92d873e7a98129bcd1c1e6f95fe4719ede928" + }, + { + "path": "src/main.rs", + "bytes": 2695, + "sha256": "8820dcd75983f6eb3b88352d0d89681065a5eb5b92c2ea15adcbf3f5a5f3ea39" + }, + { + "path": "style.css", + "bytes": 3079, + "sha256": "f3564d1aa13f211d46365c51a16e891b59ccc0f10bf91f7082f48492302a56d5" + } + ], + "zip_bytes": 1007023, + "zip_sha256": "b88f585df911ad274aaf13177f68a1b3eeaf11aeb81186f46a21224af01f0201" +} diff --git a/docs/examples.md b/docs/examples.md index d379c7a..8381a57 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -160,6 +160,8 @@ For Cloudflare Workers, try the [invoice endpoint with local runtime checks](gui For C# web applications, try the [ASP.NET Core download starter](guides/aspnet-pdf.md). For Rust applications, use the [Axum PDF download starter](guides/axum-pdf.md) with an editable invoice template and bounded rendering work. +For component authoring, the optional [Yew schedule example](getting-started/rust.md#use-yew-components) +exports PDF and styled or plain HTML from typed Rust props. For PHP applications, use the [Laravel Blade and queue starter](guides/laravel-pdf.md) with automatic rendering, private downloads, retries and expiry cleanup. For a tagged document, use the [editable C# library notice](guides/csharp-tagged-pdf.md) diff --git a/docs/getting-started/rust.md b/docs/getting-started/rust.md index 37bcfa4..c33d409 100644 --- a/docs/getting-started/rust.md +++ b/docs/getting-started/rust.md @@ -145,6 +145,55 @@ for (index, png) in engine.render_finalized_pdf_image_pages("invoice.pdf", 96)?. This previews the finalized PDF, including its supported gradient fills. The designed example and downloaded playground projects use the same path. +## Use Yew components + +If your application already uses Yew, you can author a document with typed +props and its `html!` macro. This optional example renders a fictional event +schedule to PDF and exports the same content as styled or plain HTML. + +[![Riverstone event schedule with a teal title panel, serif heading, and a table of levels and breaks.](../assets/yew-starter/document.png)](../assets/yew-starter/document.pdf) + +[Download the Yew project](../assets/yew-starter/project.zip){ .md-button .md-button--primary } +[Open the PDF](../assets/yew-starter/document.pdf){ .md-button } + +[Styled HTML](../assets/yew-starter/preview/document.html) +· [HTML without styles](../assets/yew-starter/preview/unstyled.html) +· [Source checksums](../assets/yew-starter/source.json) + +Extract the ZIP, open a terminal in `fullbleed-yew-sheet`, and run: + +```bash +cargo run --release --locked -- data.json output +``` + +Open `output/document.pdf`, `output/document.html`, or `output/unstyled.html`. +Edit `data.json` for the content and `style.css` for the design. Choose a new +output directory after edits; the program refuses to overwrite one. + +The public `StructureSheet` component and its `SheetData` and `ScheduleRow` +types live in `src/document.rs`. Its `class="masthead"` attribute selects +`.masthead` in `style.css`; styling follows ordinary CSS class rules. Text +passed through component props is escaped by Yew. The native +`to_html(data, true).await` helper includes a link to `style.css`; +`to_html(data, false).await` leaves out that link and preserves the content +and classes. Keep the CSS and bundled `fonts/` directory beside styled HTML +when copying the export. + +The component module depends on Yew and Serde. `src/main.rs` handles fonts, +files, and Fullbleed PDF rendering. The native helper is excluded from WASM +builds, allowing the shared component to compile with Yew's `csr` feature in +a browser application and `ssr` in the native renderer. PDF generation uses +static HTML; it does not execute a client-side application. + +This separate example pins Fullbleed **2.5.34**, Yew **0.23.0**, and Tokio +**1.54.1**. [Yew describes SSR as experimental](https://yew.rs/docs/advanced-topics/server-side-rendering). +The downloadable project is built and run in documentation CI on Linux with +Rust 1.85.0 and Windows with Rust 1.97.0. Checks cover the one-page fixture, +a longer schedule, escaped text, repeated output, static HTML in Chromium, +and compilation of the shared component with Yew's browser feature. This is +a standalone example, not a tested integration with an existing application. +Review browser and paginated PDF layout independently when changing a design. + ## Keep building For a Rust web application, [download the Axum PDF starter](../guides/axum-pdf.md). diff --git a/examples/yew-sheet/Cargo.lock b/examples/yew-sheet/Cargo.lock new file mode 100644 index 0000000..cd5731b --- /dev/null +++ b/examples/yew-sheet/Cargo.lock @@ -0,0 +1,1021 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] +name = "bincode" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36eaf5d7b090263e8150820482d5d93cd964a81e4019913c972f4edcc6edb740" +dependencies = [ + "bincode_derive", + "serde", + "unty", +] + +[[package]] +name = "bincode_derive" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf95709a440f45e986983918d0e8a1f30a9b1df04918fc828670606804ac3c09" +dependencies = [ + "virtue", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "cfg-if" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" + +[[package]] +name = "console_error_panic_hook" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" +dependencies = [ + "cfg-if", + "wasm-bindgen", +] + +[[package]] +name = "equivalent" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00d174d5400e5e8fd687ad1049e2f578285fa914201b1af7e8b112a4546bd826" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fullbleed" +version = "2.5.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f966cd06546d726c4ebbead2b7206d990ea06ed62a88e183e6e8d2a3b40c587" +dependencies = [ + "fullbleed_audit_contract", + "subsetter", + "unicode-bidi", +] + +[[package]] +name = "fullbleed-yew-sheet" +version = "0.1.0" +dependencies = [ + "fullbleed", + "serde", + "serde_json", + "tokio", + "yew", +] + +[[package]] +name = "fullbleed_audit_contract" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61400ea6ceb63537e5089dd945823a33c293d7aa0765b76239f2d8ec2ef96127" + +[[package]] +name = "futures" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a31d2a3fbaaeb2af2368bbdd904aa8e812d3c04a1ee10d3171f52d556e5d0a3" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-io" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" + +[[package]] +name = "futures-macro" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "futures-sink" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "gloo" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d15282ece24eaf4bd338d73ef580c6714c8615155c4190c781290ee3fa0fd372" +dependencies = [ + "gloo-console", + "gloo-dialogs", + "gloo-events", + "gloo-file", + "gloo-history", + "gloo-net", + "gloo-render", + "gloo-storage", + "gloo-timers", + "gloo-utils", + "gloo-worker", +] + +[[package]] +name = "gloo-console" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a17868f56b4a24f677b17c8cb69958385102fa879418052d60b50bc1727e261" +dependencies = [ + "gloo-utils", + "js-sys", + "serde", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-dialogs" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf4748e10122b01435750ff530095b1217cf6546173459448b83913ebe7815df" +dependencies = [ + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-events" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27c26fb45f7c385ba980f5fa87ac677e363949e065a083722697ef1b2cc91e41" +dependencies = [ + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-file" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97563d71863fb2824b2e974e754a81d19c4a7ec47b09ced8a0e6656b6d54bd1f" +dependencies = [ + "gloo-events", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-history" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "903f432be5ba34427eac5e16048ef65604a82061fe93789f2212afc73d8617d6" +dependencies = [ + "getrandom", + "gloo-events", + "gloo-utils", + "serde", + "serde-wasm-bindgen", + "serde_urlencoded", + "thiserror 1.0.69", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-net" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43aaa242d1239a8822c15c645f02166398da4f8b5c4bae795c1f5b44e9eee173" +dependencies = [ + "futures-channel", + "futures-core", + "futures-sink", + "gloo-utils", + "http", + "js-sys", + "pin-project", + "serde", + "serde_json", + "thiserror 1.0.69", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "gloo-render" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56008b6744713a8e8d98ac3dcb7d06543d5662358c9c805b4ce2167ad4649833" +dependencies = [ + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-storage" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbc8031e8c92758af912f9bc08fbbadd3c6f3cfcbf6b64cdf3d6a81f0139277a" +dependencies = [ + "gloo-utils", + "js-sys", + "serde", + "serde_json", + "thiserror 1.0.69", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-timers" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "gloo-utils" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5555354113b18c547c1d3a98fbf7fb32a9ff4f6fa112ce823a21641a0ba3aa" +dependencies = [ + "js-sys", + "serde", + "serde_json", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-worker" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "085f262d7604911c8150162529cefab3782e91adb20202e8658f7275d2aefe5d" +dependencies = [ + "bincode 1.3.3", + "futures", + "gloo-utils", + "gloo-worker-macros", + "js-sys", + "pinned", + "serde", + "thiserror 1.0.69", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "gloo-worker-macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "956caa58d4857bc9941749d55e4bd3000032d8212762586fa5705632967140e7" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "hermit-abi" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17592d60ebacc7d5e169f4663c5f84f9161cc90328abcfe8456f41e4dfcb284" + +[[package]] +name = "html-escape" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9356095b4b41197bba32173600e1582792cda618f65d12f68e2e77d273413c5" + +[[package]] +name = "http" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "implicit-clone" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1689b939ee35e3a075b0834b5672efd43aec8a6e81a1c6002b76a5ca2f211ae0" +dependencies = [ + "implicit-clone-derive", + "indexmap", +] + +[[package]] +name = "implicit-clone-derive" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "699c1b6d335e63d0ba5c1e1c7f647371ce989c3bcbe1f7ed2b85fa56e3bd1a21" +dependencies = [ + "quote", + "syn 2.0.119", +] + +[[package]] +name = "indexmap" +version = "2.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "js-sys" +version = "0.3.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7883d941dae510fb2d978fc3fe018c71c9e2892fd38854de3e8b92c2e5ad9cc5" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.191" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65cd87c9a6d60536580b316ad985726104730a5151c99845f2490428ef91240a" + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "num_cpus" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pinned" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a829027bd95e54cfe13e3e258a1ae7b645960553fb82b75ff852c29688ee595b" +dependencies = [ + "futures", + "rustversion", + "thiserror 1.0.69", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.119", +] + +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde-wasm-bindgen" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8302e169f0eddcc139c70f139d19d6467353af16f9fce27e8c30158036a1e16b" +dependencies = [ + "js-sys", + "serde", + "wasm-bindgen", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "subsetter" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38803281d1c23166c5ebcb455439a5d2afe711cc909cf88af72448c297756ad6" +dependencies = [ + "rustc-hash", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e" +dependencies = [ + "thiserror-impl 2.0.21", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "tokio" +version = "1.54.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b52fd3255258d26fac7264ab5a76b53c13367136dc011d3d6ad4f8ed070396a3" +dependencies = [ + "pin-project-lite", + "tokio-macros", +] + +[[package]] +name = "tokio-macros" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "tokio-stream" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3d06f0b082ba57c26b79407372e57cf2a1e28124f78e9479fe80322cf53420b" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokise" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba44a1b36f42a95bd21b5e4acc1468547f75a73e7cf619312408d1f74c7fb687" +dependencies = [ + "futures", + "gloo", + "num_cpus", + "once_cell", + "pin-project", + "pinned", + "tokio", + "tokio-stream", + "wasm-bindgen-futures", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap", + "toml_datetime", + "winnow", +] + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + +[[package]] +name = "unicode-ident" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563" + +[[package]] +name = "unty" +version = "0.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "virtue" +version = "0.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "051eb1abcf10076295e815102942cc58f9d5e3b4560e46e53c21e8ff6f3af7b1" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bb54f33acc68fd454578d9820b0bde1a1a3d17aa17bb7b6595806d02886d409" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cbab34de2d982e9b48e18d216d04c4a6f641066ff19ffb699980f591ee3610e" +dependencies = [ + "js-sys", + "tokio", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e29d0c35b16e224a7eeb5cd2d25e3e1968fbd65604117b44d3b789d00ee8535" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f501a8bc3719dba86ef8ae4728879c08001bea749eb1333ac5b91e040e2a6b7" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 3.0.9", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23f0c9c52aa7cd7d77769a4cfe2a9adb1b331f489a41d912ce14513d5ab995c6" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88261b9deccee56594c11a3460c462c41f58d148598fe70ad77070126a68aba4" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + +[[package]] +name = "yew" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c154fadfa97eabdd3f3b79345ceeb9c05ee8f13f76d3d881d17fba618bf558d" +dependencies = [ + "base64ct", + "bincode 2.0.1", + "console_error_panic_hook", + "futures", + "gloo", + "html-escape", + "implicit-clone", + "indexmap", + "js-sys", + "rustversion", + "serde", + "slab", + "thiserror 2.0.21", + "tokio", + "tokise", + "tracing", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "yew-macro", +] + +[[package]] +name = "yew-macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08b883d84a035f57519d057f65a2a07ae25f1884ad485e1a9a523c9d880c1ad" +dependencies = [ + "prettyplease", + "proc-macro-error", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.119", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/examples/yew-sheet/Cargo.toml b/examples/yew-sheet/Cargo.toml new file mode 100644 index 0000000..fe6d88f --- /dev/null +++ b/examples/yew-sheet/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "fullbleed-yew-sheet" +version = "0.1.0" +edition = "2024" +rust-version = "1.85" +license = "MIT" +publish = false + +[dependencies] +fullbleed = { version = "=2.5.34", features = ["svg_raster"] } +serde = { version = "=1.0.229", features = ["derive"] } +serde_json = "=1.0.151" +tokio = { version = "=1.54.1", features = ["macros", "rt-multi-thread"] } +yew = { version = "=0.23.0", features = ["ssr"] } + +[profile.dev] +debug = 0 +incremental = false diff --git a/examples/yew-sheet/LICENSE b/examples/yew-sheet/LICENSE new file mode 100644 index 0000000..4c99bbd --- /dev/null +++ b/examples/yew-sheet/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Fullbleed Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/examples/yew-sheet/README.md b/examples/yew-sheet/README.md new file mode 100644 index 0000000..e6c300b --- /dev/null +++ b/examples/yew-sheet/README.md @@ -0,0 +1,78 @@ +# Yew components to PDF and HTML + +Write a document as Rust components, then export a PDF, styled browser HTML, +or HTML without a stylesheet. The sample is a fictional event schedule with +levels and break rows. It is useful for learning the rendering path; supply +your own event data and rules for an actual structure sheet. + +The example pins Fullbleed 2.5.34, Yew 0.23.0 and Tokio 1.54.1. Its fonts and +their license notices are included. PDF rendering does not launch a browser +or use system fonts. Yew is a dependency of this example, not of Fullbleed. + +## Run + +With a native Rust toolchain installed, run from this directory: + +```sh +cargo run --release --locked -- data.json output +``` + +The first build needs internet access to download Cargo dependencies. Open: + +- `output/document.pdf`: the print document. +- `output/document.html`: the styled browser document. +- `output/unstyled.html`: the same content and classes without a stylesheet. +- `output/page-1.png`: a preview of the finalized PDF; longer documents have + one PNG per page. + +Keep `style.css` and the `fonts` directory beside the styled HTML when copying +the export. Keep font license notices when redistributing the fonts. Choose a +new output directory for each run; the program refuses to overwrite one. + +## Change the content and design + +Edit `data.json` to change the title, event settings, levels, breaks and notes. +`SheetData` and `ScheduleRow` in `src/document.rs` define the input types. Text is +passed through Yew's ordinary text nodes, which escape HTML markup. + +`StructureSheet` is the reusable component. For example, its +`class="masthead"` attribute selects `.masthead` in `style.css`. Edit that +rule to change the title panel, or add your own classes and CSS rules. The +stylesheet is read at run time, so design edits do not require recompiling +the Rust component. Screen rules provide a responsive page and a horizontally +scrollable table on narrow displays; print rules supply the paper layout. + +The `to_html(data, styled)` function renders `SheetDocument` through +`yew::ServerRenderer`, with hydration disabled. `styled = true` includes a link +to `style.css`; `false` leaves that link out while preserving the content and +classes. Fullbleed receives the unstyled HTML and the CSS separately to create +the PDF. The browser HTML works without Fullbleed or JavaScript after export. + +When adding a font, register its bytes in the `AssetBundle`, add the matching +browser `@font-face` rule, and include its license notice. This example checks +the renderer's missing-glyph report before writing its outputs. + +## Reuse in an application + +The public `StructureSheet` component and native `to_html` function live in +`src/document.rs`, which depends on Yew and Serde, independently of Fullbleed. +`src/main.rs` handles files, fonts and PDF rendering. The document component +can be shared with an existing Yew application when +it can render from props without browser-only APIs. The PDF path needs the +resulting static HTML; it does not execute a client-side Yew application or +load a live webpage. In a web service, run synchronous PDF rendering outside +the asynchronous request executor and apply the service's own input limits. + +The `to_html` helper is excluded from browser WASM builds. The shared document +module can therefore use Yew's `csr` feature in a browser project and its `ssr` +feature in the native renderer. Yew currently describes SSR as experimental; +the checks for this sample cover its pinned versions and fixtures. + +See [Yew's server-rendering guide](https://yew.rs/docs/advanced-topics/server-side-rendering) +and [Fullbleed's Rust guide](https://docs.fullbleed.dev/getting-started/rust/) +for the two APIs involved. Browser layout and paginated PDF layout should be +reviewed independently when changing a design. + +This example was authored and checked by an AI coding agent for the Fullbleed +project. Its data is synthetic; it is not an integration with a third-party +application. diff --git a/examples/yew-sheet/data.json b/examples/yew-sheet/data.json new file mode 100644 index 0000000..25f4bfb --- /dev/null +++ b/examples/yew-sheet/data.json @@ -0,0 +1,29 @@ +{ + "series": "Riverstone / Community Series", + "title": "Saturday mixed-game rotation", + "subtitle": "A sample structure for an afternoon around the table.", + "starting_stack": "20,000 chips", + "table_size": "6 players", + "registration": "Through level 4", + "rows": [ + {"kind":"level","number":1,"minutes":20,"game":"No-limit hold’em","stakes":"100 / 200","ante":"200"}, + {"kind":"level","number":2,"minutes":20,"game":"Pot-limit Omaha","stakes":"100 / 200","ante":"—"}, + {"kind":"level","number":3,"minutes":20,"game":"Limit hold’em","stakes":"300 / 600","ante":"—"}, + {"kind":"level","number":4,"minutes":20,"game":"Seven-card stud","stakes":"300 / 600","ante":"75"}, + {"kind":"break","minutes":15,"label":"First interval · registration closes"}, + {"kind":"level","number":5,"minutes":20,"game":"No-limit hold’em","stakes":"200 / 400","ante":"400"}, + {"kind":"level","number":6,"minutes":20,"game":"Pot-limit Omaha","stakes":"200 / 400","ante":"—"}, + {"kind":"level","number":7,"minutes":20,"game":"Limit hold’em","stakes":"600 / 1,200","ante":"—"}, + {"kind":"level","number":8,"minutes":20,"game":"Seven-card stud","stakes":"600 / 1,200","ante":"150"}, + {"kind":"break","minutes":10,"label":"Second interval"}, + {"kind":"level","number":9,"minutes":20,"game":"No-limit hold’em","stakes":"400 / 800","ante":"800"}, + {"kind":"level","number":10,"minutes":20,"game":"Pot-limit Omaha","stakes":"400 / 800","ante":"—"}, + {"kind":"level","number":11,"minutes":20,"game":"Limit hold’em","stakes":"1,200 / 2,400","ante":"—"}, + {"kind":"level","number":12,"minutes":20,"game":"Seven-card stud","stakes":"1,200 / 2,400","ante":"300"} + ], + "notes": [ + "This is fictional layout data, not an event announcement or a complete set of tournament rules.", + "Blinds are shown for no-limit and pot-limit games; small and big bets are shown for limit games.", + "Provide your own event settings, break policy and complete rules before using a structure sheet." + ] +} diff --git a/examples/yew-sheet/fonts/DMSerifDisplay-OFL.txt b/examples/yew-sheet/fonts/DMSerifDisplay-OFL.txt new file mode 100644 index 0000000..be384f0 --- /dev/null +++ b/examples/yew-sheet/fonts/DMSerifDisplay-OFL.txt @@ -0,0 +1,93 @@ +Copyright 2014-2018 Adobe (http://www.adobe.com/), with Reserved Font Name 'Source'. All Rights Reserved. Source is a trademark of Adobe in the United States and/or other countries. Copyright 2019 Google LLC. + +This Font Software is licensed under the SIL Open Font License, Version 1.1. + +This license is copied below, and is also available with a FAQ at: http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/examples/yew-sheet/fonts/DMSerifDisplay-Regular.ttf b/examples/yew-sheet/fonts/DMSerifDisplay-Regular.ttf new file mode 100644 index 0000000..bc5ed0c Binary files /dev/null and b/examples/yew-sheet/fonts/DMSerifDisplay-Regular.ttf differ diff --git a/examples/yew-sheet/fonts/Inter-OFL.txt b/examples/yew-sheet/fonts/Inter-OFL.txt new file mode 100644 index 0000000..21f6aff --- /dev/null +++ b/examples/yew-sheet/fonts/Inter-OFL.txt @@ -0,0 +1,93 @@ +Copyright 2020 The Inter Project Authors (https://github.com/rsms/inter) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/examples/yew-sheet/fonts/Inter-Variable.ttf b/examples/yew-sheet/fonts/Inter-Variable.ttf new file mode 100644 index 0000000..047c92f Binary files /dev/null and b/examples/yew-sheet/fonts/Inter-Variable.ttf differ diff --git a/examples/yew-sheet/src/document.rs b/examples/yew-sheet/src/document.rs new file mode 100644 index 0000000..b4bdd13 --- /dev/null +++ b/examples/yew-sheet/src/document.rs @@ -0,0 +1,121 @@ +use serde::Deserialize; +use yew::prelude::*; + +#[derive(Clone, PartialEq, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct SheetData { + pub series: String, + pub title: String, + pub subtitle: String, + pub starting_stack: String, + pub table_size: String, + pub registration: String, + pub rows: Vec, + pub notes: Vec, +} + +#[derive(Clone, PartialEq, Deserialize)] +#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)] +pub enum ScheduleRow { + Level { + number: u16, + minutes: u16, + game: String, + stakes: String, + ante: String, + }, + Break { + minutes: u16, + label: String, + }, +} + +#[derive(Clone, PartialEq, Properties)] +pub struct SheetProps { + pub data: SheetData, +} + +#[component] +pub fn StructureSheet(props: &SheetProps) -> Html { + let data = &props.data; + html! { +
+
+

{&data.series}

+

{&data.title}

+

{&data.subtitle}

+
+
+
{"Starting stack"}
{&data.starting_stack}
+
{"Table size"}
{&data.table_size}
+
{"Registration"}
{&data.registration}
+
+
+

{"The rotation"}

+
+ + + + + + + + + + {for data.rows.iter().map(|row| match row { + ScheduleRow::Level {number, minutes, game, stakes, ante} => html! { + + + + + }, + ScheduleRow::Break {minutes, label} => html! { + + }, + })} +
{"Levels, stakes and scheduled breaks"}
{"Level"}{"Minutes"}{"Game"}{"Blinds / bets"}{"Ante"}
{number}{minutes}{game}{stakes}{ante}
+ {label}{" · "}{minutes}{" minute break"} +
+
+
+
+

{"Before you begin"}

+
    {for data.notes.iter().map(|note| html! {
  • {note}
  • })}
+
+

{"Illustrative structure · synthetic event data"}

+
+ } +} + +#[derive(Clone, PartialEq, Properties)] +#[cfg(not(target_arch = "wasm32"))] +struct DocumentProps { + data: SheetData, + styled: bool, +} + +#[cfg(not(target_arch = "wasm32"))] +#[component] +fn SheetDocument(props: &DocumentProps) -> Html { + html! { + + + + + {&props.data.title} + if props.styled { } + + + + } +} + +#[cfg(not(target_arch = "wasm32"))] +pub async fn to_html(data: SheetData, styled: bool) -> String { + let markup = + yew::ServerRenderer::::with_props(move || DocumentProps { data, styled }) + .hydratable(false) + .render() + .await; + format!("\n{markup}\n") +} diff --git a/examples/yew-sheet/src/main.rs b/examples/yew-sheet/src/main.rs new file mode 100644 index 0000000..82a1d31 --- /dev/null +++ b/examples/yew-sheet/src/main.rs @@ -0,0 +1,72 @@ +mod document; + +use document::{SheetData, to_html}; +use fullbleed::{Asset, AssetBundle, AssetKind, FullBleed}; +use std::{error::Error, fs, path::PathBuf}; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let args: Vec<_> = std::env::args_os().skip(1).map(PathBuf::from).collect(); + let [input, output] = args.as_slice() else { + return Err("Usage: fullbleed-yew-sheet INPUT.json NEW_OUTPUT_DIRECTORY".into()); + }; + if output.exists() { + return Err( + "Choose a new output directory so an older document cannot be overwritten.".into(), + ); + } + let data: SheetData = serde_json::from_slice(&fs::read(input)?)?; + if data.rows.is_empty() || data.rows.len() > 200 { + return Err("Supply between 1 and 200 schedule rows.".into()); + } + let html = to_html(data.clone(), true).await; + let unstyled = to_html(data, false).await; + let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + let css = fs::read_to_string(root.join("style.css"))?; + let font_files = [ + ("Inter", "Inter-Variable.ttf"), + ("DM Serif Display", "DMSerifDisplay-Regular.ttf"), + ]; + let mut fonts = AssetBundle::default(); + for (family, filename) in font_files { + fonts.add(Asset::new( + family.into(), + AssetKind::Font, + fs::read(root.join("fonts").join(filename))?, + None, + true, + )); + } + let engine = FullBleed::builder().register_bundle(fonts).build()?; + let (pdf, glyphs) = engine.render_with_glyph_report(&unstyled, &css)?; + if !glyphs.missing().is_empty() { + return Err("The bundled fonts do not cover all document characters.".into()); + } + fs::create_dir_all(output.join("fonts"))?; + fs::write(output.join("document.html"), html)?; + fs::write(output.join("unstyled.html"), unstyled)?; + fs::write(output.join("style.css"), css)?; + fs::write(output.join("document.pdf"), pdf)?; + for (_, filename) in font_files { + fs::copy( + root.join("fonts").join(filename), + output.join("fonts").join(filename), + )?; + } + for notice in ["Inter-OFL.txt", "DMSerifDisplay-OFL.txt"] { + fs::copy( + root.join("fonts").join(notice), + output.join("fonts").join(notice), + )?; + } + let pages = engine.render_finalized_pdf_image_pages(output.join("document.pdf"), 96)?; + for (index, png) in pages.iter().enumerate() { + fs::write(output.join(format!("page-{}.png", index + 1)), png)?; + } + println!( + "{} PDF page(s), styled HTML and unstyled HTML written to {}", + pages.len(), + output.display() + ); + Ok(()) +} diff --git a/examples/yew-sheet/style.css b/examples/yew-sheet/style.css new file mode 100644 index 0000000..537b87a --- /dev/null +++ b/examples/yew-sheet/style.css @@ -0,0 +1,57 @@ +@font-face { font-family: Inter; src: url('fonts/Inter-Variable.ttf') format('truetype'); font-weight: 100 900; } +@font-face { font-family: 'DM Serif Display'; src: url('fonts/DMSerifDisplay-Regular.ttf') format('truetype'); } +@page { size: A4; margin: 16mm 17mm; } +* { box-sizing: border-box; } +body { margin: 0; color: #213436; font-family: Inter, sans-serif; font-size: 9pt; line-height: 1.4; } +.masthead { background: #183b3f; color: #fffaf1; padding: 7mm 8mm; border-top: 2mm solid #c47d4e; } +.eyebrow { margin: 0 0 3mm; font-size: 8pt; text-transform: uppercase; letter-spacing: 2pt; color: #edd4b0; } +h1 { margin: 0 0 2mm; font-family: 'DM Serif Display', serif; font-size: 27pt; font-weight: 400; line-height: 1.1; } +.subtitle { margin: 0; color: #e0e8df; font-size: 9pt; } +.facts { display: flex; gap: 7mm; padding: 5mm 0; margin: 0 0 3mm; border-bottom: 0.3mm solid #c9d5d0; } +.facts div { flex: 1; } +dt { font-size: 7.5pt; color: #5f7270; text-transform: uppercase; letter-spacing: 0.6pt; } +dd { margin: 1mm 0 0; font-weight: 700; font-size: 11pt; } +h2 { margin: 0 0 2mm; font-size: 13pt; font-weight: 700; } +table { width: 100%; border-collapse: collapse; font-size: 8.6pt; } +caption { text-align: left; color: #5f7270; margin-bottom: 3mm; font-size: 8pt; } +th, td { text-align: left; padding: 2.2mm 2.4mm; border-bottom: 0.2mm solid #d4dcd7; vertical-align: top; } +thead { display: table-header-group; } +thead th { background: #e7eee8; font-size: 7.5pt; color: #254b49; font-weight: 700; } +tbody th { color: #254b49; font-weight: 700; } +tbody tr { break-inside: avoid; } +.numeric { text-align: right; white-space: nowrap; } +.break th { background: #f8eee0; color: #7a4d31; font-size: 8pt; font-weight: 400; padding-top: 2mm; padding-bottom: 2mm; } +.break-label { font-weight: 700; } +.notes { margin-top: 5mm; padding-left: 4mm; border-left: 0.8mm solid #c47d4e; } +.notes h2 { font-size: 10pt; } +.notes ul { margin: 0; padding-left: 4mm; font-size: 8pt; } +.notes li { margin-bottom: 1.2mm; } +footer { margin-top: 5mm; border-top: 0.2mm solid #c9d5d0; padding-top: 2mm; color: #5f7270; font-size: 7pt; } +footer p { margin: 0; } +@media screen { + body { background: #e7e9e3; padding: 24px; font-size: 14px; } + .sheet { background: #fffdf8; max-width: 850px; margin: 0 auto; padding: 32px; } + .table-scroll { overflow-x: auto; } + table { min-width: 560px; font-size: 13px; } + th, td { padding: 10px; } + caption, .notes ul { font-size: 12px; } + footer { font-size: 11px; } +} +@media screen and (max-width: 600px) { + body { padding: 0; } + .sheet { padding: 16px; } + .masthead { padding: 24px; } + .facts { flex-wrap: wrap; gap: 16px; } + .facts div { flex-basis: 42%; } +} +@media print { + body { background: #fff; padding: 0; font-size: 9pt; } + .sheet { margin: 0; padding: 0; max-width: none; } + .table-scroll { overflow: visible; } + table { min-width: 0; font-size: 8.6pt; } + th, td { padding: 2.2mm 2.4mm; } + caption, .notes ul { font-size: 8pt; } + footer { font-size: 7pt; } + .facts { flex-wrap: nowrap; gap: 7mm; } + .facts div { flex-basis: auto; } +} diff --git a/tools/build_yew_starter.py b/tools/build_yew_starter.py new file mode 100644 index 0000000..d6d6809 --- /dev/null +++ b/tools/build_yew_starter.py @@ -0,0 +1,42 @@ +"""Bundle the reviewed Yew example with exact font and source bytes.""" +from hashlib import sha256 +import json +from pathlib import Path +import tomllib +import zipfile + +ROOT = Path(__file__).resolve().parents[1] +PROJECT = ROOT / "examples/yew-sheet" +DEST = ROOT / "docs/assets/yew-starter" +PREFIX = "fullbleed-yew-sheet/" +FILES = ("Cargo.toml", "Cargo.lock", "README.md", "LICENSE", "data.json", "style.css", + "src/main.rs", "src/document.rs", "fonts/Inter-Variable.ttf", "fonts/Inter-OFL.txt", + "fonts/DMSerifDisplay-Regular.ttf", "fonts/DMSerifDisplay-OFL.txt") + + +def build(): + DEST.mkdir(parents=True, exist_ok=True) + files = [] + archive_path = DEST / "project.zip" + # Stored entries avoid platform compression differences for this small source kit. + with zipfile.ZipFile(archive_path, "w", compression=zipfile.ZIP_STORED) as archive: + for name in sorted(FILES): + data = (PROJECT / name).read_bytes() + entry = zipfile.ZipInfo(PREFIX + name, (2026, 10, 23, 0, 0, 0)) + entry.create_system = 3 + entry.external_attr = 0o100644 << 16 + archive.writestr(entry, data) + files.append(dict(path=name, bytes=len(data), sha256=sha256(data).hexdigest())) + cargo = tomllib.loads((PROJECT / "Cargo.toml").read_text(encoding="utf-8")) + packages = {name: cargo["dependencies"][name]["version"].removeprefix("=") + for name in ("fullbleed", "yew", "tokio")} + result = dict(schema="fullbleed.yew-starter.v1", prefix=PREFIX, packages=packages, + files=files, zip_bytes=archive_path.stat().st_size, + zip_sha256=sha256(archive_path.read_bytes()).hexdigest()) + (DEST / "source.json").write_text(json.dumps(result, indent=2) + "\n", encoding="utf-8", newline="\n") + print(json.dumps(dict(ok=True, files=len(files), bytes=result["zip_bytes"], sha256=result["zip_sha256"]))) + return result + + +if __name__ == "__main__": + build() diff --git a/tools/check_yew_starter_browser.py b/tools/check_yew_starter_browser.py new file mode 100644 index 0000000..c0ce2ca --- /dev/null +++ b/tools/check_yew_starter_browser.py @@ -0,0 +1,115 @@ +"""Check the actual Yew exports in an isolated Chromium session.""" +import argparse +from datetime import datetime, timezone +from functools import partial +from hashlib import sha256 +from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer +import json +from pathlib import Path +import threading +from playwright.sync_api import sync_playwright + +parser = argparse.ArgumentParser() +parser.add_argument("--root", type=Path, required=True) +parser.add_argument("--output", type=Path, required=True) +parser.add_argument("--chromium", type=Path) +args = parser.parse_args() +root, output = args.root.resolve(), args.output.resolve() +assert not output.exists() +output.mkdir(parents=True) +class QuietHandler(SimpleHTTPRequestHandler): + def log_message(self, *args): + pass +server = ThreadingHTTPServer(("127.0.0.1", 0), partial(QuietHandler, directory=str(root))) +threading.Thread(target=server.serve_forever, daemon=True).start() +base = f"http://127.0.0.1:{server.server_port}" +checks = [] +try: + with sync_playwright() as p: + options = {"headless": True} + if args.chromium: + options["executable_path"] = str(args.chromium.resolve()) + browser = p.chromium.launch(**options) + try: + for viewport in ({"width": 1280, "height": 1000}, {"width": 390, "height": 844}): + context = browser.new_context(viewport=viewport) + try: + for name, filename in (("default", "document.html"), ("default", "unstyled.html"), + ("long", "document.html"), ("escaped", "document.html")): + page = context.new_page() + errors, failed_responses = [], [] + page.on("pageerror", lambda error: errors.append(str(error))) + page.on("response", lambda response: failed_responses.append(response.url) if response.status >= 400 else None) + response = page.goto(f"{base}/{name}/{filename}", wait_until="networkidle", timeout=30000) + assert response.status == 200 + page.evaluate("document.fonts.ready") + assert not errors and not failed_responses + assert page.locator("script, img").count() == 0 + assert page.evaluate("typeof window.fullbleedInjected") == "undefined" + styled = filename == "document.html" + state = page.evaluate("""() => { + const region = document.querySelector('.table-scroll'); + const header = document.querySelector('.masthead'); + return { + documentWidth: document.documentElement.scrollWidth, + viewportWidth: document.documentElement.clientWidth, + background: getComputedStyle(header).backgroundColor, + tableWidth: region.scrollWidth, + regionWidth: region.clientWidth, + inter: document.fonts.check('16px Inter'), + serif: document.fonts.check('16px "DM Serif Display"'), + stylesheetCount: document.styleSheets.length + }; + }""") + if styled: + assert state["documentWidth"] <= state["viewportWidth"] + 1, state + assert state["background"] == "rgb(24, 59, 63)" + assert state["inter"] and state["serif"] and state["stylesheetCount"] == 1 + if viewport["width"] < 600: + region = page.get_by_role("region", name="Schedule table") + region.focus() + for _ in range(5): + page.keyboard.press("ArrowRight") + page.wait_for_function("document.querySelector('.table-scroll').scrollLeft > 0") + region.evaluate("""el => new Promise(resolve => { + let previous = -1, stable = 0; + function frame() { + const current = el.scrollLeft; + stable = current === previous ? stable + 1 : 0; + previous = current; + if (stable >= 6) resolve(); + else requestAnimationFrame(frame); + } + frame(); + })""") + region.evaluate("el => { el.scrollTo({left: 0, behavior: 'instant'}); el.blur(); }") + page.wait_for_function("document.querySelector('.table-scroll').scrollLeft === 0") + else: + assert state["stylesheetCount"] == 0 + assert page.locator("tr.level").count() == (64 if name == "long" else 12) + if name == "escaped": + assert page.get_by_role("heading", name="Riverstone & Friends", exact=True).count() == 1 + assert " ') +render("escaped", escaped) +escaped_html = HtmlDocument() +escaped_html.feed((OUT / "escaped/document.html").read_text(encoding="utf-8")) +assert not {"script", "img", "spring"}.intersection(escaped_html.tags) +assert escaped["notes"][-1] in escaped_html.text + +invalid = deepcopy(source) +invalid["rows"][0]["minutes"] = "twenty" +invalid_path = OUT / "invalid.json" +invalid_path.write_text(json.dumps(invalid), encoding="utf-8") +negative = subprocess.run([str(EXE), str(invalid_path), str(OUT / "invalid")], + text=True, encoding="utf-8", capture_output=True, timeout=30) +assert negative.returncode != 0 and not (OUT / "invalid").exists() +before = (OUT / "default/document.pdf").read_bytes() +overwrite = subprocess.run([str(EXE), str(PROJECT / "data.json"), str(OUT / "default")], + text=True, encoding="utf-8", capture_output=True, timeout=30) +assert overwrite.returncode != 0 and (OUT / "default/document.pdf").read_bytes() == before +result = dict(ok=True, at=datetime.now(timezone.utc).isoformat(), packages=build["packages"], + executable_sha256=build["executable_sha256"], documents=records, + repeat_pdf_bytes_equal=True, long_rows_complete_once=True, + long_table_headers_repeat=True, escaped_text_not_markup=True, + invalid_data_rejected_before_output=True, existing_output_preserved=True, + visual_review_pending=True, + scope="Standalone Yew SSR fixtures; not an application integration or accessibility certification.") +(OUT / "verification.json").write_text(json.dumps(result, indent=2) + "\n", encoding="utf-8") +print(json.dumps({"ok": True, "documents": len(records), "output": str(OUT)}), flush=True) diff --git a/tools/verify_yew_starter.py b/tools/verify_yew_starter.py new file mode 100644 index 0000000..c632687 --- /dev/null +++ b/tools/verify_yew_starter.py @@ -0,0 +1,139 @@ +"""Build the actual Yew source download and check its native and browser boundaries.""" +import argparse +from datetime import datetime, timezone +from hashlib import sha256 +from importlib.metadata import version +import json +import os +from pathlib import Path, PurePosixPath +import platform +import shutil +import subprocess +import sys +import zipfile + +from PIL import Image + +ROOT = Path(__file__).resolve().parents[1] +ASSETS = ROOT / "docs/assets/yew-starter" + + +def digest(path): + return sha256(path.read_bytes()).hexdigest() + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--output", type=Path, default=ROOT / "target/yew-starter") + parser.add_argument("--target-dir", type=Path, default=ROOT / "examples/yew-sheet/target") + args = parser.parse_args() + out = args.output.resolve() + out.mkdir(parents=True, exist_ok=False) + env = {**os.environ, "CARGO_TARGET_DIR": str(args.target_dir.resolve())} + for name in ("RUST_MIN_STACK", "RUSTFLAGS", "CARGO_ENCODED_RUSTFLAGS"): + env.pop(name, None) + report = dict(ok=False, at=datetime.now(timezone.utc).isoformat(), + platform=platform.platform(), checks=[], commands=[], + verifier_versions={name: version(name) for name in ("pypdf", "pypdfium2", "pillow")}) + + def check(label, condition): + if not condition: + raise AssertionError(label) + report["checks"].append(label) + + def run(label, command, cwd): + print("Checking " + label, flush=True) + result = subprocess.run(list(map(str, command)), cwd=cwd, env=env, + capture_output=True, text=True, encoding="utf-8", timeout=1200) + (out / (label + ".stdout.txt")).write_text(result.stdout, encoding="utf-8") + (out / (label + ".stderr.txt")).write_text(result.stderr, encoding="utf-8") + report["commands"].append(dict(name=label, exit_code=result.returncode)) + check(label + " exit status", result.returncode == 0) + return result.stdout + + try: + manifest = json.loads((ASSETS / "source.json").read_text(encoding="utf-8")) + archive_path = ASSETS / "project.zip" + check("source archive checksum and size", digest(archive_path) == manifest["zip_sha256"] + and archive_path.stat().st_size == manifest["zip_bytes"]) + prefix = manifest["prefix"] + check("expected archive root", prefix == "fullbleed-yew-sheet/") + workspace = out / "extracted" + with zipfile.ZipFile(archive_path) as archive: + names = [prefix + item["path"] for item in manifest["files"]] + check("archive contains exactly the declared files", sorted(archive.namelist()) == sorted(names) + and len(names) == len(set(names))) + check("archive paths are confined", all(not PurePosixPath(name).is_absolute() + and ".." not in PurePosixPath(name).parts and "\\" not in name + and ":" not in name for name in names)) + for item in manifest["files"]: + data = archive.read(prefix + item["path"]) + check("source digest: " + item["path"], len(data) == item["bytes"] + and sha256(data).hexdigest() == item["sha256"]) + archive.extractall(workspace) + project = workspace / prefix + command = "cargo run --release --locked -- data.json output" + check("documented command appears in README and guide", all(command in path.read_text(encoding="utf-8") + for path in (project / "README.md", ROOT / "docs/getting-started/rust.md"))) + lock_hash = digest(project / "Cargo.lock") + run("build", ["cargo", "build", "--release", "--locked"], project) + metadata = json.loads(run("metadata", ["cargo", "metadata", "--format-version", "1", "--locked"], project)) + packages = {} + for name, expected in manifest["packages"].items(): + package = next(p for p in metadata["packages"] if p["name"] == name) + check("published pinned dependency: " + name, package["version"] == expected + and package["source"].startswith("registry+")) + packages[name] = package["version"] + executable = Path(metadata["target_directory"]) / "release" / ("fullbleed-yew-sheet" + (".exe" if os.name == "nt" else "")) + build = dict(executable=str(executable), executable_sha256=digest(executable), packages=packages, + font_hashes={Path(f["path"]).name: f["sha256"] for f in manifest["files"] if f["path"].startswith("fonts/")}) + metadata_path = out / "build.json" + metadata_path.write_text(json.dumps(build, indent=2) + "\n", encoding="utf-8") + report.update(packages=packages, source_zip_sha256=manifest["zip_sha256"], + rustc=run("rustc", ["rustc", "--version"], project).strip()) + run("native-outputs", [sys.executable, ROOT / "tools/verify_yew_outputs.py", "--project", project, + "--metadata", metadata_path, "--output", out / "documents"], project) + report["native"] = json.loads((out / "documents/verification.json").read_text(encoding="utf-8")) + run("readme-command", command.split(), project) + generated = out / "documents/default" + for path in sorted((project / "output").rglob("*")): + if path.is_file(): + relative = path.relative_to(project / "output") + check("README command output: " + relative.as_posix(), path.read_bytes() == (generated / relative).read_bytes()) + check("public PDF fixture matches generated bytes", (ASSETS / "document.pdf").read_bytes() == (generated / "document.pdf").read_bytes()) + for path in sorted((ASSETS / "preview").rglob("*")): + if path.is_file(): + relative = path.relative_to(ASSETS / "preview") + check("public HTML export: " + relative.as_posix(), path.read_bytes() == (generated / relative).read_bytes()) + with Image.open(ASSETS / "document.png") as published, Image.open(generated / "pdfium-1.png") as actual: + published.load() + actual.load() + check("public preview format, mode, dimensions, metadata and pixels match", + published.format == actual.format == "PNG" and published.mode == actual.mode + and published.size == actual.size and published.info == actual.info + and published.tobytes() == actual.tobytes()) + fixture = out / "client-check" + shutil.copytree(ROOT / "tools/fixtures/yew-client", fixture) + shutil.copy2(project / "src/document.rs", fixture / "src/document.rs") + shutil.copy2(project / "data.json", fixture / "data.json") + client_lock_hash = digest(fixture / "Cargo.lock") + run("client-check", ["cargo", "check", "--target", "wasm32-unknown-unknown", "--locked"], fixture) + client_metadata = json.loads(run("client-metadata", ["cargo", "metadata", "--format-version", "1", "--locked"], fixture)) + check("browser component has no Fullbleed dependency", not any(p["name"] == "fullbleed" for p in client_metadata["packages"])) + yew = next(p for p in client_metadata["packages"] if p["name"] == "yew") + features = next(n["features"] for n in client_metadata["resolve"]["nodes"] if n["id"] == yew["id"]) + check("browser component uses Yew CSR without SSR", "csr" in features and "ssr" not in features) + check("both Cargo lockfiles unchanged", digest(project / "Cargo.lock") == lock_hash + and digest(fixture / "Cargo.lock") == client_lock_hash) + report["ok"] = True + report["scope"] = "Pinned native fixtures and browser-component compilation; browser execution is checked separately for static HTML exports." + except Exception as error: + report["error"] = str(error) + raise + finally: + (out / "verification.json").write_text(json.dumps(report, indent=2) + "\n", encoding="utf-8") + print(json.dumps(dict(ok=True, checks=len(report["checks"]), output=str(out))), flush=True) + + +if __name__ == "__main__": + main()