reamkit 1.24.0 → 1.25.1

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.
Files changed (56) hide show
  1. package/README.md +22 -11
  2. package/dist/esm/core/converter/facade.d.ts +3 -3
  3. package/dist/esm/core/converter/facade.js +12 -0
  4. package/dist/esm/core/converter/ream.d.ts +48 -4
  5. package/dist/esm/core/converter/ream.js +25 -4
  6. package/dist/esm/core/document-model/index.d.ts +1 -1
  7. package/dist/esm/core/document-model/types.d.ts +17 -0
  8. package/dist/esm/core/outline.d.ts +17 -0
  9. package/dist/esm/core/outline.js +30 -0
  10. package/dist/esm/core/style-cascade/resolver.js +1 -0
  11. package/dist/esm/core/style-cascade/types.d.ts +3 -1
  12. package/dist/esm/excel/sheet-to-flow.d.ts +10 -0
  13. package/dist/esm/excel/sheet-to-flow.js +14 -1
  14. package/dist/esm/html/html-writer.js +3 -2
  15. package/dist/esm/index.d.ts +3 -0
  16. package/dist/esm/index.js +2 -1
  17. package/dist/esm/layout/page-doc.js +1 -1
  18. package/dist/esm/layout/styled-layout.js +41 -15
  19. package/dist/esm/markdown/markdown-writer.d.ts +41 -0
  20. package/dist/esm/markdown/markdown-writer.js +733 -0
  21. package/dist/esm/pdf/styled-page-emitter.js +20 -1
  22. package/dist/esm/pdf-reader/annots.d.ts +24 -0
  23. package/dist/esm/pdf-reader/annots.js +126 -0
  24. package/dist/esm/pdf-reader/content.d.ts +131 -5
  25. package/dist/esm/pdf-reader/content.js +169 -12
  26. package/dist/esm/pdf-reader/display.d.ts +56 -0
  27. package/dist/esm/pdf-reader/display.js +162 -0
  28. package/dist/esm/pdf-reader/document.d.ts +36 -1
  29. package/dist/esm/pdf-reader/document.js +92 -25
  30. package/dist/esm/pdf-reader/embedded-fonts.d.ts +31 -0
  31. package/dist/esm/pdf-reader/embedded-fonts.js +94 -0
  32. package/dist/esm/pdf-reader/flow-build.d.ts +61 -6
  33. package/dist/esm/pdf-reader/flow-build.js +128 -22
  34. package/dist/esm/pdf-reader/font.js +185 -4
  35. package/dist/esm/pdf-reader/image-decode.js +55 -4
  36. package/dist/esm/pdf-reader/images.d.ts +6 -0
  37. package/dist/esm/pdf-reader/images.js +25 -5
  38. package/dist/esm/pdf-reader/jpeg.d.ts +18 -0
  39. package/dist/esm/pdf-reader/jpeg.js +419 -0
  40. package/dist/esm/pdf-reader/layout.d.ts +1 -1
  41. package/dist/esm/pdf-reader/layout.js +221 -32
  42. package/dist/esm/pdf-reader/pattern-tint.d.ts +17 -0
  43. package/dist/esm/pdf-reader/pattern-tint.js +181 -0
  44. package/dist/esm/pdf-reader/reader.d.ts +9 -1
  45. package/dist/esm/pdf-reader/reader.js +23 -7
  46. package/dist/esm/pdf-reader/shading.d.ts +14 -0
  47. package/dist/esm/pdf-reader/shading.js +27 -1
  48. package/dist/esm/pdf-reader/tagged.js +156 -17
  49. package/dist/esm/pdf-reader/text.d.ts +13 -1
  50. package/dist/esm/pdf-reader/text.js +70 -3
  51. package/dist/esm/pdf-reader/vector.d.ts +25 -1
  52. package/dist/esm/pdf-reader/vector.js +168 -12
  53. package/dist/esm/pptx/slide-parser.js +5 -0
  54. package/dist/esm/word/docx-writer.js +79 -17
  55. package/dist/esm/word/drawing-parser.js +7 -1
  56. package/package.json +1 -1
package/README.md CHANGED
@@ -1,10 +1,10 @@
1
1
  # Ream
2
2
 
3
- > Read Word, Excel, PowerPoint and PDF — and convert any of them to PDF, SVG, HTML, DOCX or XLSX. From scratch, in the browser. No LibreOffice, no headless Office, no commercial SDK.
3
+ > Read Word, Excel, PowerPoint and PDF — and convert any of them to PDF, SVG, HTML, Markdown, DOCX or XLSX. From scratch, in the browser. No LibreOffice, no headless Office, no commercial SDK.
4
4
 
5
5
  Ream parses **seven document formats** — the modern Office Open XML trio (`.docx`,
6
6
  `.xlsx`, `.pptx`), `.pdf`, and the legacy binary `.doc` / `.xls` / `.ppt` — into one
7
- format-neutral interlayer, then renders that to **PDF, SVG, HTML, DOCX or XLSX**.
7
+ format-neutral interlayer, then renders that to **PDF, SVG, HTML, Markdown, DOCX or XLSX**.
8
8
  It is implemented directly from the **ECMA-376** (OOXML), **ISO 32000 / 19005**
9
9
  (PDF / PDF/A), and Microsoft's binary-format specifications — no wrapper around
10
10
  LibreOffice, headless Office, or any commercial SDK. Pure TypeScript/JavaScript on
@@ -14,7 +14,7 @@ Node.js, serverless and edge runtimes.
14
14
  | | |
15
15
  | ---------- | ------------------------------------------------------------------------------------------- |
16
16
  | **Reads** | `.docx` · `.xlsx` · `.pptx` · `.pdf` · legacy `.doc` · `.xls` · `.ppt` |
17
- | **Writes** | `.pdf` (incl. PDF/A-1/2/3, PDF/UA-1, signed, encrypted) · `.svg` · `.html` · `.docx` · `.xlsx` |
17
+ | **Writes** | `.pdf` (incl. PDF/A-1/2/3, PDF/UA-1, signed, encrypted) · `.svg` · `.html` · `.md` · `.docx` · `.xlsx` |
18
18
 
