werkmap 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -2
- package/lib/index.d.ts +17 -0
- package/lib/index.js +93 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -88,7 +88,7 @@ werkmap writes the slice of the format a report needs, and refuses the rest.
|
|
|
88
88
|
**It does not fit when:**
|
|
89
89
|
|
|
90
90
|
- You need to read, edit or convert an existing workbook.
|
|
91
|
-
- You need formulas, charts, pivot tables, conditional formatting, data validation, hyperlinks, comments or
|
|
91
|
+
- You need formulas, charts, pivot tables, conditional formatting, data validation, hyperlinks, comments, sheet protection, or a page header and footer.
|
|
92
92
|
- You need column widths or row heights. A reader sizes columns from its own defaults.
|
|
93
93
|
- You have more rows than fit in memory. There is no streaming, row-at-a-time output.
|
|
94
94
|
|
|
@@ -141,6 +141,12 @@ One merged range across `width` columns of `row`, starting at the 1-based column
|
|
|
141
141
|
|
|
142
142
|
Freeze the top `rows` rows. `0` clears.
|
|
143
143
|
|
|
144
|
+
### `sheet.print(setup)`
|
|
145
|
+
|
|
146
|
+
How this worksheet prints: `{ margin, size, orientation, fit }`, every key optional. `margin` is all four page margins in points, `size` one of `letter`, `tabloid`, `legal`, `A3`, `A4`, `A5`, `orientation` either `portrait` or `landscape`, and `fit: true` scales the sheet to one page wide and as many pages tall as it takes.
|
|
147
|
+
|
|
148
|
+
A worksheet that never calls this carries **no print setup at all**, so a reader applies its own defaults rather than this writer's opinion. Calls merge, so two calls naming different keys both take effect. Print setup is per worksheet, which is where OOXML puts it.
|
|
149
|
+
|
|
144
150
|
### `sheet.place(id, { row, col, width, height })`
|
|
145
151
|
|
|
146
152
|
One floating picture anchored to the top-left of a 1-based cell, drawn at `width` × `height` CSS pixels at 96 dpi.
|
|
@@ -173,7 +179,7 @@ There is nothing to configure. Each of these is a property of the writer rather
|
|
|
173
179
|
|
|
174
180
|
- **Text is always interned** into the shared string table.
|
|
175
181
|
- **Style tables are always interned.**
|
|
176
|
-
- **Dates are the 1900 system**, converted in UTC. A reader's timezone
|
|
182
|
+
- **Dates are the 1900 system**, converted in UTC. A reader's timezone never enters the conversion. This writer counts Excel's phantom 1900-02-29, so a date before March 1900 lands on the day it names in Excel. ECMA-376 defines the 1900 system with that fictitious day. LibreOffice Calc omits it and shows those dates one day early. No single serial satisfies both readers, so this writer follows the specification. A date from 1900-03-01 on reads the same in both, which covers every date a report is likely to hold.
|
|
177
183
|
- **A date with no format of its own** gets the short-date built-in, so it reads back as a day rather than as the number underneath it.
|
|
178
184
|
- **Media deduplicates by bytes.**
|
|
179
185
|
- **Every element is written in the Open XML SDK's child order.**
|
package/lib/index.d.ts
CHANGED
|
@@ -75,6 +75,17 @@ export interface Placement {
|
|
|
75
75
|
height: number;
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
/** How a worksheet prints. Every key is optional. */
|
|
79
|
+
export interface PrintSetup {
|
|
80
|
+
/** All four page margins, in points. Defaults to the reader's own. */
|
|
81
|
+
margin?: number;
|
|
82
|
+
/** A paper size OOXML names. There is no arbitrary width and height. */
|
|
83
|
+
size?: "letter" | "tabloid" | "legal" | "A3" | "A4" | "A5";
|
|
84
|
+
orientation?: "portrait" | "landscape";
|
|
85
|
+
/** Scale the sheet to one page wide, and as many pages tall as it takes. */
|
|
86
|
+
fit?: boolean;
|
|
87
|
+
}
|
|
88
|
+
|
|
78
89
|
export interface Sheet {
|
|
79
90
|
/** This sheet's name. */
|
|
80
91
|
readonly name: string;
|
|
@@ -90,6 +101,12 @@ export interface Sheet {
|
|
|
90
101
|
merge(row: number, at: number, width: number): void;
|
|
91
102
|
/** Freeze the top `count` rows. `0` clears. */
|
|
92
103
|
freeze(count: number): void;
|
|
104
|
+
/**
|
|
105
|
+
* How this worksheet prints. A worksheet that never calls this carries no
|
|
106
|
+
* print setup at all, so a reader applies its own defaults. Calls merge, so
|
|
107
|
+
* two calls naming different keys both take effect.
|
|
108
|
+
*/
|
|
109
|
+
print(setup: PrintSetup): void;
|
|
93
110
|
/** Float a picture over the sheet, anchored to one cell. */
|
|
94
111
|
place(id: number, at: Placement): void;
|
|
95
112
|
}
|
package/lib/index.js
CHANGED
|
@@ -26,10 +26,13 @@ for (let n = 0; n < 256; n++) {
|
|
|
26
26
|
CRC[n] = c;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
// Indexed rather than `for...of`: this runs over every part's uncompressed
|
|
30
|
+
// bytes, so it scales with the whole document, and the iterator protocol costs
|
|
31
|
+
// about four times the arithmetic it wraps (32 ms against 9 ms over 3 MB).
|
|
29
32
|
/** @param {Uint8Array} bytes */
|
|
30
33
|
const crc32 = (bytes) => {
|
|
31
34
|
let c = -1;
|
|
32
|
-
for (
|
|
35
|
+
for (let at = 0; at < bytes.length; at++) c = CRC[(c ^ bytes[at]) & 0xff] ^ (c >>> 8);
|
|
33
36
|
return (c ^ -1) >>> 0;
|
|
34
37
|
};
|
|
35
38
|
|
|
@@ -590,6 +593,55 @@ const strings = () => {
|
|
|
590
593
|
};
|
|
591
594
|
};
|
|
592
595
|
|
|
596
|
+
// --------------------------------------------------------- print setup ----
|
|
597
|
+
|
|
598
|
+
// The paper sizes OOXML names by number. A workbook carries a code, not a
|
|
599
|
+
// width and a height, so this is the whole of what a caller may ask for.
|
|
600
|
+
const PAPER = new Map([
|
|
601
|
+
["letter", 1],
|
|
602
|
+
["tabloid", 3],
|
|
603
|
+
["legal", 5],
|
|
604
|
+
["A3", 8],
|
|
605
|
+
["A4", 9],
|
|
606
|
+
["A5", 11],
|
|
607
|
+
]);
|
|
608
|
+
|
|
609
|
+
const ORIENTATION = new Set(["portrait", "landscape"]);
|
|
610
|
+
|
|
611
|
+
// OOXML states margins in inches; this surface takes points, the unit a print
|
|
612
|
+
// margin is written in everywhere else. 72 points to the inch.
|
|
613
|
+
const POINTS_PER_INCH = 72;
|
|
614
|
+
|
|
615
|
+
// The gap Excel leaves for a header and a footer when a caller says nothing.
|
|
616
|
+
// `pageMargins` has no optional attributes, so a value is owed either way.
|
|
617
|
+
const FURNITURE = 0.3;
|
|
618
|
+
|
|
619
|
+
/**
|
|
620
|
+
* `pageMargins` and `pageSetup`, or nothing at all. `pageMargins` has no
|
|
621
|
+
* optional attributes, so asking for any margin means writing all six — the
|
|
622
|
+
* two this surface does not take keep Excel's own gap for a header and a
|
|
623
|
+
* footer.
|
|
624
|
+
*
|
|
625
|
+
* @param {{ margin?: number, size?: string, orientation?: string, fit?: boolean } | null} setup
|
|
626
|
+
*/
|
|
627
|
+
const printXml = (setup) => {
|
|
628
|
+
if (setup === null) return "";
|
|
629
|
+
let out = "";
|
|
630
|
+
if (setup.margin !== undefined) {
|
|
631
|
+
const inches = (setup.margin / POINTS_PER_INCH).toFixed(3);
|
|
632
|
+
out +=
|
|
633
|
+
`<pageMargins left="${inches}" right="${inches}" top="${inches}" bottom="${inches}"` +
|
|
634
|
+
` header="${FURNITURE}" footer="${FURNITURE}"/>`;
|
|
635
|
+
}
|
|
636
|
+
let attributes = "";
|
|
637
|
+
if (setup.size !== undefined) attributes += ` paperSize="${PAPER.get(setup.size)}"`;
|
|
638
|
+
if (setup.orientation !== undefined) attributes += ` orientation="${setup.orientation}"`;
|
|
639
|
+
// `fitToHeight="0"` is what makes it *width* the document fits to: one page
|
|
640
|
+
// across, as many down as it takes.
|
|
641
|
+
if (setup.fit === true) attributes += ' fitToWidth="1" fitToHeight="0"';
|
|
642
|
+
return attributes === "" ? out : out + `<pageSetup${attributes}/>`;
|
|
643
|
+
};
|
|
644
|
+
|
|
593
645
|
// -------------------------------------------------------------- sheet ----
|
|
594
646
|
|
|
595
647
|
const FORBIDDEN_IN_NAME = /[:\\/?*[\]]/;
|
|
@@ -634,6 +686,10 @@ const worksheet = (name, styles, sst, knows) => {
|
|
|
634
686
|
const pictures = [];
|
|
635
687
|
let frozen = 0;
|
|
636
688
|
let widest = 1;
|
|
689
|
+
// What `print` was told, or null. Nothing reaches the file until a caller
|
|
690
|
+
// asks: a reader's own print defaults are better than this writer guessing.
|
|
691
|
+
/** @type {{ margin?: number, size?: string, orientation?: string, fit?: boolean } | null} */
|
|
692
|
+
let printing = null;
|
|
637
693
|
|
|
638
694
|
/**
|
|
639
695
|
* @param {unknown} value
|
|
@@ -743,6 +799,30 @@ const worksheet = (name, styles, sst, knows) => {
|
|
|
743
799
|
if (right > widest) widest = right;
|
|
744
800
|
},
|
|
745
801
|
|
|
802
|
+
/**
|
|
803
|
+
* How this worksheet prints. Every key is optional, and a worksheet that
|
|
804
|
+
* never calls this carries no print setup at all — a reader's own defaults
|
|
805
|
+
* are better than a guess, and writing one would put this writer's opinion
|
|
806
|
+
* in every file.
|
|
807
|
+
*
|
|
808
|
+
* @param {{ margin?: number, size?: string, orientation?: string, fit?: boolean }} setup
|
|
809
|
+
*/
|
|
810
|
+
print(setup) {
|
|
811
|
+
if (setup === null || typeof setup !== "object")
|
|
812
|
+
throw TypeError(`print: expected a setup object, got ${JSON.stringify(setup)}`);
|
|
813
|
+
if (setup.margin !== undefined && requireNumber(setup.margin, "print: margin") < 0)
|
|
814
|
+
throw RangeError(`print: margin cannot be negative, got ${setup.margin}`);
|
|
815
|
+
if (setup.size !== undefined && !PAPER.has(setup.size))
|
|
816
|
+
throw RangeError(
|
|
817
|
+
`print: unknown paper size ${JSON.stringify(setup.size)} -- known: ${[...PAPER.keys()].join(", ")}`,
|
|
818
|
+
);
|
|
819
|
+
if (setup.orientation !== undefined && !ORIENTATION.has(setup.orientation))
|
|
820
|
+
throw RangeError(
|
|
821
|
+
`print: expected portrait or landscape, got ${JSON.stringify(setup.orientation)}`,
|
|
822
|
+
);
|
|
823
|
+
printing = { ...printing, ...setup };
|
|
824
|
+
},
|
|
825
|
+
|
|
746
826
|
/** @param {number} count */
|
|
747
827
|
freeze(count) {
|
|
748
828
|
if (typeof count !== "number" || !Number.isInteger(count) || count < 0 || count >= MAX_ROW)
|
|
@@ -790,13 +870,20 @@ const worksheet = (name, styles, sst, knows) => {
|
|
|
790
870
|
.join("") +
|
|
791
871
|
`</mergeCells>`;
|
|
792
872
|
|
|
873
|
+
// `fitToPage` lives on `sheetPr`, which the schema puts before every
|
|
874
|
+
// other child of a worksheet.
|
|
875
|
+
const properties =
|
|
876
|
+
printing?.fit === true ? `<sheetPr><pageSetUpPr fitToPage="1"/></sheetPr>` : "";
|
|
877
|
+
|
|
793
878
|
return (
|
|
794
879
|
DECLARATION +
|
|
795
880
|
`<worksheet xmlns="${NS}" xmlns:r="${NS_R}">` +
|
|
881
|
+
properties +
|
|
796
882
|
`<dimension ref="${dimension}"/>` +
|
|
797
883
|
`<sheetViews>${pane}</sheetViews>` +
|
|
798
884
|
`<sheetData>${rows.join("")}</sheetData>` +
|
|
799
885
|
merged +
|
|
886
|
+
printXml(printing) +
|
|
800
887
|
(drawing === null ? "" : `<drawing r:id="rId${drawing}"/>`) +
|
|
801
888
|
`</worksheet>`
|
|
802
889
|
);
|
|
@@ -850,6 +937,11 @@ const CREATED = "1970-01-01T00:00:00Z";
|
|
|
850
937
|
|
|
851
938
|
/** @param {Uint8Array} a @param {Uint8Array} b */
|
|
852
939
|
const same = (a, b) => {
|
|
940
|
+
// The same array twice is the common case, and the one the byte loop is
|
|
941
|
+
// worst at: equal bytes never exit early, so it reads the whole image every
|
|
942
|
+
// time. A caller placing one logo on a thousand rows pays 54 ms for that,
|
|
943
|
+
// and nothing for this.
|
|
944
|
+
if (a === b) return true;
|
|
853
945
|
if (a.length !== b.length) return false;
|
|
854
946
|
for (let at = 0; at < a.length; at++) if (a[at] !== b[at]) return false;
|
|
855
947
|
return true;
|