reamkit 1.0.1 → 1.1.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 CHANGED
@@ -33,6 +33,7 @@ const bytes = new Uint8Array(await file.arrayBuffer());
33
33
  const doc = Ream.parse(bytes); // docx or xlsx — sniffed
34
34
  const pdf = await doc.convert('pdf'); // async — fetches a font if needed
35
35
  const svg = await doc.convert('svg'); // same parse, different target
36
+ const html = await doc.convert('html'); // flowed HTML — needs no fonts at all
36
37
 
37
38
  // Hand the bytes to the browser: preview, download, upload, …
38
39
  const url = URL.createObjectURL(new Blob([pdf], { type: 'application/pdf' }));
@@ -147,7 +148,7 @@ automatically from the document's `docProps/core.xml`), `attachments`
147
148
 
148
149
  ### Lower-level APIs
149
150
 
150
- - `docxReader` / `xlsxReader`, `svgWriter` — the `@experimental` reader/writer
151
+ - `docxReader` / `xlsxReader`, `svgWriter`, `htmlWriter` — the `@experimental` reader/writer
151
152
  interfaces of the interlayer, for building custom pipelines (and keeping
152
153
  unused formats out of your bundle); `layoutStyledDocument` produces the
153
154
  frozen page model (`PageItem` pages in a top-left `Pt` frame) they consume.
@@ -163,7 +164,7 @@ repeated titles, page breaks), DrawingML shapes and charts, OMML math,
163
164
  Type0+CIDFontType2 embedding with subsetting, Knuth-Plass line breaking,
164
165
  Liang hyphenation, OpenType ligatures/kerning + Arabic cursive joining,