19
19
  ## Install
20
20
 
@@ -42,6 +42,7 @@ const doc = Ream.parse(bytes); // docx, xlsx, pptx or pdf — sniffed
42
42
  const pdf = await doc.convert('pdf'); // async — fetches a font if needed
43
43
  const svg = await doc.convert('svg'); // same parse, different target
44
44
  const html = await doc.convert('html'); // flowed HTML — needs no fonts at all
45
+ const md = await doc.convert('md'); // GitHub-Flavored Markdown — same, narrower
45
46
  const docx = await doc.convert('docx'); // write WordprocessingML back out
46
47
  const xlsx = await doc.convert('xlsx'); // write SpreadsheetML back (xlsx source)
47
48
 
@@ -160,7 +161,7 @@ automatically from the document's `docProps/core.xml`), `attachments`
160
161
 
161
162
  ### Lower-level APIs
162
163
 
163
- - `docxReader` / `xlsxReader`, `svgWriter`, `htmlWriter`, `docxWriter` — the `@experimental`
164
+ - `docxReader` / `xlsxReader`, `svgWriter`, `htmlWriter`, `markdownWriter`, `docxWriter` — the `@experimental`
164
165
  reader/writer interfaces of the interlayer, for building custom pipelines (and
165
166
  keeping unused formats out of your bundle); `layoutStyledDocument` produces the
166
167
  frozen page model (`PageItem` pages in a top-left `Pt` frame) the page-based
