Write on paper: a sheet per page, with a gap between them
The writing surface was a column of text in an empty pane. A sheet was drawn behind it, but only once you started drawing — so writing looked like text floating in the middle of nothing, and the space either side of the column read as a void rather than as a margin. Now every page is a sheet of its own, centred, with a gap after it: the same way the PDF reader draws its pages, because a page in a note and a page in a document should read as the same kind of thing. The gap is the part that takes work. The browser knows nothing about the sheets and lays the text out as one flow, putting a line straight across the gap and cutting it in half. So editor/pagination.js measures the flow and pushes any block that would straddle a gap onto the next page, with a spacer of exactly the right height in front of it. A paragraph moves down entire, which is what a page break means on paper. A block taller than a page is left to run over, since splitting one would mean laying out its insides ourselves. The spacers are decorations, not content: they never enter the document, never reach the server and never appear in an export. Two checks hold that down — one asks whether any block sits in a gap, the other reads the stored body back and looks for them there. Two things that are easy to get wrong. The spacer's height is not the distance the text moves: margins that used to collapse into each other no longer do with something between them, so the layout gains that much for free and the spacer is made exactly that much shorter. And heights change with no transaction to announce it — a webfont landing, an image arriving, the window resizing — which a ResizeObserver on the editor element catches. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
3ce867c6d0
commit
97a3764d92
9 files changed
+492
-47
No files matched your search
@@ -180,10 +180,43 @@ try {
|
||||
nearEnd
|
||||
);
|
||||
|
||||
/* ── the paper ──
|
||||
* Each page is a sheet of its own with a gap after it, the way the PDF reader
|
||||
* draws its pages. The gap is the part that has to be got right: the browser
|
||||
* knows nothing about our sheets and would lay a line of text straight across
|
||||
* it, so the text flow holds it open — see editor/pagination.js. */
|
||||
check(
|
||||
(await p.locator("[data-page-sheet]").count()) === Number((await position()).match(/of (\d+)/)[1]),
|
||||
"there is a sheet of paper for every page",
|
||||
`${await p.locator("[data-page-sheet]").count()} sheets, ${await position()}`
|
||||
);
|
||||
const straddling = await p.evaluate(() => {
|
||||
const pm = document.querySelector(".ProseMirror");
|
||||
const top = pm.getBoundingClientRect().top;
|
||||
const sheets = [...document.querySelectorAll("[data-page-sheet]")].map((el) => el.getBoundingClientRect());
|
||||
const gaps = sheets.slice(0, -1).map((r, i) => ({ from: r.bottom - top, to: sheets[i + 1].top - top }));
|
||||
const bad = [];
|
||||
for (const el of pm.children) {
|
||||
if (el.classList.contains("ne-page-spacer")) continue;
|
||||
const r = el.getBoundingClientRect();
|
||||
if (r.height === 0) continue;
|
||||
for (const g of gaps) {
|
||||
if (r.bottom - top > g.from + 0.5 && r.top - top < g.to - 0.5) bad.push((el.textContent || "").slice(0, 30));
|
||||
}
|
||||
}
|
||||
return bad;
|
||||
});
|
||||
check(straddling.length === 0, "and no line of text is cut in half by a gap", straddling.join(" | "));
|
||||
check(
|
||||
(await p.locator(".ne-page-spacer").count()) > 0,
|
||||
"which takes holding the gap open inside the text itself"
|
||||
);
|
||||
|
||||
/* The note is still one document — pages are drawn, not stored. */
|
||||
const stored = (await api(p, "GET", "/notes")).body[0];
|
||||
const body = (await api(p, "GET", `/notes/${stored.id}`)).body.content;
|
||||
check(!/"type"\s*:\s*"page"/.test(body), "no page ever reaches the stored document");
|
||||
check(!/page-spacer/.test(body), "and neither does anything holding a gap open");
|
||||
|
||||
/* ── alignment ── */
|
||||
// Left is the default and is never written into the document, so an untouched
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
/* Throwaway: four pages, and what that stray placeholder is. */
|
||||
import { launch, withFreshUser, createNotebook, createNote, OUT } from "./lib.mjs";
|
||||
|
||||
const browser = await launch();
|
||||
const s = await withFreshUser(browser, "probe");
|
||||
const p = s.page;
|
||||
p.on("pageerror", (e) => console.log("PAGEERROR", String(e)));
|
||||
|
||||
await p.setViewportSize({ width: 1680, height: 950 });
|
||||
await createNotebook(p, "Papier");
|
||||
await createNote(p);
|
||||
await p.waitForTimeout(1500);
|
||||
await p.locator(".ProseMirror").click();
|
||||
// Paste rather than type: typing this much loses keystrokes and says nothing
|
||||
// about the layout, which is what is being looked at here.
|
||||
await p.evaluate(() => {
|
||||
const text = Array.from({ length: 160 }, (_, i) => `Absatz ${i + 1}: Dichtungen altern durch Wärme und Zeit.`).join("\n");
|
||||
const dt = new DataTransfer();
|
||||
dt.setData("text/plain", text);
|
||||
document.querySelector(".ProseMirror").dispatchEvent(
|
||||
new ClipboardEvent("paste", { clipboardData: dt, bubbles: true, cancelable: true })
|
||||
);
|
||||
});
|
||||
await p.waitForTimeout(4000);
|
||||
|
||||
console.log(
|
||||
JSON.stringify(
|
||||
await p.evaluate(() => {
|
||||
const pm = document.querySelector(".ProseMirror");
|
||||
const pmTop = pm.getBoundingClientRect().top;
|
||||
const sheets = [...document.querySelectorAll("[data-page-sheet]")];
|
||||
const gaps = sheets.slice(0, -1).map((el, i) => ({
|
||||
from: el.getBoundingClientRect().bottom - pmTop,
|
||||
to: sheets[i + 1].getBoundingClientRect().top - pmTop,
|
||||
}));
|
||||
const inGap = [];
|
||||
for (const el of pm.children) {
|
||||
if (el.classList.contains("ne-page-spacer")) continue;
|
||||
const r = el.getBoundingClientRect();
|
||||
if (r.height === 0) continue;
|
||||
const top = r.top - pmTop,
|
||||
bottom = r.bottom - pmTop;
|
||||
for (const g of gaps) {
|
||||
if (bottom > g.from + 0.5 && top < g.to - 0.5) inGap.push((el.textContent || "").slice(0, 24));
|
||||
}
|
||||
}
|
||||
const placeholder = [...pm.querySelectorAll("[data-placeholder]")].map((el) => ({
|
||||
text: (el.textContent || "").slice(0, 20),
|
||||
first: el === pm.firstElementChild,
|
||||
top: Math.round(el.getBoundingClientRect().top - pmTop),
|
||||
}));
|
||||
return {
|
||||
blocks: [...pm.children].filter((c) => !c.classList.contains("ne-page-spacer")).length,
|
||||
height: pm.scrollHeight,
|
||||
sheets: sheets.length,
|
||||
spacers: [...document.querySelectorAll(".ne-page-spacer")].map((el) => el.offsetHeight),
|
||||
inGap,
|
||||
placeholder,
|
||||
position: document.querySelector("[data-note-position]")?.textContent,
|
||||
margins: (() => {
|
||||
const sheet = sheets[0].getBoundingClientRect();
|
||||
const col = pm.getBoundingClientRect();
|
||||
return {
|
||||
sheetW: Math.round(sheet.width),
|
||||
colW: Math.round(col.width),
|
||||
left: Math.round(col.left - sheet.left),
|
||||
right: Math.round(sheet.right - col.right),
|
||||
};
|
||||
})(),
|
||||
};
|
||||
}),
|
||||
null,
|
||||
1
|
||||
)
|
||||
);
|
||||
await p.screenshot({ path: `${OUT}/probe.png` });
|
||||
await s.cleanup();
|
||||
await browser.close();
|
||||
Reference in new issue
Block a user