Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion examples/browser/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,12 @@ not upload document inputs to a PDF service. Nothing persists after a reload.
## Make your template

Change the invoice fields, or open **Edit the HTML & CSS** below the preview.
Edits in that editor last until reload. To keep them in the project, edit:
Use **Save HTML** and **Save CSS** to download the selected template's current
source, including your edits and any placeholders. Saving does not require a
successful PDF render. The files are named `invoice.html` / `invoice.css` or
`report.html` / `report.css`; put them in the project's `src/` folder to use them
after a reload. The downloads contain template source, not the values filled
into the last PDF. You can also edit these files directly:

- `src/invoice.html` and `src/invoice.css`
- `src/report.html` and `src/report.css`
Expand Down
9 changes: 7 additions & 2 deletions examples/browser/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,18 @@ <h1>A proper document.<br>Made in your browser.</h1>
</div>
<details class="template-editor" id="template-editor">
<summary>Edit the HTML &amp; CSS</summary>
<p class="editor-note">Make the template your own. Invoice fields replace <code>{{customer}}</code> and <code>{{reference}}</code>; <code>{{ink}}</code> applies the color in CSS. Edits stay in this tab until you reload. Fullbleed uses static print HTML and CSS; document scripts do not run.</p>
<p class="editor-note">Make the template your own. Invoice fields replace <code>{{customer}}</code> and <code>{{reference}}</code>; <code>{{ink}}</code> applies the color in CSS. Save your HTML and CSS before leaving to keep your edits, including these placeholders. Fullbleed uses static print HTML and CSS; document scripts do not run.</p>
<fieldset id="editor-fields"><legend class="sr-only">Template source</legend><div class="editor-grid">
<div><label for="html-source">HTML</label><textarea id="html-source" spellcheck="false" aria-label="Document HTML"></textarea></div>
<div><label for="css-source">Print CSS</label><textarea id="css-source" spellcheck="false" aria-label="Document print CSS"></textarea></div>
</div></fieldset>
<div class="editor-actions"><button type="button" id="render-source">Generate edited PDF</button><button type="button" class="secondary" id="reset-source">Reset this template</button></div>
<div class="editor-actions"><button type="button" id="render-source">Generate edited PDF</button><button type="button" class="secondary" id="reset-source">Reset this template</button><div class="source-downloads" role="group" aria-label="Save template source"><button type="button" class="source-save" id="save-html">Save HTML</button><button type="button" class="source-save" id="save-css">Save CSS</button></div></div>
</details>
<section class="next-step" aria-labelledby="next-step-heading">
<h2 id="next-step-heading">Use this in your app</h2>
<p>Download the complete browser starter, then put your saved HTML and CSS in its <code>src/</code> folder. It includes the templates, renderer setup, and dependency lockfile.</p>
<div><a href="https://docs.fullbleed.dev/assets/browser-starter/project.zip">Download starter <span aria-hidden="true">↓</span></a><a href="https://docs.fullbleed.dev/guides/browser-pdf/">Setup guide <span aria-hidden="true">↗</span></a></div>
</section>
<footer><p>Static HTML. Deterministic print layout. Open source.</p><a href="https://docs.fullbleed.dev/">Built with Fullbleed <span aria-hidden="true">↗</span></a></footer>
</main>
<script type="module" src="/src/main.js"></script>
Expand Down
10 changes: 10 additions & 0 deletions examples/browser/src/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,16 @@ $('reset-source').addEventListener('click', () => {
loadSource(); markChanged();
});
$('render-source').addEventListener('click', () => $('design-form').requestSubmit());
function saveSource(extension) {
const design = $('kind').value;
const type = extension === 'html' ? 'text/html' : 'text/css';
const url = URL.createObjectURL(new Blob([templates[design][extension]], { type: `${type};charset=utf-8` }));
const link = document.createElement('a');
link.href = url; link.download = `${design}.${extension}`; link.click();
setTimeout(() => URL.revokeObjectURL(url), 60_000);
}
$('save-html').addEventListener('click', () => saveSource('html'));
$('save-css').addEventListener('click', () => saveSource('css'));
$('cancel').addEventListener('click', () => controller?.abort());
$('download').addEventListener('click', () => {
if (!pdfUrl) return;
Expand Down
1 change: 1 addition & 0 deletions examples/browser/src/style.css

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

34 changes: 29 additions & 5 deletions tools/check-browser-starter.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ def log_message(self,*a):pass
server=ThreadingHTTPServer(('127.0.0.1',0),Handler)
thread=threading.Thread(target=server.serve_forever,daemon=True);thread.start()
url=f'http://127.0.0.1:{server.server_port}{prefix}'
checks=[];network=[];errors=[];documents=[]
checks=[];network=[];errors=[];documents=[];sources=[]
def check(name,passed):
assert passed,name
checks.append(name);print(name+': passed',flush=True)
Expand All @@ -66,6 +66,13 @@ def download(name,pages,text):
check(name+': download has the expected pages and text',len(pdf.pages)==pages and all(t in contents for t in text))
documents.append({'name':name,'filename':item.suggested_filename,'pages':len(pdf.pages),'sha256':hashlib.sha256(path.read_bytes()).hexdigest()})
return path
def save_source(design,extension,expected,name):
with page.expect_download() as event:page.locator('#save-'+extension).click()
item=event.value;path=out/(name+'.'+extension);item.save_as(path)
check(name+': source download preserves the edited UTF-8 bytes and template filename',
item.suggested_filename==design+'.'+extension and path.read_bytes()==expected.encode('utf-8'))
sources.append({'name':name,'filename':item.suggested_filename,'bytes':path.stat().st_size,
'sha256':hashlib.sha256(path.read_bytes()).hexdigest()})
try:
page.goto(url,wait_until='domcontentloaded');ready()
check('runtime resolves beneath a nested deployment path',any('/nested/demo/fullbleed/worker.js' in r['url'] for r in network))
Expand All @@ -89,26 +96,43 @@ def download(name,pages,text):
download('customized',1,['Birch & <Briar>','TEST-2042'])
check('customized bytes differ from the default invoice',documents[-1]['sha256']!=documents[0]['sha256'])
page.locator('#template-editor summary').click()
page.locator('#html-source').fill('<h1>Edited template</h1><p>{{customer}} / {{reference}}</p><script>window.documentScriptExecuted = true;</script>')
page.locator('#css-source').fill('@page { size: A5; margin: 15mm } body { font-family: Helvetica } h1 { color: {{ink}}; font-size: 24pt }')
edited_html='<h1>Edited template</h1>\n<p>{{customer}} / {{reference}}</p><!-- Montréal, 日本語, 😀, &<>" -->\n<script>window.documentScriptExecuted = true;</script>'
edited_css='/* Color: café & "ink" */\n@page { size: A5; margin: 15mm } body { font-family: Helvetica } h1 { color: {{ink}}; font-size: 24pt }\n'
page.locator('#html-source').fill(edited_html)
page.locator('#css-source').fill(edited_css)
check('source can be saved before rendering pending edits',page.locator('#download').is_disabled())
save_source('invoice','html',edited_html,'edited-invoice')
save_source('invoice','css',edited_css,'edited-invoice')
page.locator('#render-source').click();ready()
path=download('edited',1,['Edited template','Birch & <Briar>','TEST-2042'])
check('edited print CSS changes the PDF page size',abs(float(PdfReader(path).pages[0].mediabox.width)-419.52756)<1)
check('document script does not run on the page',page.evaluate('window.documentScriptExecuted !== true'))
verify_edited_preview(page, path, out, check)
page.screenshot(path=str(out/'edited-template.png'),full_page=True)
for width,height in [(390,844),(320,760)]:
page.set_viewport_size({'width':width,'height':height})
check(f'{width}px: expanded editor and source downloads fit the viewport',page.evaluate('document.documentElement.scrollWidth <= innerWidth'))
page.screenshot(path=str(out/f'editor-width-{width}.png'),full_page=True)
page.set_viewport_size({'width':1440,'height':1000})
verify_engine_fixes(page, out, check, ready, manual=True)
page.locator('#kind').select_option('report');page.locator('#generate').click();ready()
page.locator('#kind').select_option('report')
report_html=page.locator('#html-source').input_value()+'\n<!-- Saved report edit -->'
page.locator('#html-source').fill(report_html)
save_source('report','html',report_html,'edited-report')
save_source('report','css',page.locator('#css-source').input_value(),'edited-report')
page.locator('#generate').click();ready()
download('report',3,['COMMON','1,240','420,000.00'])
check('multi-page preview displays all report pages',page.locator('#preview img').count()==3)
page.locator('#kind').select_option('invoice')
check('each design preserves its template edits',page.locator('#html-source').input_value().startswith('<h1>Edited template'))
save_source('invoice','html',edited_html,'invoice-after-switch')
page.locator('#reset-source').click()
check('reset restores only the selected template', 'Northstar' in page.locator('#html-source').input_value())
page.locator('#html-source').fill('')
page.locator('#render-source').click()
expect(page.locator('#status')).to_contain_text('nonempty')
check('invalid input displays an error without a stale download',page.locator('#download').is_disabled())
save_source('invoice','html','','empty-html-after-error')
page.locator('#reset-source').click()
pause_engine.set()
page.locator('#generate').click()
Expand All @@ -122,7 +146,7 @@ def download(name,pages,text):
local_blob=f'blob:http://127.0.0.1:{server.server_port}/'
check('no document uploads or cross-origin runtime requests',all(r['method']=='GET' and not r['body'] and r['url'].startswith((url,local_blob)) for r in network))
check('no uncaught page errors',not errors)
report={'ok':True,'checked_at':datetime.now(timezone.utc).isoformat(),'browser_engine':args.browser,'browser':browser.version,'playwright':version('playwright'),'pypdf':version('pypdf'),'checks':checks,'documents':documents,'network':network,'page_errors':errors,'csp':policy,'scope':'Owned browser; synthetic inputs; production build served at /nested/demo/ with no PDF server.'}
report={'ok':True,'checked_at':datetime.now(timezone.utc).isoformat(),'browser_engine':args.browser,'browser':browser.version,'playwright':version('playwright'),'pypdf':version('pypdf'),'checks':checks,'documents':documents,'source_downloads':sources,'network':network,'page_errors':errors,'csp':policy,'scope':'Owned browser; synthetic inputs; production build served at /nested/demo/ with no PDF server; exact source downloads including unsaved edits and failed renders.'}
(out/'verification.json').write_text(json.dumps(report,indent=2)+'\n',encoding='utf-8')
except Exception:
page.screenshot(path=str(out/'failure.png'),full_page=True)
Expand Down
Loading
Loading