165
166
  BiDi (UAX #9), tagged PDF, PDF/A-1/2/3 (a/b/u), digital signatures
166
- (PKCS#7/ECDSA/PAdES/RFC 3161), SVG page preview.
167
+ (PKCS#7/ECDSA/PAdES/RFC 3161), SVG page preview, flowed HTML export.
167
168
 
168
169
  See `handoff.md` for the full feature matrix and known limitations.
169
170
 
@@ -1 +1,2 @@
1
+ export declare function toBase64(bytes: Uint8Array): string;
1
2
  export declare function bytesInclude(haystack: Uint8Array, needle: string): boolean;
@@ -1,4 +1,9 @@
1
1
  //#region src/core/bytes.ts
2
+ function toBase64(bytes) {
3
+ let bin = "";
4
+ for (const b of bytes) bin += String.fromCharCode(b);
5
+ return btoa(bin);
6
+ }
2
7
  function bytesInclude(haystack, needle) {
3
8
  const n = new TextEncoder().encode(needle);
4
9
  outer: for (let i = 0; i + n.length <= haystack.length; i++) {
@@ -8,4 +13,4 @@ function bytesInclude(haystack, needle) {
8
13
  return false;
9
14
  }
10
15
  //#endregion
11
- export { bytesInclude };
16
+ export { bytesInclude, toBase64 };
@@ -5,8 +5,8 @@ import { FontProvider } from '../fonts/provider.js';
5
5
  import { Loss, LossReport } from '../ir/index.js';
6
6
  import { FontBytesByVariant } from '../font/index.js';
7
7
  export interface ConvertOptions extends ConvertDocxOptions {
8
- /** Target format: 'pdf' (default) or 'svg' (page-stack preview). */
9
- readonly to?: 'pdf' | 'svg';
8
+ /** Target: 'pdf' (default), 'svg' (page-stack preview) or 'html' (flowed). */
9
+ readonly to?: 'pdf' | 'svg' | 'html';
10
10
  /**
11
11
  * Strict mode (handoff v1 §5): throw ConversionLossError on the first
12
12
  * recorded loss instead of returning it in the report.
@@ -6,6 +6,7 @@ import { docxReader } from "../../word/docx-reader.js";
6
6
  import { xlsxReader } from "../../excel/xlsx-reader.js";
7
7
  import { chainProviders } from "../fonts/provider.js";
8
8
  import { flowRenderOptions } from "./project.js";
9
+ import { writeHtml } from "../../html/html-writer.js";
9
10
  import { writeSvg } from "../../svg/svg-writer.js";
10
11
  import { convertDocxToPdf } from "../../word/docx-to-pdf.js";
11
12
  import { convertXlsxToPdf } from "../../excel/xlsx-to-pdf.js";
@@ -19,6 +20,17 @@ function createConverter(opts = {}) {
19
20
  const reader = detect(bytes);
20
21
  if (!reader) throw new Error("Unrecognized input format (no registered reader sniffs these bytes)");
21
22
  const losses = [];
23
+ if (to === "html") {
24
+ const { doc: flow, losses: readLosses } = reader.read(bytes);
25
+ losses.push(...readLosses);
26
+ const html = writeHtml(flow);
27
+ losses.push(...html.losses);
28
+ if (strict && losses.length > 0) throw new ConversionLossError(losses[0]);
29
+ return {
30
+ bytes: html.bytes,
31
+ losses
32
+ };
33
+ }
22
34
  let conv = rest;
23
35
  if (fontProviders && fontProviders.length > 0 && !rest.fonts && !rest.fontBytes) {
24
36
  const { fonts, loss } = await resolveFontsViaChain(fontProviders);
@@ -6,7 +6,7 @@ import { Loss } from '../ir/index.js';
6
6
  import { DocumentReader } from '../ir/adapters.js';
7
7
  import { FlowDoc } from '../ir/flow.js';
8
8
  import { SignatureOptions, StyledRenderOptions } from '../../pdf/index.js';
9
- export type ReamTarget = 'pdf' | 'svg';
9
+ export type ReamTarget = 'pdf' | 'svg' | 'html';
10
10
  export interface ReamParseOptions {
11
11
  readonly readers?: ReadonlyArray<DocumentReader>;
12
12
  }
@@ -6,6 +6,7 @@ import { signPdf } from "../../pdf/signature.js";
6
6
  import { renderStyledPdf } from "../../pdf/styled-page-renderer.js";
7
7
  import "../../pdf/index.js";
8
8
  import { flowRenderOptions } from "./project.js";
9
+ import { writeHtml } from "../../html/html-writer.js";
9
10
  import { writeSvg } from "../../svg/svg-writer.js";
10
11
  import { resolveDocxAutoFonts } from "../../word/docx-to-pdf.js";
11
12
  import { DEFAULT_READERS, resolveFontsViaChain } from "./facade.js";
@@ -36,6 +37,15 @@ var Ream = class Ream {
36
37
  }
37
38
  async convertWithReport(to, options = {}) {
38
39
  const losses = [...this.losses];
40
+ if (to === "html") {
41
+ const html = writeHtml(this.flow);
42
+ losses.push(...html.losses);
43
+ this.enforceStrict(options, losses);
44
+ return {
45
+ bytes: html.bytes,
46
+ losses
47
+ };
48
+ }
39
49
  const { fonts, registriesByFamily } = await this.resolveFonts(options, losses);
40
50
  const registry = FontRegistry.fromBytes(fonts);
41
51
  if (to === "svg") {
@@ -3,5 +3,5 @@ import { ResolvedParagraphProperties, ResolvedRunProperties } from './types.js';
3
3
  export declare const EMPTY_STYLE_SHEET: StyleSheet;
4
4
  export declare function resolveRunProperties(runDirect: RunProperties, paragraphDirect: ParagraphProperties, sheet: StyleSheet): ResolvedRunProperties;
5
5
  export declare function resolveParagraphProperties(paragraphDirect: ParagraphProperties, sheet: StyleSheet): ResolvedParagraphProperties;
6
- export declare function resolveBodyStyles(body: ReadonlyArray<BodyElement>, sheet: StyleSheet): Array<BodyElement>;
6
+ export declare function resolveBodyStyles(body: ReadonlyArray<BodyElement>, sheet: StyleSheet): ReadonlyArray<BodyElement>;
7
7
  export declare function resolveHeadersFootersStyles(hf: ReadonlyMap<string, ReadonlyArray<BodyElement>>, sheet: StyleSheet): ReadonlyMap<string, ReadonlyArray<BodyElement>>;
@@ -35,7 +35,20 @@ function computeRunProperties(runDirect, paragraphDirect, sheet) {
35
35
  }
36
36
  return mergeRun(acc, runDirect);
37
37
  }
38
+ var paragraphCascadeCache = /* @__PURE__ */ new WeakMap();
38
39
  function resolveParagraphProperties(paragraphDirect, sheet) {
40
+ let bySheet = paragraphCascadeCache.get(sheet);
41
+ if (!bySheet) {
42
+ bySheet = /* @__PURE__ */ new WeakMap();
43
+ paragraphCascadeCache.set(sheet, bySheet);
44
+ }
45
+ const hit = bySheet.get(paragraphDirect);
46
+ if (hit) return hit;
47
+ const resolved = computeParagraphProperties(paragraphDirect, sheet);
48
+ bySheet.set(paragraphDirect, resolved);
49
+ return resolved;
50
+ }
51
+ function computeParagraphProperties(paragraphDirect, sheet) {
39
52
  let acc = mergePar(DEFAULT_RESOLVED_PARAGRAPH, sheet.defaultParagraphProperties);
40
53
  if (paragraphDirect.styleId) {
41
54
  const chain = resolveStyleChain(paragraphDirect.styleId, sheet);
@@ -117,52 +130,45 @@ function copyDefined(base, override) {
117
130
  }
118
131
  return out;
119
132
  }
133
+ function primeResolvedFixpoint(run, para) {
134
+ let bySheet = runCascadeCache.get(EMPTY_STYLE_SHEET);
135
+ if (!bySheet) {
136
+ bySheet = /* @__PURE__ */ new WeakMap();
137
+ runCascadeCache.set(EMPTY_STYLE_SHEET, bySheet);
138
+ }
139
+ let byRun = bySheet.get(run);
140
+ if (!byRun) {
141
+ byRun = /* @__PURE__ */ new WeakMap();
142
+ bySheet.set(run, byRun);
143
+ }
144
+ if (!byRun.has(para)) byRun.set(para, run);
145
+ }
146
+ function primeParagraphFixpoint(para) {
147
+ let bySheet = paragraphCascadeCache.get(EMPTY_STYLE_SHEET);
148
+ if (!bySheet) {
149
+ bySheet = /* @__PURE__ */ new WeakMap();
150
+ paragraphCascadeCache.set(EMPTY_STYLE_SHEET, bySheet);
151
+ }
152
+ if (!bySheet.has(para)) bySheet.set(para, para);
153
+ }
120
154
  function resolveBodyStyles(body, sheet) {
121
- const visitParagraph = (p) => ({
122
- ...p,
123
- properties: resolveParagraphProperties(p.properties, sheet),
124
- runs: p.runs.map((r) => ({
125
- ...r,
126
- properties: resolveRunProperties(r.properties, p.properties, sheet)
127
- }))
128
- });
155
+ const visitParagraph = (p) => {
156
+ for (const r of p.runs) r.properties = resolveRunProperties(r.properties, p.properties, sheet);
157
+ p.properties = resolveParagraphProperties(p.properties, sheet);
158
+ primeParagraphFixpoint(p.properties);
159
+ for (const r of p.runs) primeResolvedFixpoint(r.properties, p.properties);
160
+ };
129
161
  const visit = (el) => {
130
- if (el.kind === "paragraph") return {
131
- kind: "paragraph",
132
- paragraph: visitParagraph(el.paragraph)
133
- };
134
- if (el.kind === "table") return {
135
- kind: "table",
136
- table: {
137
- ...el.table,
138
- rows: el.table.rows.map((row) => ({
139
- ...row,
140
- cells: row.cells.map((cell) => ({
141
- ...cell,
142
- content: cell.content.map(visit)
143
- }))
144
- }))
145
- }
146
- };
147
- if (el.kind === "shape" && el.shape.text) return {
148
- kind: "shape",
149
- shape: {
150
- ...el.shape,
151
- text: {
152
- ...el.shape.text,
153
- content: el.shape.text.content.map(visit)
154
- }
155
- }
156
- };
157
- return el;
162
+ if (el.kind === "paragraph") visitParagraph(el.paragraph);
163
+ else if (el.kind === "table") for (const row of el.table.rows) for (const cell of row.cells) for (const child of cell.content) visit(child);
164
+ else if (el.kind === "shape" && el.shape.text) for (const child of el.shape.text.content) visit(child);
158
165
  };
159
- return body.map(visit);
166
+ for (const el of body) visit(el);
167
+ return body;
160
168
  }
161
169
  function resolveHeadersFootersStyles(hf, sheet) {
162
- if (hf.size === 0) return hf;
163
- const out = /* @__PURE__ */ new Map();
164
- for (const [key, value] of hf) out.set(key, resolveBodyStyles(value, sheet));
165
- return out;
170
+ for (const value of hf.values()) resolveBodyStyles(value, sheet);
171
+ return hf;
166
172
  }
167
173
  //#endregion
168
174
  export { EMPTY_STYLE_SHEET, resolveBodyStyles, resolveHeadersFootersStyles, resolveParagraphProperties, resolveRunProperties };
@@ -169,6 +169,25 @@ function worksheetToBody(worksheet, sharedStrings, styles, date1904, print) {
169
169
  const scaled = printScale < .999;
170
170
  const breakRows = new Set(worksheet.rowBreaks ?? []);
171
171
  let textBudget = MAX_SHEET_TEXT_CHARS;
172
+ const runPropsByXf = /* @__PURE__ */ new Map();
173
+ const cellRunProps = (xf) => {
174
+ let props = runPropsByXf.get(xf);
175
+ if (props === void 0) {
176
+ const base = xf ? runPropsFromXf(xf, styles) : {};
177
+ props = scaled ? scaleRunFont(base, printScale) : base;
178
+ runPropsByXf.set(xf, props);
179
+ }
180
+ return props;
181
+ };
182
+ const paraPropsByAlignment = /* @__PURE__ */ new Map();
183
+ const cellParaProps = (alignment) => {
184
+ let props = paraPropsByAlignment.get(alignment);
185
+ if (props === void 0) {
186
+ props = alignment ? { alignment } : {};
187
+ paraPropsByAlignment.set(alignment, props);
188
+ }
189
+ return props;
190
+ };
172
191
  const rows = [];
173
192
  for (let r = 0; r < rowCount; r++) {
174
193
  const absR = r + rowStart;
@@ -186,8 +205,7 @@ function worksheetToBody(worksheet, sharedStrings, styles, date1904, print) {
186
205
  if (text.length > textBudget) text = text.slice(0, Math.max(0, textBudget));
187
206
  textBudget -= text.length;
188
207
  const xf = ws && ws.styleIndex !== void 0 ? styles.cellXfs[ws.styleIndex] : void 0;
189
- const baseRunProps = xf ? runPropsFromXf(xf, styles) : {};
190
- const runProps = scaled ? scaleRunFont(baseRunProps, printScale) : baseRunProps;
208
+ const runProps = cellRunProps(xf);
191
209
  const alignment = xf ? alignmentFromXf(xf) : void 0;
192
210
  const shading = xf ? shadingFromXf(xf, styles) : void 0;
193
211
  const borders = xf ? bordersFromXf(xf, styles) : void 0;
@@ -210,7 +228,7 @@ function worksheetToBody(worksheet, sharedStrings, styles, date1904, print) {
210
228
  ...shading ? { shading } : {},
211
229
  ...borders ? { borders } : {}
212
230
  };
213
- const paragraphProps = alignment ? { alignment } : {};
231
+ const paragraphProps = cellParaProps(alignment);
214
232
  cells.push({
215
233
  properties,
216
234
  content: [{
@@ -0,0 +1,4 @@
1
+ import { DocumentWriter, WriteResult } from '../core/ir/adapters.js';
2
+ import { FlowDoc } from '../core/ir/flow.js';
3
+ export declare function writeHtml(flow: FlowDoc): WriteResult;
4
+ export declare const htmlWriter: DocumentWriter<FlowDoc>;
@@ -0,0 +1,292 @@
1
+ import { FEATURES } from "../core/ir/features.js";
2
+ import { detectImageFormat } from "../core/images.js";
3
+ import { EMPTY_STYLE_SHEET, resolveParagraphProperties, resolveRunProperties } from "../core/style-cascade/resolver.js";
4
+ import "../core/style-cascade/index.js";
5
+ import { toBase64 } from "../core/bytes.js";
6
+ //#region src/html/html-writer.ts
7
+ function writeHtml(flow) {
8
+ const losses = [];
9
+ const out = [];
10
+ const ctx = {
11
+ resources: flow.resources,
12
+ losses
13
+ };
14
+ const lang = flow.language ?? "en-US";
15
+ const title = flow.info?.title ?? "Document";
16
+ out.push("<!DOCTYPE html>");
17
+ out.push(`<html lang="${escapeAttr(lang)}">`);
18
+ out.push("<head>");
19
+ out.push("<meta charset=\"utf-8\"/>");
20
+ out.push("<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"/>");
21
+ out.push(`<title>${escapeText(title)}</title>`);
22
+ out.push(`<style>${BASE_CSS}</style>`);
23
+ out.push("</head>");
24
+ out.push("<body>");
25
+ out.push(`<article style="max-width: ${fmt(contentWidthPt(flow))}pt">`);
26
+ if (flow.headersFooters && flow.headersFooters.size > 0) losses.push({
27
+ severity: "dropped",
28
+ feature: FEATURES.headersFooters,
29
+ detail: "headers/footers have no fixed pages to band onto in flowed HTML"
30
+ });
31
+ for (const el of flow.body) emitBlock(out, el, ctx);
32
+ out.push("</article>");
33
+ out.push("</body>");
34
+ out.push("</html>");
35
+ return {
36
+ bytes: new TextEncoder().encode(out.join("\n")),
37
+ losses
38
+ };
39
+ }
40
+ var htmlWriter = {
41
+ id: "html",
42
+ consumes: "flow",
43
+ supports: new Set([
44
+ FEATURES.text,
45
+ FEATURES.tables,
46
+ FEATURES.tablesNested,
47
+ FEATURES.lists,
48
+ FEATURES.images,
49
+ FEATURES.rtl,
50
+ FEATURES.trackedChanges
51
+ ]),
52
+ write: (doc) => writeHtml(doc)
53
+ };
54
+ var BASE_CSS = [
55
+ "body{margin:24pt;color:#000;background:#fff}",
56
+ "article{margin:0 auto}",
57
+ "p,h1,h2,h3,h4,h5,h6,figure{margin:0}",
58
+ "table{border-collapse:collapse}",
59
+ "td,th{vertical-align:top}",
60
+ "th{text-align:inherit}",
61
+ ".tab{display:inline-block;min-width:18pt}",
62
+ "img{vertical-align:baseline}"
63
+ ].join("");
64
+ function emitBlock(out, el, ctx) {
65
+ if (el.kind === "paragraph") emitParagraph(out, el.paragraph, ctx);
66
+ else if (el.kind === "table") emitTable(out, el.table, ctx);
67
+ else if (el.kind === "image") emitImageBlock(out, el.image, ctx);
68
+ else if (el.kind === "chart") ctx.losses.push({
69
+ severity: "dropped",
70
+ feature: FEATURES.charts,
71
+ detail: "chart not rendered by the HTML writer (v0)"
72
+ });
73
+ else {
74
+ ctx.losses.push({
75
+ severity: "dropped",
76
+ feature: FEATURES.shapes,
77
+ detail: "shape geometry not rendered by the HTML writer (v0)"
78
+ });
79
+ if (el.shape.text) {
80
+ out.push("<div>");
81
+ for (const child of el.shape.text.content) emitBlock(out, child, ctx);
82
+ out.push("</div>");
83
+ }
84
+ }
85
+ }
86
+ function emitParagraph(out, p, ctx) {
87
+ const resolved = resolveParagraphProperties(p.properties, EMPTY_STYLE_SHEET);
88
+ const lvl = resolved.outlineLevel;
89
+ const tag = lvl !== void 0 && lvl >= 0 && lvl <= 8 ? `h${Math.min(lvl, 5) + 1}` : "p";
90
+ const style = paragraphCss(resolved);
91
+ const open = `<${tag}${resolved.bidi ? " dir=\"rtl\"" : ""}${style ? ` style="${style}"` : ""}>`;
92
+ const parts = [];
93
+ for (const run of p.runs) parts.push(runHtml(run, p, ctx));
94
+ out.push(`${open}${parts.join("") || "&nbsp;"}</${tag}>`);
95
+ }
96
+ function paragraphCss(r) {
97
+ const css = [];
98
+ if (r.alignment === "center" || r.alignment === "right") css.push(`text-align:${r.alignment}`);
99
+ else if (r.alignment === "both" || r.alignment === "distribute") css.push("text-align:justify");
100
+ if (r.spacingBefore > 0) css.push(`margin-top:${fmt(r.spacingBefore)}pt`);
101
+ if (r.spacingAfter > 0) css.push(`margin-bottom:${fmt(r.spacingAfter)}pt`);
102
+ if (r.indentLeft !== 0) css.push(`margin-left:${fmt(r.indentLeft)}pt`);
103
+ if (r.indentRight !== 0) css.push(`margin-right:${fmt(r.indentRight)}pt`);
104
+ if (r.indentFirstLine !== 0) css.push(`text-indent:${fmt(r.indentFirstLine)}pt`);
105
+ if (r.spacingLine > 0) if (r.spacingLineRule === "auto") css.push(`line-height:${fmt(r.spacingLine / 12)}`);
106
+ else css.push(`line-height:${fmt(r.spacingLine)}pt`);
107
+ if (r.pageBreakBefore) css.push("break-before:page");
108
+ return css.join(";");
109
+ }
110
+ function runHtml(run, p, ctx) {
111
+ if (run.math !== void 0) {
112
+ ctx.losses.push({
113
+ severity: "dropped",
114
+ feature: FEATURES.math,
115
+ detail: "inline math not rendered by the HTML writer (v0)"
116
+ });
117
+ return "";
118
+ }
119
+ if (run.inlineImage) {
120
+ const img = run.inlineImage;
121
+ const src = dataUri(img.resource, ctx.resources);
122
+ if (!src) return "";
123
+ return `<img src="${src}" style="width:${fmt(img.width)}pt;height:${fmt(img.height)}pt" alt=""/>`;
124
+ }
125
+ if (run.text.length === 0) return "";
126
+ const resolved = resolveRunProperties(run.properties, p.properties, EMPTY_STYLE_SHEET);
127
+ const style = runCss(resolved);
128
+ let html = `<span${resolved.rtl ? " dir=\"rtl\"" : ""}${style ? ` style="${style}"` : ""}>${textHtml(run.text)}</span>`;
129
+ if (resolved.verticalAlign === "superscript") html = `<sup>${html}</sup>`;
130
+ else if (resolved.verticalAlign === "subscript") html = `<sub>${html}</sub>`;
131
+ return html;
132
+ }
133
+ function runCss(r) {
134
+ const css = [];
135
+ const family = r.fontFamily.ascii;
136
+ if (family) css.push(`font-family:${JSON.stringify(family)}`);
137
+ css.push(`font-size:${fmt(r.fontSizePt)}pt`);
138
+ if (r.bold) css.push("font-weight:700");
139
+ if (r.italic) css.push("font-style:italic");
140
+ const deco = [];
141
+ const underlined = r.underline !== "none";
142
+ if (underlined) deco.push("underline");
143
+ if (r.strike) deco.push("line-through");
144
+ if (deco.length > 0) {
145
+ css.push(`text-decoration-line:${deco.join(" ")}`);
146
+ const style = underlined ? decorationStyle(r.underline) : void 0;
147
+ if (style) css.push(`text-decoration-style:${style}`);
148
+ }
149
+ if (r.colorHex !== "000000") css.push(`color:#${r.colorHex}`);
150
+ return css.join(";");
151
+ }
152
+ function decorationStyle(u) {
153
+ switch (u) {
154
+ case "double": return "double";
155
+ case "dotted":
156
+ case "dottedHeavy": return "dotted";
157
+ case "dash":
158
+ case "dashHeavy": return "dashed";
159
+ case "wave": return "wavy";
160
+ default: return;
161
+ }
162
+ }
163
+ function textHtml(text) {
164
+ return escapeText(text).replaceAll(" ", "<span class=\"tab\"></span>").replaceAll("\n", "<br/>");
165
+ }
166
+ function emitTable(out, table, ctx) {
167
+ const css = [];
168
+ if (table.properties.widthPt !== void 0) css.push(`width:${fmt(table.properties.widthPt)}pt`);
169
+ else if (table.properties.widthFraction !== void 0) css.push(`width:${fmt(table.properties.widthFraction * 100)}%`);
170
+ if (table.properties.alignment === "center") css.push("margin-left:auto;margin-right:auto");
171
+ else if (table.properties.alignment === "right") css.push("margin-left:auto");
172
+ out.push(`<table${css.length > 0 ? ` style="${css.join(";")}"` : ""}>`);
173
+ if (table.grid.length > 0) {
174
+ out.push("<colgroup>");
175
+ for (const w of table.grid) out.push(`<col style="width:${fmt(w)}pt"/>`);
176
+ out.push("</colgroup>");
177
+ }
178
+ const colStarts = table.rows.map((row) => {
179
+ let col = 0;
180
+ return row.cells.map((cell) => {
181
+ const start = col;
182
+ col += cell.properties.colSpan ?? 1;
183
+ return start;
184
+ });
185
+ });
186
+ for (let ri = 0; ri < table.rows.length; ri++) {
187
+ const row = table.rows[ri];
188
+ out.push("<tr>");
189
+ for (let ci = 0; ci < row.cells.length; ci++) {
190
+ const cell = row.cells[ci];
191
+ const merge = cell.properties.merge;
192
+ if (merge === "middle" || merge === "end") continue;
193
+ const rowSpan = merge === "start" ? mergeRowSpan(table, colStarts, ri, colStarts[ri][ci]) : 1;
194
+ emitCell(out, cell, table, ctx, {
195
+ isHeader: row.properties.isHeader === true,
196
+ firstRow: ri === 0,
197
+ lastRow: ri === table.rows.length - 1,
198
+ firstCol: colStarts[ri][ci] === 0,
199
+ lastCol: colStarts[ri][ci] + (cell.properties.colSpan ?? 1) >= table.grid.length,
200
+ rowSpan
201
+ });
202
+ }
203
+ out.push("</tr>");
204
+ }
205
+ out.push("</table>");
206
+ }
207
+ function mergeRowSpan(table, colStarts, startRow, colStart) {
208
+ let span = 1;
209
+ for (let r = startRow + 1; r < table.rows.length; r++) {
210
+ const row = table.rows[r];
211
+ const idx = colStarts[r].findIndex((c) => c === colStart);
212
+ const merge = (idx >= 0 ? row.cells[idx] : void 0)?.properties.merge;
213
+ if (merge !== "middle" && merge !== "end") break;
214
+ span++;
215
+ if (merge === "end") break;
216
+ }
217
+ return span;
218
+ }
219
+ function emitCell(out, cell, table, ctx, pos) {
220
+ const tag = pos.isHeader ? "th" : "td";
221
+ const attrs = [];
222
+ const colSpan = cell.properties.colSpan ?? 1;
223
+ if (colSpan > 1) attrs.push(`colspan="${colSpan}"`);
224
+ if (pos.rowSpan > 1) attrs.push(`rowspan="${pos.rowSpan}"`);
225
+ const css = [];
226
+ const t = table.properties.borders;
227
+ const c = cell.properties.borders;
228
+ pushBorder(css, "top", c?.top ?? (pos.firstRow ? t?.top : t?.insideH));
229
+ pushBorder(css, "bottom", c?.bottom ?? (pos.lastRow ? t?.bottom : t?.insideH));
230
+ pushBorder(css, "left", c?.left ?? (pos.firstCol ? t?.left : t?.insideV));
231
+ pushBorder(css, "right", c?.right ?? (pos.lastCol ? t?.right : t?.insideV));
232
+ if (cell.properties.shading) css.push(`background-color:#${cell.properties.shading.colorHex}`);
233
+ const margins = cell.properties.margins ?? table.properties.defaultCellMargins;
234
+ const padTop = margins?.top ?? 0;
235
+ const padRight = margins?.right ?? 5.4;
236
+ const padBottom = margins?.bottom ?? 0;
237
+ const padLeft = margins?.left ?? 5.4;
238
+ css.push(`padding:${fmt(padTop)}pt ${fmt(padRight)}pt ${fmt(padBottom)}pt ${fmt(padLeft)}pt`);
239
+ out.push(`<${tag}${attrs.length > 0 ? ` ${attrs.join(" ")}` : ""} style="${css.join(";")}">`);
240
+ for (const child of cell.content) emitBlock(out, child, ctx);
241
+ out.push(`</${tag}>`);
242
+ }
243
+ function pushBorder(css, side, border) {
244
+ if (!border || border.style === "none") return;
245
+ const width = border.width ?? .5;
246
+ const style = border.style === "double" ? "double" : border.style === "dotted" ? "dotted" : border.style === "dashed" ? "dashed" : "solid";
247
+ css.push(`border-${side}:${fmt(width)}pt ${style} #${border.colorHex ?? "000000"}`);
248
+ }
249
+ function emitImageBlock(out, image, ctx) {
250
+ const src = dataUri(image.resource, ctx.resources);
251
+ if (!src) return;
252
+ const pp = image.paragraphProperties;
253
+ const align = pp.alignment === "center" || pp.alignment === "right" ? pp.alignment : void 0;
254
+ const css = [];
255
+ if (align) css.push(`text-align:${align}`);
256
+ if (pp.spacingBefore !== void 0 && pp.spacingBefore > 0) css.push(`margin-top:${fmt(pp.spacingBefore)}pt`);
257
+ if (pp.spacingAfter !== void 0 && pp.spacingAfter > 0) css.push(`margin-bottom:${fmt(pp.spacingAfter)}pt`);
258
+ const alt = image.altText ?? "";
259
+ out.push(`<figure${css.length > 0 ? ` style="${css.join(";")}"` : ""}><img src="${src}" style="width:${fmt(image.width)}pt;height:${fmt(image.height)}pt" alt="${escapeAttr(alt)}"/></figure>`);
260
+ }
261
+ var MIME_BY_FORMAT = {
262
+ jpeg: "image/jpeg",
263
+ png: "image/png",
264
+ jpeg2000: "image/jp2"
265
+ };
266
+ function dataUri(resource, store) {
267
+ if (resource === void 0) return void 0;
268
+ const bytes = store.get(resource);
269
+ if (!bytes) return void 0;
270
+ const format = detectImageFormat(bytes);
271
+ if (!format) return void 0;
272
+ return `data:${MIME_BY_FORMAT[format]};base64,${toBase64(bytes)}`;
273
+ }
274
+ function contentWidthPt(flow) {
275
+ const props = flow.sections[0]?.properties ?? flow.section;
276
+ const pageWidth = props?.pageSize?.width ?? 595;
277
+ const left = props?.margins?.left ?? 72;
278
+ const right = props?.margins?.right ?? 72;
279
+ return Math.max(72, pageWidth - left - right);
280
+ }
281
+ function escapeText(s) {
282
+ return s.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
283
+ }
284
+ function escapeAttr(s) {
285
+ return escapeText(s).replaceAll("\"", "&quot;");
286
+ }
287
+ function fmt(n) {
288
+ const r = Math.round(n * 100) / 100;
289
+ return Object.is(r, -0) ? "0" : String(r);
290
+ }
291
+ //#endregion
292
+ export { htmlWriter, writeHtml };
@@ -23,5 +23,6 @@ export { chainProviders, callerFontProvider, embeddedDocFontProvider, remoteFont
23
23
  export type { FontProvider, FontRequest, FontAnswer } from './core/fonts/provider.js';
24
24
  export { svgWriter, writeSvg } from './svg/svg-writer.js';
25
25
  export type { SvgWriteOptions } from './svg/svg-writer.js';
26
+ export { htmlWriter, writeHtml } from './html/html-writer.js';
26
27
  export { layoutStyledDocument } from './layout/styled-layout.js';
27
28
  export type { LaidOutDocument } from './layout/page-doc.js';
package/dist/esm/index.js CHANGED
@@ -15,7 +15,8 @@ import { createLanguageHyphenator, getHyphenator } from "./core/hyphenation/inde
15
15
  import { docxReader, readDocx } from "./word/docx-reader.js";
16
16
  import { readXlsx, xlsxReader } from "./excel/xlsx-reader.js";
17
17
  import { NO_FONT, callerFontProvider, chainProviders, embeddedDocFontProvider, isEmbeddingRestricted, localFontProvider, readOs2FsType, remoteFontProvider } from "./core/fonts/provider.js";
18
+ import { htmlWriter, writeHtml } from "./html/html-writer.js";
18
19
  import { svgWriter, writeSvg } from "./svg/svg-writer.js";
19
20
  import { createConverter } from "./core/converter/facade.js";
20
21
  import { Ream } from "./core/converter/ream.js";
21
- export { ConversionLossError, FEATURES, FontRegistry, NO_FONT, Ream, ResourceStore, callerFontProvider, chainProviders, createConverter, createHyphenator, createLanguageHyphenator, docxReader, eighthPtToPt, embeddedDocFontProvider, emuToPt, fetchFontSet, formatLoss, getHyphenator, halfPtToPt, inchToPt, isEmbeddingRestricted, layoutStyledDocument, localFontProvider, mmToPt, parseTtf, pt, pxToPt, readDocx, readOs2FsType, readXlsx, remoteFontProvider, renderStyledPdf, resolveFamilyKey, signPdf, splitPatternBundle, subsetTtf, svgWriter, twipsToPt, writeSvg, xlsxReader };
22
+ export { ConversionLossError, FEATURES, FontRegistry, NO_FONT, Ream, ResourceStore, callerFontProvider, chainProviders, createConverter, createHyphenator, createLanguageHyphenator, docxReader, eighthPtToPt, embeddedDocFontProvider, emuToPt, fetchFontSet, formatLoss, getHyphenator, halfPtToPt, htmlWriter, inchToPt, isEmbeddingRestricted, layoutStyledDocument, localFontProvider, mmToPt, parseTtf, pt, pxToPt, readDocx, readOs2FsType, readXlsx, remoteFontProvider, renderStyledPdf, resolveFamilyKey, signPdf, splitPatternBundle, subsetTtf, svgWriter, twipsToPt, writeHtml, writeSvg, xlsxReader };
@@ -1,5 +1,6 @@
1
1
  import { FEATURES } from "../core/ir/features.js";
2
2
  import { paintPlan } from "../layout/page-doc.js";
3
+ import { toBase64 } from "../core/bytes.js";
3
4
  //#region src/svg/svg-writer.ts
4
5
  var PAGE_GAP = 12;
5
6
  function writeSvg(laid, opts = {}) {
@@ -121,11 +122,6 @@ function imageHref(resourceName, laid) {
121
122
  return `data:${res.prepared.mimeType};base64,${toBase64(bytes)}`;
122
123
  }
123
124
  }
124
- function toBase64(bytes) {
125
- let bin = "";
126
- for (const b of bytes) bin += String.fromCharCode(b);
127
- return btoa(bin);
128
- }
129
125
  function escapeXml(s) {
130
126
  return s.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("\"", "&quot;");
131
127
  }
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "reamkit",
3
- "version": "1.0.1",
3
+ "version": "1.1.0",
4
4
  "description": "Ream — DOCX/XLSX to PDF (and SVG), built from scratch on the ECMA-376 and ISO 32000 specifications. Parse once, convert anywhere.",
5
5
  "license": "MIT",
6
- "author": "Alex Krassavin",
6
+ "author": "Alex Krassavin <info@reamkit.dev>",
7
7
  "repository": {
8
8
  "type": "git",
9
9
  "url": "git+https://github.com/alex-krassavin/reamkit.git"