MIT licensed · v0.1.0 · pre-1.0

slipsheet

Inline PDF reading for the web. Two small libraries joined only by plain HTML, so nothing breaks when one of them does.

Live. A real PDF, rendered by the viewer you would install.

Page through it with the toolbar, or with arrow keys once it has focus. Press F for fullscreen.

The whole contract is one div

The editor plugin writes this. The viewer reads it. Neither one knows the other exists, and this markup is the only thing they agree on.

<div class="slipsheet"
     data-src="https://cdn.example.com/report.pdf"
     data-pages="12"
     data-filename="quarterly-report.pdf">
  <a href="https://cdn.example.com/report.pdf"
     download="quarterly-report.pdf">
    Download quarterly-report.pdf (12 pages)
  </a>
</div>

That nested link is the entire trick. It is what stays behind when JavaScript is off, when PDF.js fails to load, or when the viewer is never installed at all. The button above removes the viewer from the live demo so you can watch it happen instead of taking our word for it.

Install

Take one or take both. The viewer is useful with any editor, or none.

npm install @slipsheet/viewer @slipsheet/hugerte

Render stored content

import { Slipsheet } from '@slipsheet/viewer';
import '@slipsheet/viewer/style';

Slipsheet.init();   // hydrates every .slipsheet on the page

Author it in an editor

hugerte.init({            // or tinymce.init on TinyMCE 6
  plugins: 'slipsheet',
  toolbar: 'slipsheet',
  slipsheet_upload_handler: async (file, progress) => {
    const { url, pages } = await myUpload(file, progress);
    return { src: url, pages, filename: file.name };
  },
  extended_valid_elements:
    'div[class|data-src|data-pages|data-filename|contenteditable],a[href|download|target]',
  convert_urls: false,
});

Both of those last two options are required. The first keeps the editor from stripping the embed's attributes on save; the second stops it rewriting the fallback link to a different path than the viewer reads.

The upload handler is yours. slipsheet never talks to a backend, and has no opinion about where your files live.

Why it is built this way

Nothing depends on JavaScript
Stored content is plain HTML with a working download link inside it. Every failure mode, including the total absence of this library, leaves the reader with something they can use.
Neither half knows about the other
The plugin cannot render and the viewer cannot upload. They share no runtime and no build step. Bring your own editor, or your own renderer, and the other piece still works.
PDF.js is fetched, not bundled
It loads on first use from a URL you control, so you get lazy loading and PDF.js security updates without waiting on a slipsheet release.
Keyboard and screen reader are not an afterthought
Arrow keys, Home, End, and F for fullscreen. Page changes announce through aria-live. Motion respects prefers-reduced-motion. Try it above.