@@ -190,17 +191,27 @@ preview, flowed HTML export, and **docx + xlsx output** (write WordprocessingML
190
191
  tagged PDF from its structure tree (headings, tables, lists, reading order), an
191
192
  untagged one heuristically from glyph positions (lines, paragraphs, headings,
192
193
  and a clean two-column split). It lifts back the text (via each font's
193
- `/ToUnicode`), raster images (JPEG verbatim; PNG/Flate/LZW/CCITT-fax decoded and
194
- re-encoded), `/Link` hyperlinks, form-XObject content, and filled / stroked /
195
- gradient vector shapes. It reads modern compressed files (cross-reference + object
196
- streams) and encrypted ones (RC4 / AES — the user password is passed to
197
- `Ream.parse(bytes, { password })`, defaulting to the permissions-only case).
194
+ `/ToUnicode`, or the embedded program's own `cmap` where there is none), the
195
+ font programs themselves, raster images (JPEG verbatim; PNG/Flate/LZW/CCITT-fax
196
+ decoded and re-encoded), `/Link` hyperlinks, form-XObject content, annotation
197
+ appearances, and the page's artwork: filled / stroked / gradient shapes,
198
+ clipping paths, tiling patterns, constant alpha, and the Type 3 glyphs that are
199
+ drawings rather than letters. It reads modern compressed files (cross-reference
200
+ + object streams) and encrypted ones (RC4 / AES — the user password is passed to
201
+ `Ream.parse(bytes, { password })`, defaulting to the permissions-only case); a
202
+ filter it does not carry can be handed to it through
203
+ `Ream.parse(bytes, { filters })`.
204
+
205
+ A form or a drawing is not a reflowable document, so
206
+ `Ream.parse(bytes, { pdfLayout: 'positional' })` reads the page instead: every
207
+ line stands where its glyphs stand, beside the artwork, the way up the page is
208
+ shown.
198
209
 
199
210
  **Reads PowerPoint, too.** `Ream.parse` accepts a `.pptx` and turns each slide
200
211
  into a page at the deck size — text boxes (with run formatting, alignment,
201
212
  bullets and indents), layout/master placeholders, pictures, shapes, DrawingML
202
213
  tables, embedded charts, theme colours, slide backgrounds, grouped shapes and
203
- hyperlinks — then converts onward to PDF, SVG, HTML or DOCX like any source.
214
+ hyperlinks — then converts onward to PDF, SVG, HTML, Markdown or DOCX like any source.
204
215
 
205
216
  **Reads legacy `.doc`, `.xls` and `.ppt`, too.** The binary Word / Excel /
206
217
  PowerPoint 97–2003 formats (OLE2/CFB) parse through a shared container reader: a
@@ -217,7 +228,7 @@ text (with run and paragraph formatting), embedded images, per-shape placement
217
228
  (anchored text boxes and pictures at their slide rectangles) and decorative
218
229
  autoshapes (preset or exact freeform geometry, with fill / line colours resolved
219
230
  through the slide's colour scheme), one page per slide — all convert onward to PDF,
220
- SVG, HTML, or back to `.docx` / `.xlsx` like any source.
231
+ SVG, HTML, Markdown, or back to `.docx` / `.xlsx` like any source.
221
232
 
222
233
  See [`CHANGELOG.md`](./CHANGELOG.md) for the release history; the docs
223
234
  [**Scope**](https://reamkit.dev/guides/scope/) guide has the full feature matrix
@@ -9,10 +9,10 @@ import { ProjectSheetOptions } from '../../excel/sheet-to-flow.js';
9
9
  /** Options for a {@link Converter.convert} call (extends the docx PDF options). */
10
10
  export interface ConvertOptions extends ConvertDocxOptions {
11
11
  /**
12
- * Target: 'pdf' (default), 'svg' (page-stack preview), 'html'/'docx' (flowed),
13
- * or 'xlsx' (the native grid writer — spreadsheet input only).
12
+ * Target: 'pdf' (default), 'svg' (page-stack preview), 'html'/'md'/'docx'
13
+ * (flowed), or 'xlsx' (the native grid writer — spreadsheet input only).
14
14
  */
15
- readonly to?: 'pdf' | 'svg' | 'html' | 'docx' | 'xlsx';
15
+ readonly to?: 'pdf' | 'svg' | 'html' | 'md' | 'docx' | 'xlsx';
16
16
  /**
17
17
  * Strict mode (handoff v1 §5): throw ConversionLossError on the first
18
18
  * recorded loss instead of returning it in the report.
@@ -14,6 +14,7 @@ import { flowRenderOptions } from "./project.js";
14
14
  import { writeDocx } from "../../word/docx-writer.js";
15
15
  import { writeXlsx } from "../../excel/xlsx-writer.js";
16
16
  import { writeHtml } from "../../html/html-writer.js";
17
+ import { writeMarkdown } from "../../markdown/markdown-writer.js";
17
18
  import { writeSvg } from "../../svg/svg-writer.js";
18
19
  import { convertDocxToPdf } from "../../word/docx-to-pdf.js";
19
20
  import { convertXlsxToPdf } from "../../excel/xlsx-to-pdf.js";
@@ -87,6 +88,17 @@ function createConverter(opts = {}) {
87
88
  losses
88
89
  };
89
90
  }
91
+ if (to === "md") {
92
+ const { doc: flow, losses: readLosses } = readToFlow(reader, bytes, rest.now ? { now: rest.now } : void 0);
93
+ losses.push(...readLosses);
94
+ const markdown = writeMarkdown(flow);
95
+ losses.push(...markdown.losses);
96
+ if (strict && losses.length > 0) throw new ConversionLossError(losses[0]);
97
+ return {
98
+ bytes: markdown.bytes,
99
+ losses
100
+ };
101
+ }
90
102
  if (to === "xlsx") {
91
103
  const { doc, losses: readLosses } = reader.read(bytes);
92
104
  losses.push(...readLosses);
@@ -6,9 +6,10 @@ 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 { SheetDoc } from '../ir/sheet.js';
9
+ import { StreamFilters } from '../../pdf-reader/document.js';
9
10
  import { SignatureOptions, StyledRenderOptions } from '../../pdf/index.js';
10
11
  /** The output formats {@link Ream.convert} can produce. */
11
- export type ReamTarget = 'pdf' | 'svg' | 'html' | 'docx' | 'xlsx';
12
+ export type ReamTarget = 'pdf' | 'svg' | 'html' | 'md' | 'docx' | 'xlsx';
12
13
  /** Options for {@link Ream.parse}. */
13
14
  export interface ReamParseOptions {
14
15
  /** Reader registry override; defaults to the built-in docx + xlsx readers. */
@@ -20,6 +21,30 @@ export interface ReamParseOptions {
20
21
  * encryption (EP14); an encrypted OOXML package always names a password.
21
22
  */
22
23
  readonly password?: string;
24
+ /**
25
+ * PDF only: what to read out of the page. `'flow'` (the default) reconstructs
26
+ * a re-flowable document — paragraphs and tables in reading order, from the
27
+ * structure tree where the file has one. `'positional'` keeps the page as a
28
+ * page: every line stands where its glyphs stand, beside the rules and fills
29
+ * the page draws. A form or a drawing needs the second; a report to be edited
30
+ * or converted to markdown needs the first.
31
+ */
32
+ readonly pdfLayout?: 'flow' | 'positional';
33
+ /**
34
+ * PDF only: decoders for `/Filter` names the reader does not implement
35
+ * (§7.4). A filter it cannot undo leaves that stream unread — and when the
36
+ * unread one is the cross-reference, the whole document is missing — so a
37
+ * caller who needs such a file supplies the decoder rather than the library
38
+ * carrying one for every filter anyone might write:
39
+ *
40
+ * ```ts
41
+ * import { brotliDecompressSync } from 'node:zlib';
42
+ * Ream.parse(pdf, { filters: { BrotliDecode: (b) => brotliDecompressSync(b) } });
43
+ * ```
44
+ *
45
+ * Absent, or throwing, the filter is reported unreadable by name.
46
+ */
47
+ readonly filters?: StreamFilters;
23
48
  }
24
49
  /**
25
50
  * Options for {@link Ream.convert} and {@link Ream.convertWithReport}. Extends
@@ -62,6 +87,25 @@ export interface ReamConvertOptions extends Omit<StyledRenderOptions, 'registry'
62
87
  * the code resolves, and omitted it is dropped exactly as before.
63
88
  */
64
89
  readonly fileName?: string;
90
+ /**
91
+ * Markdown only: how a picture reaches the output — inlined as a `data:` URI
92
+ * (the default), named under `./media/` for a caller that writes the bytes
93
+ * itself, or dropped. See {@link MarkdownWriteOptions}.
94
+ */
95
+ readonly images?: 'dataUri' | 'link' | 'drop';
96
+ /**
97
+ * Markdown only: what a page break becomes — nothing (the default), or the
98
+ * `---` thematic break a slide deck wants between its slides. See
99
+ * {@link MarkdownWriteOptions}.
100
+ */
101
+ readonly pageBreaks?: 'rule' | 'drop';
102
+ /**
103
+ * Markdown from a SPREADSHEET only: open each sheet with a heading carrying
104
+ * its tab name. On by default — markdown has no pages to tell one sheet from
105
+ * the next by, so without them a workbook is a pile of tables with nothing to
106
+ * say which is which. Set `false` for the bare tables.
107
+ */
108
+ readonly sheetNames?: boolean;
65
109
  }
66
110
  /**
67
111
  * The object face of the library: parse a document once into the format-neutral
@@ -121,9 +165,9 @@ export declare class Ream {
121
165
  /**
122
166
  * Convert the parsed document to `to`, returning the output bytes together with
123
167
  * the accumulated {@link Loss} report (read-time losses plus any added while
124
- * writing). HTML, DOCX and XLSX are produced straight from the interlayer — no
125
- * layout, no fonts, zero I/O; SVG and PDF run the layout engine and resolve
126
- * fonts first.
168
+ * writing). HTML, Markdown, DOCX and XLSX are produced straight from the
169
+ * interlayer — no layout, no fonts, zero I/O; SVG and PDF run the layout
170
+ * engine and resolve fonts first.
127
171
  *
128
172
  * @param to The target format. `'xlsx'` requires a spreadsheet source.
129
173
  * @param options Font resolution and target-specific options.
@@ -12,6 +12,7 @@ import { flowRenderOptions } from "./project.js";
12
12
  import { writeDocx } from "../../word/docx-writer.js";
13
13
  import { writeXlsx } from "../../excel/xlsx-writer.js";
14
14
  import { writeHtml } from "../../html/html-writer.js";
15
+ import { writeMarkdown } from "../../markdown/markdown-writer.js";
15
16
  import { writeSvg } from "../../svg/svg-writer.js";
16
17
  import { DEFAULT_READERS, resolveFontsViaChain, toFlowDoc } from "./facade.js";
17
18
  import { familiesInFlow } from "../fonts/families.js";
@@ -85,7 +86,11 @@ var Ream = class Ream {
85
86
  }
86
87
  const reader = readers.find((r) => r.sniff(source));
87
88
  if (!reader) throw new Error(`Unrecognized document format (readers: ${readers.map((r) => r.id).join(", ")})`);
88
- const { doc, losses } = reader.read(source, { password: options.password });
89
+ const { doc, losses } = reader.read(source, {
90
+ password: options.password,
91
+ ...options.pdfLayout ? { pdfLayout: options.pdfLayout } : {},
92
+ ...options.filters ? { filters: options.filters } : {}
93
+ });
89
94
  const sheet = doc.kind === "sheet" ? doc : void 0;
90
95
  return new Ream(toFlowDoc(doc), sheet, losses, bytes, reader.id);
91
96
  }
@@ -107,9 +112,9 @@ var Ream = class Ream {
107
112
  /**
108
113
  * Convert the parsed document to `to`, returning the output bytes together with
109
114
  * the accumulated {@link Loss} report (read-time losses plus any added while
110
- * writing). HTML, DOCX and XLSX are produced straight from the interlayer — no
111
- * layout, no fonts, zero I/O; SVG and PDF run the layout engine and resolve
112
- * fonts first.
115
+ * writing). HTML, Markdown, DOCX and XLSX are produced straight from the
116
+ * interlayer — no layout, no fonts, zero I/O; SVG and PDF run the layout
117
+ * engine and resolve fonts first.
113
118
  *
114
119
  * @param to The target format. `'xlsx'` requires a spreadsheet source.
115
120
  * @param options Font resolution and target-specific options.
@@ -132,6 +137,22 @@ var Ream = class Ream {
132
137
  losses
133
138
  };
134
139
  }
140
+ if (to === "md") {
141
+ const markdown = writeMarkdown(this.sheet && options.sheetNames !== false ? projectSheetDoc(this.sheet, {
142
+ ...options.now ? { now: options.now } : {},
143
+ ...options.fileName ? { fileName: options.fileName } : {},
144
+ sheetHeadings: true
145
+ }) : flow, {
146
+ ...options.images ? { images: options.images } : {},
147
+ ...options.pageBreaks ? { pageBreaks: options.pageBreaks } : {}
148
+ });
149
+ losses.push(...markdown.losses);
150
+ this.enforceStrict(options, losses);
151
+ return {
152
+ bytes: markdown.bytes,
153
+ losses
154
+ };
155
+ }
135
156
  if (to === "docx") {
136
157
  const docx = writeDocx(flow);
137
158
  losses.push(...docx.losses);
@@ -5,4 +5,4 @@
5
5
  *
6
6
  * @packageDocumentation
7
7
  */
8
- export type { AbstractNumbering, Alignment, Border, BorderStyle, BodyElement, Comment, CellBorders, CellMerge, DocumentInfo, CellMargins, CellProperties, CellShading, CellDataBar, CellIcon, CellIconShape, CellSparkline, Chart, ChartBlock, ChartDataPoint, ChartLineStyle, ChartMarker, ChartMarkerSymbol, ChartSeries, ChartType, CustomGeometry, CustomPathCmd, DocumentModel, FontFamilyMap, HeaderFooterReference, HeaderFooterType, ImageBlock, ImageCrop, InlineImage, MathAccent, MathBar, MathDelimiter, MathEqArray, MathFraction, MathFunc, MathGroupChr, MathLimit, MathMatrix, MathNary, MathNode, MathRadical, MathRow, MathRun, MathScript, Numbering, NumberingFormat, NumberingInstance, NumberingLevel, PictureBullet, PictureOutline, NumberingReference, PageMargins, PageSize, Paragraph, ParagraphProperties, FrameProperties, RelativeSize, TabStop, Run, RunProperties, RowConditionalFormat, RowProperties, Section, FloatAnchor, SectionColumns, SectionProperties, ShapeBlock, ShapeShadow, ShapeDash, ShapeFill, ShapeFillKind, ShapeGeometry, ShapeGroupChild, ShapeLine, LineEnd, ShapeTextBody, ShapeTransform, Style, StyleSheet, StyleType, Table, TableCell, TableLook, TableProperties, TableStyleCondition, TableStyleConditionType, TableStyleLayer, TableRow, UnderlineStyle, VerticalAlign, } from './types.js';
8
+ export type { AbstractNumbering, Alignment, Border, BorderStyle, BodyElement, Comment, CellBorders, CellMerge, DocumentInfo, CellMargins, CellProperties, CellShading, CellDataBar, CellIcon, CellIconShape, CellSparkline, Chart, ChartBlock, ChartDataPoint, ChartLineStyle, ChartMarker, ChartMarkerSymbol, ChartSeries, ChartType, CustomGeometry, CustomPathCmd, DocumentModel, FontFamilyMap, HeaderFooterReference, HeaderFooterType, ImageBlock, ImageCrop, InlineImage, MathAccent, MathBar, MathDelimiter, MathEqArray, MathFraction, MathFunc, MathGroupChr, MathLimit, MathMatrix, MathNary, MathNode, MathRadical, MathRow, MathRun, MathScript, Numbering, NumberingFormat, NumberingInstance, NumberingLevel, PictureBullet, PictureOutline, NumberingReference, PageMargins, PageSize, Paragraph, ParagraphProperties, FrameProperties, RelativeSize, TabStop, Run, RunProperties, TextOutline, RowConditionalFormat, RowProperties, Section, FloatAnchor, SectionColumns, SectionProperties, ShapeBlock, ShapeShadow, ShapeDash, ShapeFill, ShapeFillKind, ShapeGeometry, ShapeGroupChild, ShapeLine, LineEnd, ShapeTextBody, ShapeTransform, Style, StyleSheet, StyleType, Table, TableCell, TableLook, TableProperties, TableStyleCondition, TableStyleConditionType, TableStyleLayer, TableRow, UnderlineStyle, VerticalAlign, } from './types.js';
@@ -63,6 +63,17 @@ export interface RunProperties {
63
63
  * (negative tightens). Word states it in twentieths of a point.
64
64
  */
65
65
  readonly letterSpacingPt?: Pt;
66
+ /**
67
+ * §21.1.2.3.9 `a:rPr/a:ln` — a line drawn round the glyphs themselves, which
68
+ * DrawingML puts on a run and ISO 32000-1 §9.3.6 calls a text rendering mode
69
+ * that strokes as well as fills.
70
+ */
71
+ readonly textOutline?: TextOutline;
72
+ }
73
+ /** A line drawn round a run's glyphs: its colour and how wide the pen is. */
74
+ export interface TextOutline {
75
+ readonly colorHex: string;
76
+ readonly widthPt: Pt;
66
77
  }
67
78
  /**
68
79
  * ECMA-376 Part 1 §17.3.1 — Paragraph Properties (`pPr`). All lengths in
@@ -1169,6 +1180,12 @@ export interface ShapeTextBody {
1169
1180
  * quarter clockwise), `vert270` bottom-to-top.
1170
1181
  */
1171
1182
  readonly vertical?: 'vert' | 'vert270';
1183
+ /**
1184
+ * §20.1.10.55 `a:bodyPr @upright` — the words stay level however far the
1185
+ * shape is turned. bnc762542.xlsx turns each legend label a quarter and asks
1186
+ * for this, and every reader draws those labels lying flat.
1187
+ */
1188
+ readonly upright?: boolean;
1172
1189
  /**
1173
1190
  * §20.1.10.28 `a:spAutoFit` — the SHAPE follows its text: its height is
1174
1191
  * whatever the text needs, whatever the stated box says.
@@ -0,0 +1,17 @@
1
+ import { ResolvedParagraphProperties } from './style-cascade/index.js';
2
+ /**
3
+ * The heading level (1–6) a paragraph's resolved properties describe, or
4
+ * `undefined` when it is body text.
5
+ *
6
+ * @param resolved The paragraph's fully-resolved properties.
7
+ * @returns The heading level 1–6, or `undefined` for body text.
8
+ */
9
+ export declare function headingLevelOf(resolved: ResolvedParagraphProperties): number | undefined;
10
+ /**
11
+ * The heading level a `"Heading N"` / `"Title"` / `"Subtitle"` style id names,
12
+ * for a heading style that declares no outline level of its own.
13
+ *
14
+ * @param styleId The paragraph's style id, if it has one.
15
+ * @returns The heading level 1–6, or `undefined` when the id names no heading.
16
+ */
17
+ export declare function headingLevelFromStyleId(styleId: string | undefined): number | undefined;
@@ -0,0 +1,30 @@
1
+ //#region src/core/outline.ts
2
+ /** The deepest heading level the consuming media can express. */
3
+ var MAX_HEADING_LEVEL = 6;
4
+ /**
5
+ * The heading level (1–6) a paragraph's resolved properties describe, or
6
+ * `undefined` when it is body text.
7
+ *
8
+ * @param resolved The paragraph's fully-resolved properties.
9
+ * @returns The heading level 1–6, or `undefined` for body text.
10
+ */
11
+ function headingLevelOf(resolved) {
12
+ const lvl = resolved.outlineLevel;
13
+ if (lvl !== void 0 && lvl >= 0 && lvl <= 8) return Math.min(lvl + 1, MAX_HEADING_LEVEL);
14
+ return headingLevelFromStyleId(resolved.styleId);
15
+ }
16
+ /**
17
+ * The heading level a `"Heading N"` / `"Title"` / `"Subtitle"` style id names,
18
+ * for a heading style that declares no outline level of its own.
19
+ *
20
+ * @param styleId The paragraph's style id, if it has one.
21
+ * @returns The heading level 1–6, or `undefined` when the id names no heading.
22
+ */
23
+ function headingLevelFromStyleId(styleId) {
24
+ if (styleId === void 0) return void 0;
25
+ const m = /^Heading\s*([1-9])$/i.exec(styleId);
26
+ if (m) return Math.min(Number(m[1]), MAX_HEADING_LEVEL);
27
+ if (/^(Title|Subtitle)$/i.test(styleId)) return 1;
28
+ }
29
+ //#endregion
30
+ export { headingLevelOf };
@@ -136,6 +136,7 @@ function mergeRun(base, override) {
136
136
  rtl: override.rtl ?? base.rtl,
137
137
  ...lang !== void 0 ? { lang } : {},
138
138
  ...(override.shadingColorHex ?? base.shadingColorHex) !== void 0 ? { shadingColorHex: override.shadingColorHex ?? base.shadingColorHex } : {},
139
+ ...(override.textOutline ?? base.textOutline) !== void 0 ? { textOutline: override.textOutline ?? base.textOutline } : {},
139
140
  ...(override.letterSpacingPt ?? base.letterSpacingPt) !== void 0 ? { letterSpacingPt: override.letterSpacingPt ?? base.letterSpacingPt } : {}
140
141
  };
141
142
  }
@@ -1,4 +1,4 @@
1
- import { Alignment, CellBorders, CellShading, FontFamilyMap, FrameProperties, NumberingReference, RunProperties, TabStop, UnderlineStyle, VerticalAlign } from '../document-model/index.js';
1
+ import { Alignment, CellBorders, CellShading, FontFamilyMap, FrameProperties, NumberingReference, RunProperties, TabStop, TextOutline, UnderlineStyle, VerticalAlign } from '../document-model/index.js';
2
2
  import { Pt } from '../ir/index.js';
3
3
  /**
4
4
  * A run's fully-resolved properties: every field required because the cascade
@@ -30,6 +30,8 @@ export interface ResolvedRunProperties {
30
30
  readonly shadingColorHex?: string;
31
31
  /** §17.3.2.35 — extra space between the run's characters, in points. */
32
32
  readonly letterSpacingPt?: Pt;
33
+ /** §21.1.2.3.9 — a line drawn round the glyphs themselves. */
34
+ readonly textOutline?: TextOutline;
33
35
  }
34
36
  /**
35
37
  * A paragraph's fully-resolved properties: every field required, the cascade
@@ -42,6 +42,16 @@ export interface ProjectSheetOptions {
42
42
  * supplies it; absent, the code is dropped exactly as before.
43
43
  */
44
44
  readonly fileName?: string;
45
+ /**
46
+ * Open each printed sheet with a heading carrying its NAME.
47
+ *
48
+ * Off by default, and deliberately: a printed page shows no tab name — Excel
49
+ * and Calc both emit it nowhere — so the paginated targets must not see one.
50
+ * A flowed target has no pages to tell one sheet from the next by, and a
51
+ * workbook rendered without them is a pile of tables with nothing to say
52
+ * which is which; markdown asks for this, and gets `# Sheet1`.
53
+ */
54
+ readonly sheetHeadings?: boolean;
45
55
  }
46
56
  /**
47
57
  * Project a {@link SheetDoc} into a {@link FlowDoc} (E-SHEET SA2): each grid sheet
@@ -67,7 +67,20 @@ function projectSheetDoc(sheet, options = {}) {
67
67
  const sheetSection = withHeaderFooter(sectionFromWorksheet(ws.grid), ws, headersFooters, scaleSink.value, sheet.styles.fonts[0]?.sizePt, printed, options.fileName, sheet.themePalette, options.now);
68
68
  if (printed === 0) firstSheetSection = sheetSection;
69
69
  sheetSections.push(sheetSection);
70
- if (printed > 0) body.push({
70
+ if (options.sheetHeadings === true) body.push({
71
+ kind: "paragraph",
72
+ paragraph: {
73
+ properties: {
74
+ outlineLevel: 0,
75
+ ...printed > 0 ? { pageBreakBefore: true } : {}
76
+ },
77
+ runs: [{
78
+ text: ws.name,
79
+ properties: {}
80
+ }]
81
+ }
82
+ });
83
+ else if (printed > 0) body.push({
71
84
  kind: "paragraph",
72
85
  paragraph: PAGE_BREAK_PARAGRAPH
73
86
  });
@@ -1,4 +1,5 @@
1
1
  import { FEATURES } from "../core/ir/features.js";
2
+ import { headingLevelOf } from "../core/outline.js";
2
3
  import { detectImageFormat } from "../core/images.js";
3
4
  import { PathBuilder, flipTransform, svgPathData } from "../core/vector.js";
4
5
  import { EMPTY_STYLE_SHEET, resolveParagraphProperties, resolveRunProperties } from "../core/style-cascade/resolver.js";
@@ -321,8 +322,8 @@ function emitOneShape(out, shape, ctx) {
321
322
  }
322
323
  function emitParagraph(out, p, ctx) {
323
324
  const resolved = resolveParagraphProperties(p.properties, EMPTY_STYLE_SHEET);
324
- const lvl = resolved.outlineLevel;
325
- const tag = lvl !== void 0 && lvl >= 0 && lvl <= 8 ? `h${Math.min(lvl, 5) + 1}` : "p";
325
+ const lvl = headingLevelOf(resolved);
326
+ const tag = lvl !== void 0 ? `h${String(lvl)}` : "p";
326
327
  const style = paragraphCss(resolved);
327
328
  const dir = resolved.bidi ? " dir=\"rtl\"" : "";
328
329
  const anchored = (p.bookmarks ?? []).filter((b) => ctx.referencedAnchors.has(b));
@@ -49,6 +49,9 @@ export type { FontProvider, FontRequest, FontAnswer } from './core/fonts/provide
49
49
  export { svgWriter, writeSvg } from './svg/svg-writer.js';
50
50
  export type { SvgWriteOptions } from './svg/svg-writer.js';
51
51
  export { htmlWriter, writeHtml } from './html/html-writer.js';
52
+ export { markdownWriter, writeMarkdown } from './markdown/markdown-writer.js';
53
+ export type { StreamFilter, StreamFilters } from './pdf-reader/document.js';
54
+ export type { MarkdownWriteOptions } from './markdown/markdown-writer.js';
52
55
  export { docxWriter, writeDocx } from './word/docx-writer.js';
53
56
  export { layoutStyledDocument } from './layout/styled-layout.js';
54
57
  export type { LaidOutDocument } from './layout/page-doc.js';
package/dist/esm/index.js CHANGED
@@ -17,8 +17,9 @@ 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
18
  import { docxWriter, writeDocx } from "./word/docx-writer.js";
19
19
  import { htmlWriter, writeHtml } from "./html/html-writer.js";
20
+ import { markdownWriter, writeMarkdown } from "./markdown/markdown-writer.js";
20
21
  import { svgWriter, writeSvg } from "./svg/svg-writer.js";
21
22
  import { createConverter } from "./core/converter/facade.js";
22
23
  import { WrongPasswordError, isEncryptedPackage } from "./core/crypto/offcrypto.js";
23
24
  import { Ream } from "./core/converter/ream.js";
24
- export { ConversionLossError, FEATURES, FontRegistry, NO_FONT, Ream, ResourceStore, WrongPasswordError, callerFontProvider, chainProviders, createConverter, createHyphenator, createLanguageHyphenator, docxReader, docxWriter, eighthPtToPt, embeddedDocFontProvider, emuToPt, fetchFontSet, formatLoss, getHyphenator, halfPtToPt, htmlWriter, inchToPt, isEmbeddingRestricted, isEncryptedPackage, layoutStyledDocument, localFontProvider, mmToPt, parseTtf, pt, pxToPt, readDocx, readOs2FsType, readXlsx, remoteFontProvider, renderStyledPdf, resolveFamilyKey, signPdf, splitPatternBundle, subsetTtf, svgWriter, twipsToPt, writeDocx, writeHtml, writeSvg, xlsxReader };
25
+ export { ConversionLossError, FEATURES, FontRegistry, NO_FONT, Ream, ResourceStore, WrongPasswordError, callerFontProvider, chainProviders, createConverter, createHyphenator, createLanguageHyphenator, docxReader, docxWriter, eighthPtToPt, embeddedDocFontProvider, emuToPt, fetchFontSet, formatLoss, getHyphenator, halfPtToPt, htmlWriter, inchToPt, isEmbeddingRestricted, isEncryptedPackage, layoutStyledDocument, localFontProvider, markdownWriter, mmToPt, parseTtf, pt, pxToPt, readDocx, readOs2FsType, readXlsx, remoteFontProvider, renderStyledPdf, resolveFamilyKey, signPdf, splitPatternBundle, subsetTtf, svgWriter, twipsToPt, writeDocx, writeHtml, writeMarkdown, writeSvg, xlsxReader };
@@ -14,7 +14,7 @@ function paintPlan(commands) {
14
14
  behind.push(c);
15
15
  continue;
16
16
  }
17
- if (c.z !== void 0 && c.type !== "line" && c.type !== "fill" && c.type !== "border") {
17
+ if (c.z !== void 0 && c.type !== "fill" && c.type !== "border" && (c.type !== "line" || c.pictureId !== void 0)) {
18
18
  const key = c.pictureId !== void 0 ? `p${c.pictureId}` : `i${orderOf.size}`;
19
19
  const run = ordered.get(key);
20
20
  if (run) run.push(c);
@@ -4,6 +4,7 @@ import { shapeText } from "../core/font/opentype-layout.js";
4
4
  import { createFontMeasure } from "../core/font/measure.js";
5
5
  import { halfPtToPt, pt } from "../core/ir/units.js";
6
6
  import { ResourceStore } from "../core/ir/resources.js";
7
+ import { headingLevelOf } from "../core/outline.js";
7
8
  import { prepareImage } from "../core/images.js";
8
9
  import { PathBuilder, flipTransform } from "../core/vector.js";
9
10
  import { isEmf, readEmf } from "../core/metafile/emf.js";
@@ -1049,6 +1050,7 @@ function layoutShapeBlock(shape, options, fontResources, imageResources, content
1049
1050
  const textLineGaps = /* @__PURE__ */ new Map();
1050
1051
  let textHeightPt = 0;
1051
1052
  const vertical = text?.vertical;
1053
+ const upright = text?.upright;
1052
1054
  const warp = text?.warp;
1053
1055
  if (text && text.content.length > 0) {
1054
1056
  const innerWidth = warp ? Number.MAX_SAFE_INTEGER : vertical ? Math.max(1, heightPt - insetTopPt - insetBottomPt) : Math.max(1, widthPt - insetLeftPt - insetRightPt);
@@ -1235,6 +1237,7 @@ function layoutShapeBlock(shape, options, fontResources, imageResources, content
1235
1237
  insetTopPt,
1236
1238
  insetBottomPt,
1237
1239
  anchor: text?.anchor ?? "t",
1240
+ ...upright ? { upright } : {},
1238
1241
  ...vertical ? { vertical } : {},
1239
1242
  ...shape.altText ? { altText: shape.altText } : {},
1240
1243
  ...shape.float ? { float: shape.float } : {}
@@ -1885,6 +1888,9 @@ function chartPageItems(laid, x, bottomYUp, pageHeight, structId) {
1885
1888
  var nextPictureId = 1;
1886
1889
  function emitShapeText(sh, x, bottomYUp, sink, pageHeight, figId) {
1887
1890
  if (sh.textLines.length === 0) return;
1891
+ const turnDeg = sh.upright === true || sh.flipH || sh.flipV ? 0 : -(sh.rotation60k || 0) / 6e4;
1892
+ const centreX = x + sh.widthPt / 2;
1893
+ const centreY = bottomYUp + sh.heightPt / 2;
1888
1894
  if (sh.vertical) {
1889
1895
  const up = sh.vertical === "vert270";
1890
1896
  const innerHeight = Math.max(1, sh.heightPt - sh.insetTopPt - sh.insetBottomPt);
@@ -1894,12 +1900,13 @@ function emitShapeText(sh, x, bottomYUp, sink, pageHeight, figId) {
1894
1900
  const lineOffset = alignmentOffset(line.resolved.alignment, line.contentWidthPt, innerHeight);
1895
1901
  const off = lineBaselineOffset(line, line.resolved);
1896
1902
  const baselineAcross = up ? across + h - off : across + off;
1903
+ const origin = spinAboutCentre(up ? x + baselineAcross : x + sh.widthPt - baselineAcross, up ? bottomYUp + sh.insetBottomPt + lineOffset : bottomYUp + sh.heightPt - sh.insetTopPt - lineOffset, centreX, centreY, turnDeg);
1897
1904
  sink.push({
1898
1905
  type: "line",
1899
1906
  line,
1900
- originX: pt(up ? x + baselineAcross : x + sh.widthPt - baselineAcross),
1901
- baselineY: pt(pageHeight - (up ? bottomYUp + sh.insetBottomPt + lineOffset : bottomYUp + sh.heightPt - sh.insetTopPt - lineOffset)),
1902
- rotationDeg: up ? 90 : -90,
1907
+ originX: pt(origin.x),
1908
+ baselineY: pt(pageHeight - origin.y),
1909
+ rotationDeg: (up ? 90 : -90) + turnDeg,
1903
1910
  ...figId !== void 0 ? { structId: figId } : {}
1904
1911
  });
1905
1912
  across += h;
@@ -1939,17 +1946,44 @@ function emitShapeText(sh, x, bottomYUp, sink, pageHeight, figId) {
1939
1946
  return;
1940
1947
  }
1941
1948
  const indentLeft = line.resolved.indentLeft + (line.firstLine ? line.resolved.indentFirstLine : 0);
1949
+ const origin = spinAboutCentre(x + sh.insetLeftPt + indentLeft + lineOffset, textY + lineBaselineOffset(line, line.resolved), centreX, centreY, turnDeg);
1942
1950
  sink.push({
1943
1951
  type: "line",
1944
1952
  line,
1945
- originX: pt(x + sh.insetLeftPt + indentLeft + lineOffset),
1946
- baselineY: pt(pageHeight - (textY + lineBaselineOffset(line, line.resolved))),
1953
+ originX: pt(origin.x),
1954
+ baselineY: pt(pageHeight - origin.y),
1955
+ ...turnDeg !== 0 ? { rotationDeg: turnDeg } : {},
1947
1956
  ...figId !== void 0 ? { structId: figId } : {}
1948
1957
  });
1949
1958
  textY -= sh.textLineGaps?.get(i) ?? 0;
1950
1959
  });
1951
1960
  }
1952
1961
  /**
1962
+ * A point spun about another, counter-clockwise in the y-up page frame.
1963
+ *
1964
+ * @param px The point's x.
1965
+ * @param py The point's y (y-up).
1966
+ * @param cx The centre's x.
1967
+ * @param cy The centre's y (y-up).
1968
+ * @param deg How far to turn, counter-clockwise; zero returns the point.
1969
+ * @returns The turned point.
1970
+ */
1971
+ function spinAboutCentre(px, py, cx, cy, deg) {
1972
+ if (deg === 0) return {
1973
+ x: px,
1974
+ y: py
1975
+ };
1976
+ const rad = deg * Math.PI / 180;
1977
+ const cos = Math.cos(rad);
1978
+ const sin = Math.sin(rad);
1979
+ const dx = px - cx;
1980
+ const dy = py - cy;
1981
+ return {
1982
+ x: cx + dx * cos - dy * sin,
1983
+ y: cy + dx * sin + dy * cos
1984
+ };
1985
+ }
1986
+ /**
1953
1987
  * §20.1.9.10 — place a WordArt body's lines in the block's own frame and hand
1954
1988
  * each one the warp that maps that frame onto the shape's box.
1955
1989
  *
@@ -4339,16 +4373,8 @@ function resolveCellBorders(cellBorders, tableBorders, rowIdx, colStart, colEnd,
4339
4373
  return out;
4340
4374
  }
4341
4375
  function paragraphStructType(resolved) {
4342
- const lvl = resolved.outlineLevel;
4343
- if (lvl !== void 0 && lvl >= 0 && lvl <= 8) return `H${Math.min(lvl, 5) + 1}`;
4344
- return headingFromStyleId(resolved.styleId) ?? "P";
4345
- }
4346
- function headingFromStyleId(styleId) {
4347
- if (!styleId) return null;
4348
- const m = /^Heading\s*([1-9])$/i.exec(styleId);
4349
- if (m) return `H${Math.min(Number(m[1]), 6)}`;
4350
- if (/^(Title|Subtitle)$/i.test(styleId)) return "H1";
4351
- return null;
4376
+ const lvl = headingLevelOf(resolved);
4377
+ return lvl !== void 0 ? `H${String(lvl)}` : "P";
4352
4378
  }
4353
4379
  function dominantParagraphLang(lines) {
4354
4380
  const counts = /* @__PURE__ */ new Map();
@@ -0,0 +1,41 @@
1
+ import { DocumentWriter, WriteResult } from '../core/ir/adapters.js';
2
+ import { FlowDoc } from '../core/ir/flow.js';
3
+ /** Options for {@link writeMarkdown}. */
4
+ export interface MarkdownWriteOptions {
5
+ /**
6
+ * How a picture reaches the output. `'dataUri'` (the default) inlines the
7
+ * bytes as a `data:` URI, keeping the single-blob {@link WriteResult}
8
+ * contract and the zero-I/O promise; `'link'` emits a relative
9
+ * `./media/…` path and records that the bytes were not written anywhere;
10
+ * `'drop'` omits pictures entirely.
11
+ */
12
+ readonly images?: 'dataUri' | 'link' | 'drop';
13
+ /**
14
+ * What a page break becomes. `'drop'` (the default) leaves it out and
15
+ * reports it: a paginated document breaks its pages wherever the layout
16
+ * needs to, and a rule at every one of them would litter the text. `'rule'`
17
+ * writes a `---` thematic break instead, which is what a SLIDE DECK wants —
18
+ * the `.pptx` and `.ppt` readers mark each slide boundary with a page break,
19
+ * and it is the only structure a deck has.
20
+ */
21
+ readonly pageBreaks?: 'rule' | 'drop';
22
+ }
23
+ /**
24
+ * Render a {@link FlowDoc} to GitHub-Flavored Markdown (ir-design §7).
25
+ *
26
+ * A flow medium: no pagination, no layout engine and no fonts, so this is a
27
+ * pure, zero-I/O transform. Markdown says far less than the document model
28
+ * does — alignment, indents, colour, font metrics, tab stops and page
29
+ * geometry have no expression at all — so those are dropped and reported as
30
+ * {@link Loss} entries, deduplicated so one recurring omission reports once.
31
+ *
32
+ * @param flow The format-neutral interlayer document tree.
33
+ * @param options Picture handling; see {@link MarkdownWriteOptions}.
34
+ * @returns The encoded Markdown bytes plus the recorded {@link Loss} list.
35
+ */
36
+ export declare function writeMarkdown(flow: FlowDoc, options?: MarkdownWriteOptions): WriteResult;
37
+ /**
38
+ * The flow-medium {@link DocumentWriter} adapter (id `'md'`), wrapping
39
+ * {@link writeMarkdown}, with the set of {@link FEATURES} it renders.
40
+ */
41
+ export declare const markdownWriter: DocumentWriter<FlowDoc>;