theseus/bundled-addons/docx-editor/lib/doc-css.js

110 lines
5.5 KiB
JavaScript
Raw Normal View History

feat(docx-editor): Save as…, PDF export, and a mark of our own Three gaps, one theme: the editor could produce a file but not decide where it went, what format it was in, or look like anything in the dock. **Save as…** opens a real file dialog, and the extension typed there picks the format. Save then writes to that file instead of dropping another copy in Downloads every time. The renderer never names a path: the dialog returns an opaque token, and the add-on will only write to a path a dialog actually returned. An extension page is the least trusted thing in the add-on, and "write these bytes anywhere" is not a capability it needs. **PDF** goes through Chromium's own print pipeline in a hidden window — the same engine as Ctrl+P — on the paper size read out of the document's own sectPr. For that to match what the user was looking at, the page's typography had to stop living in editor.css, which the export window can't reach: it moves to lib/doc-css.js and both surfaces read the one string. The result embeds subsetted fonts, keeps images, and turns hyperlinks into real PDF link annotations. **The icon** is ours. Microsoft's Word mark is a trademark and borrowing it to look official is not something a browser that talks about sovereignty should do. icon.svg says "text document" in its own words — a turned corner, a heading rule, body lines, a pilcrow badge in Silent Mode green — and `npm run icons` derives the PNGs and addon.json's copy from it, so there is one drawing rather than several that drift. Also: the scratch folder follows the profile rename to extensions-data/ via the api.dataDir the host now provides, instead of creating a stale addons-data/ beside it.
2026-09-21 03:35:55 +02:00
// How a document looks — the typography of the page itself, as opposed to
// the editor's furniture around it (that lives in editor.css).
//
// It is a string rather than a stylesheet because two different documents
// need exactly these rules: the editor's own page, and the standalone HTML
// handed to Chromium when exporting a PDF. Loading a shared .css file into
// the export isn't possible — it is rendered in a throwaway window from a
// temp file, with no add-on folder to resolve relative URLs against — and
// keeping two copies in step by hand is how a PDF starts quietly disagreeing
// with what the user was looking at.
//
// Defaults follow Word's: an 11pt serif body with a little space after each
// paragraph, so an untouched document looks about the same in both places.
(function (root, factory) {
const api = factory();
if (typeof module === "object" && module.exports) module.exports = api;
root.DocxEditor = Object.assign(root.DocxEditor || {}, { docCss: api });
})(typeof globalThis !== "undefined" ? globalThis : this, function () {
"use strict";
const DOC_CSS = `
.sheet { font: 11pt/1.5 Georgia, "Times New Roman", serif; color: #14161a; }
.sheet p { margin: 0 0 8pt; }
.sheet h1, .sheet h2, .sheet h3, .sheet h4, .sheet h5, .sheet h6 {
font-family: "Segoe UI Semibold", "Segoe UI", Calibri, system-ui, sans-serif;
color: #1f4e79; font-weight: 600; margin: 14pt 0 6pt; line-height: 1.25;
}
.sheet h1 { font-size: 20pt; } .sheet h2 { font-size: 16pt; }
.sheet h3 { font-size: 13pt; } .sheet h4 { font-size: 12pt; }
.sheet h5 { font-size: 11pt; } .sheet h6 { font-size: 10.5pt; color: #2e74b5; }
.sheet blockquote { margin: 8pt 0 8pt 24pt; padding-left: 10pt;
border-left: 3px solid rgba(0,0,0,.15); color: #404040; font-style: italic; }
.sheet pre { font: 10pt/1.4 Consolas, "Courier New", monospace;
background: rgba(0,0,0,.04); border: 1px solid rgba(0,0,0,.08);
border-radius: 3px; padding: 8pt 10pt; margin: 8pt 0; white-space: pre-wrap; }
.sheet hr { border: 0; border-top: 1px solid #808080; margin: 10pt 0; }
.sheet ul, .sheet ol { margin: 0 0 8pt; padding-left: 28pt; }
.sheet li { margin: 0 0 2pt; }
.sheet li > p { margin: 0 0 2pt; }
.sheet a { color: #0563c1; text-decoration: underline; }
.sheet img { max-width: 100%; height: auto; vertical-align: baseline; }
.sheet table { border-collapse: collapse; margin: 8pt 0; width: 100%; table-layout: fixed; }
.sheet td, .sheet th { border: 1px solid #999; padding: 4pt 6pt; vertical-align: top;
position: relative; min-width: 1em; }
.sheet th { background: rgba(0,0,0,.04); font-weight: 600; text-align: left; }
.sheet td > p:last-child, .sheet th > p:last-child { margin-bottom: 0; }
.sheet .docx-note-ref { color: #0563c1; font-size: .7em; padding: 0 1px;
border-bottom: 1px dotted #0563c1; }
/* A page break is a labelled dashed rule on screen and a real break on
paper. printToPDF renders with print media, so this covers the PDF
export and Ctrl+P both. */
.sheet .docx-page-break { border-top: 1px dashed #b00; margin: 14pt 0; text-align: center;
user-select: none; position: relative; }
.sheet .docx-page-break span { font: 9pt/1 system-ui, sans-serif; color: #b00;
background: #fff; padding: 0 8px; position: relative;
top: -6pt; letter-spacing: .04em; }
@media print {
.sheet .docx-page-break { border: 0; margin: 0; break-after: page; page-break-after: always; }
.sheet .docx-page-break span { display: none; }
.sheet table, .sheet tr, .sheet img { break-inside: avoid; }
.sheet h1, .sheet h2, .sheet h3, .sheet h4, .sheet h5, .sheet h6 { break-after: avoid; }
}
`;
// Twips are Word's unit (1440 to the inch); CSS wants inches. Rounded to
// thousandths, which is finer than any printer cares about and keeps the
// @page rule readable.
const inches = (twips, fallback) => {
const n = Number(twips);
return (Number.isFinite(n) && n > 0 ? n / 1440 : fallback).toFixed(3);
};
// An @page rule from the document's own section setup, so the PDF comes
// out on the paper the author chose instead of Chromium's default Letter.
function pageRule(setup) {
const page = (setup && setup.page) || {};
const size = page.size || {};
const m = page.margin || {};
const w = inches(size.width, 8.27); // A4 unless the file says otherwise
const h = inches(size.height, 11.69);
const top = inches(m.top, 1);
const right = inches(m.right, 1);
const bottom = inches(m.bottom, 1);
const left = inches(m.left, 1);
return `@page { size: ${w}in ${h}in; margin: ${top}in ${right}in ${bottom}in ${left}in; }`;
}
/**
* A standalone HTML page of the document, ready to be handed to Chromium's
* print pipeline. Everything is inline: no stylesheet to resolve, no
* scripts, and images are already data URLs by the time they reach here.
*/
function printableHtml({ title, bodyHtml, setup }) {
const esc = (s) => String(s || "").replace(/[&<>]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;" }[c]));
return `<!doctype html>
<html><head><meta charset="utf-8"><title>${esc(title || "Document")}</title>
<style>
${pageRule(setup)}
html, body { margin: 0; padding: 0; background: #fff; }
${DOC_CSS}
/* The page box is the paper; the sheet must not add a second margin. */
.sheet { max-width: none; margin: 0; padding: 0; background: #fff; box-shadow: none; }
</style></head>
<body><div class="sheet">${bodyHtml}</div></body></html>`;
}
return { DOC_CSS, printableHtml, pageRule };
});