Release Office Tools 0.2.0
Verify / verify (push) Canceled after 0s

This commit is contained in:
2026-09-02 07:32:31 +02:00
parent a5524e27e3
commit 08b8f84f7b
50 changed files with 2196 additions and 165 deletions
+203
View File
@@ -0,0 +1,203 @@
import {
stableStringify,
stringifyCsv,
triggerBlobDownload,
} from "@add-ideas/toolbox-helpers";
import type {
OdfDocument,
OdfPresentationShape,
OdfSpreadsheetSheet,
OdfTextBlock,
} from "./odf";
const MAX_EXPORT_ROWS = 50_000;
const MAX_EXPORT_CELLS = 250_000;
const MAX_EXPORT_TEXT = 5_000_000;
export function downloadExactCopy(file: File): void {
triggerBlobDownload(file, file.name || "office-source.bin");
}
export function downloadOdfSemanticExport(
document: OdfDocument,
sourceName: string,
activeSheetIndex = 0,
): void {
const base = withoutExtension(sourceName) || "office-export";
if (document.format === "ods") {
const sheet = document.sheets[activeSheetIndex];
if (!sheet) throw new Error("The selected workbook has no active sheet.");
const csv = sheetToCsv(sheet);
triggerBlobDownload(
new Blob(["\uFEFF", csv], { type: "text/csv;charset=utf-8" }),
`${base}-${safePart(sheet.name)}.csv`,
);
return;
}
const text =
document.format === "odt"
? [
blocksToText(document.blocks),
...document.notes.map((note) => blocksToText(note.blocks)),
...document.annotations.map((comment) =>
blocksToText(comment.blocks),
),
]
.filter(Boolean)
.join("\n\n")
: presentationToText(document.slides);
if (text.length > MAX_EXPORT_TEXT)
throw new Error(
"The semantic text export exceeds the 5,000,000-character limit.",
);
triggerBlobDownload(
new Blob([text], { type: "text/plain;charset=utf-8" }),
`${base}-${document.format === "odt" ? "text" : "outline"}.txt`,
);
}
export function downloadOdfModel(
document: OdfDocument,
sourceName: string,
): void {
const { assets, ...model } = document;
const source = stableStringify(
{
schema: "de.add-ideas.office-tools.odf-inspection.v1",
sourceName,
...model,
assets: assets.map((asset) => ({
id: asset.id,
path: asset.path,
mediaType: asset.mediaType,
byteLength: asset.byteLength,
})),
},
2,
{ maxDepth: 160, maxNodes: 500_000, maxTextChars: MAX_EXPORT_TEXT },
);
triggerBlobDownload(
new Blob([source, "\n"], { type: "application/json;charset=utf-8" }),
`${withoutExtension(sourceName) || "office"}-inspection.json`,
);
}
export function sheetToCsv(sheet: OdfSpreadsheetSheet): string {
if (sheet.expandedRowCount > MAX_EXPORT_ROWS)
throw new Error(
`This sheet has ${sheet.expandedRowCount.toLocaleString()} rows; CSV export is limited to ${MAX_EXPORT_ROWS.toLocaleString()} rows.`,
);
const rows: string[][] = [];
let cellCount = 0;
for (const sourceRow of sheet.rows) {
for (let repeat = 0; repeat < sourceRow.rowRepeat; repeat += 1) {
const row: string[] = [];
for (const cell of sourceRow.cells) {
for (let offset = 0; offset < cell.columnRepeat; offset += 1) {
const column = cell.column + offset;
if (column >= 256) break;
row[column] = cell.kind === "covered" ? "" : cellDisplay(cell);
}
}
while (row.at(-1) === "") row.pop();
cellCount += row.length;
if (cellCount > MAX_EXPORT_CELLS)
throw new Error(
`CSV export is limited to ${MAX_EXPORT_CELLS.toLocaleString()} populated-range cells.`,
);
rows.push(row);
}
}
return stringifyCsv(rows, {
maxRows: MAX_EXPORT_ROWS,
maxColumns: 256,
maxFieldChars: 1_000_000,
});
}
function cellDisplay(
cell: OdfSpreadsheetSheet["rows"][number]["cells"][number],
): string {
if (cell.display) return cell.display;
return cell.value === undefined ? "" : String(cell.value);
}
function presentationToText(
slides: Extract<OdfDocument, { format: "odp" }>["slides"],
): string {
return slides
.map((slide, index) => {
const content = slide.shapes
.flatMap((shape) => shapeText(shape))
.filter(Boolean);
const notes = blocksToText(slide.notes).trim();
return [
`Slide ${index + 1}: ${slide.name}`,
...content,
...(notes ? ["Speaker notes:", notes] : []),
].join("\n");
})
.join("\n\n");
}
function shapeText(shape: OdfPresentationShape): string[] {
const content = blocksToText(shape.blocks).trim();
const table = shape.table ? blocksToText([shape.table]).trim() : "";
return [
content,
table,
...shape.children.flatMap((child) => shapeText(child)),
].filter(Boolean);
}
function blocksToText(blocks: readonly OdfTextBlock[], depth = 0): string {
if (depth > 128)
throw new Error("Document nesting exceeds the export limit.");
return blocks
.map((block) => {
if (block.kind === "paragraph" || block.kind === "heading")
return block.text;
if (block.kind === "image")
return `[Image: ${block.alt ?? block.title ?? block.path}]`;
if (block.kind === "list")
return block.items
.map(
(item) =>
`${" ".repeat(depth)}- ${blocksToText(item.blocks, depth + 1).trim()}`,
)
.join("\n");
return block.rows
.flatMap((row) =>
Array.from({ length: row.rowRepeat }, () =>
row.cells
.flatMap((cell) =>
Array.from({ length: cell.columnRepeat }, () =>
cell.covered
? ""
: blocksToText(cell.blocks, depth + 1).replaceAll(
"\n",
" ",
),
),
)
.join("\t"),
),
)
.join("\n");
})
.filter(Boolean)
.join("\n");
}
function withoutExtension(name: string): string {
return name.replace(/\.[^.]+$/u, "");
}
function safePart(value: string): string {
return (
value
.trim()
.replace(/[^\p{L}\p{N}._-]+/gu, "-")
.slice(0, 80) || "sheet"
);
}
+152 -20
View File
@@ -2,36 +2,97 @@ export const MAX_SOURCE_BYTES = 100 * 1024 * 1024;
export const OFFICE_ACCEPT = [
".docx",
".docm",
".dotx",
".dotm",
".odt",
".xlsx",
".xlsm",
".xltx",
".xltm",
".ods",
".pptx",
".pptm",
".potx",
".potm",
".ppsx",
".ppsm",
".odp",
// Accepted by the picker so users get a precise compatibility diagnostic.
".doc",
".xls",
".ppt",
".rtf",
].join(",");
export type OfficeFormat = "docx" | "odt" | "xlsx" | "ods" | "pptx" | "odp";
export type OfficeFamily = "document" | "spreadsheet" | "presentation";
const FORMAT_BY_EXTENSION: Record<string, OfficeFormat> = {
docx: "docx",
odt: "odt",
xlsx: "xlsx",
ods: "ods",
pptx: "pptx",
odp: "odp",
};
const FORMAT_BY_EXTENSION: Readonly<Record<string, OfficeFormat>> =
Object.freeze({
docx: "docx",
docm: "docx",
dotx: "docx",
dotm: "docx",
odt: "odt",
xlsx: "xlsx",
xlsm: "xlsx",
xltx: "xlsx",
xltm: "xlsx",
ods: "ods",
pptx: "pptx",
pptm: "pptx",
potx: "pptx",
potm: "pptx",
ppsx: "pptx",
ppsm: "pptx",
odp: "odp",
});
const LEGACY_EXTENSIONS = new Set(["doc", "dot", "xls", "xlt", "ppt", "pps"]);
const LEGACY_EXTENSIONS = new Set([
"doc",
"dot",
"wps",
"xls",
"xlt",
"xla",
"ppt",
"pps",
"pot",
"rtf",
"sxw",
"sxc",
"sxi",
]);
const FLAT_ODF_EXTENSIONS = new Set(["fodt", "fods", "fodp"]);
const VARIANT_NOTICE: Readonly<Record<string, string>> = Object.freeze({
docm: "VBA content is kept inert; the document view uses the OOXML content only.",
dotm: "VBA content is kept inert; the template is opened as a read-only document.",
dotx: "This OOXML template is opened as a read-only document.",
xlsm: "VBA content is kept inert; cached workbook content is shown without recalculation.",
xltm: "VBA content is kept inert; the template is opened as a read-only workbook.",
xltx: "This OOXML template is opened as a read-only workbook.",
pptm: "VBA content is kept inert; media and active content remain disabled.",
potm: "VBA content is kept inert; the template is opened as a read-only presentation.",
potx: "This OOXML template is opened as a read-only presentation.",
ppsm: "VBA content is kept inert; the slide show is opened as a read-only presentation.",
ppsx: "This OOXML slide show is opened as a read-only presentation.",
});
export type OfficeFileErrorCode =
| "empty-file"
| "file-too-large"
| "legacy-format"
| "flat-odf"
| "invalid-container"
| "unsupported-format";
export class OfficeFileError extends Error {
readonly code:
"empty-file" | "file-too-large" | "legacy-format" | "unsupported-format";
readonly code: OfficeFileErrorCode;
constructor(
message: string,
code:
"empty-file" | "file-too-large" | "legacy-format" | "unsupported-format",
) {
constructor(message: string, code: OfficeFileErrorCode) {
super(message);
this.name = "OfficeFileError";
this.code = code;
@@ -51,29 +112,100 @@ export function detectOfficeFormat(
}
if (file.size > MAX_SOURCE_BYTES) {
throw new OfficeFileError(
`This first viewer slice accepts files up to ${formatBytes(MAX_SOURCE_BYTES)}.`,
`This viewer accepts files up to ${formatBytes(MAX_SOURCE_BYTES)}.`,
"file-too-large",
);
}
const extension = extensionOf(file.name);
if (LEGACY_EXTENSIONS.has(extension)) {
throw new OfficeFileError(legacyMessage(extension), "legacy-format");
}
if (FLAT_ODF_EXTENSIONS.has(extension)) {
throw new OfficeFileError(
`.${extension} is a legacy binary Office format. Convert it to a modern OOXML or OpenDocument file before opening it here.`,
"legacy-format",
`.${extension} is a flat XML OpenDocument file. This package reader currently supports zipped ODT, ODS and ODP; save it in the corresponding packaged format first.`,
"flat-odf",
);
}
const format = FORMAT_BY_EXTENSION[extension];
if (!format) {
throw new OfficeFileError(
"Choose a DOCX, ODT, XLSX, ODS, PPTX, or ODP file.",
"Choose a supported OOXML or OpenDocument package. Legacy DOC/XLS/PPT/RTF and flat OpenDocument XML are diagnosed but not rendered.",
"unsupported-format",
);
}
return format;
}
export interface OfficeFileInspection {
format: OfficeFormat;
extension: string;
notice?: string;
}
/**
* Perform a small bounded content sniff before a package parser receives the
* file. It catches renamed OLE/RTF files and ordinary non-package data while
* reading no more than eight bytes.
*/
export async function inspectOfficeFile(
file: File,
): Promise<OfficeFileInspection> {
const format = detectOfficeFormat(file),
extension = extensionOf(file.name),
prefix = new Uint8Array(await file.slice(0, 8).arrayBuffer());
validateOfficePackagePrefix(prefix, extension);
return { format, extension, notice: VARIANT_NOTICE[extension] };
}
export function validateOfficePackagePrefix(
prefix: Uint8Array,
extension: string,
): void {
if (hasPrefix(prefix, [0xd0, 0xcf, 0x11, 0xe0, 0xa1, 0xb1, 0x1a, 0xe1])) {
throw new OfficeFileError(
`The bytes use the legacy OLE Compound File container, even though the name ends in .${extension}. Binary DOC, XLS and PPT parsing is not available; convert the original file to OOXML or OpenDocument first.`,
"legacy-format",
);
}
const ascii = new TextDecoder("ascii").decode(prefix).toLowerCase();
if (ascii.startsWith("{\\rtf")) {
throw new OfficeFileError(
"The bytes are Rich Text Format (RTF). RTF is identified explicitly but is not yet rendered; save or convert it as DOCX or ODT first.",
"legacy-format",
);
}
if (!isZipPrefix(prefix)) {
throw new OfficeFileError(
`.${extension} must be a ZIP-based office package, but its file signature is different. The file may be renamed, truncated or use a legacy format.`,
"invalid-container",
);
}
}
function isZipPrefix(bytes: Uint8Array): boolean {
return (
bytes[0] === 0x50 &&
bytes[1] === 0x4b &&
((bytes[2] === 0x03 && bytes[3] === 0x04) ||
(bytes[2] === 0x05 && bytes[3] === 0x06) ||
(bytes[2] === 0x07 && bytes[3] === 0x08))
);
}
function hasPrefix(bytes: Uint8Array, signature: readonly number[]): boolean {
return signature.every((value, index) => bytes[index] === value);
}
function legacyMessage(extension: string): string {
if (extension === "rtf")
return "RTF is a legacy text interchange format, not an OOXML package. Save or convert it as DOCX or ODT first.";
if (["sxw", "sxc", "sxi"].includes(extension))
return `.${extension} is an older OpenOffice XML package. Save it as ODT, ODS or ODP before opening it here.`;
return `.${extension} uses a legacy compound-binary Office format. Its macros and embedded objects require a distinct bounded parser; convert it to a modern OOXML or OpenDocument file first.`;
}
export function familyForFormat(format: OfficeFormat): OfficeFamily {
if (format === "docx" || format === "odt") return "document";
if (format === "xlsx" || format === "ods") return "spreadsheet";
+2
View File
@@ -60,6 +60,8 @@ export function parsePreparedOdp(
styles: prepared.styles,
assets: prepared.context.assets,
warnings: prepared.context.warnings,
pageWidth: prepared.pageWidth,
pageHeight: prepared.pageHeight,
slides,
};
}
+57
View File
@@ -3,6 +3,7 @@ import { parseParagraph, parseTextBlocks } from "./text";
import type {
OdfParseOptions,
OdfSpreadsheetCell,
OdfSpreadsheetColumn,
OdfSpreadsheetDocument,
OdfSpreadsheetRow,
OdfSpreadsheetSheet,
@@ -61,6 +62,8 @@ export function parsePreparedOds(
styles: prepared.styles,
assets: prepared.context.assets,
warnings: prepared.context.warnings,
pageWidth: prepared.pageWidth,
pageHeight: prepared.pageHeight,
sheets,
};
}
@@ -70,6 +73,34 @@ function parseSheet(
sheetIndex: number,
context: ReturnType<typeof prepareOdfPackage>["context"],
): OdfSpreadsheetSheet {
const columnElements: XmlElement[] = [];
collectSheetColumns(table, columnElements);
const columns: OdfSpreadsheetColumn[] = [];
let columnIndex = 0;
for (const column of columnElements) {
const repeat = boundedRepeat(
attribute(column, NS.table, "number-columns-repeated"),
context.limits,
"Repeated spreadsheet column count",
);
columns.push({
index: columnIndex,
repeat,
styleName: attribute(column, NS.table, "style-name"),
defaultCellStyleName: attribute(
column,
NS.table,
"default-cell-style-name",
),
visibility: visibility(attribute(column, NS.table, "visibility")),
});
columnIndex += repeat;
if (!Number.isSafeInteger(columnIndex))
throw new OdfParseError(
"limit-exceeded",
"Expanded spreadsheet column count is too large",
);
}
const rowElements: XmlElement[] = [];
collectSheetRows(table, rowElements);
let rows: OdfSpreadsheetRow[] = [];
@@ -112,6 +143,11 @@ function parseSheet(
index: rowIndex,
rowRepeat,
styleName: attribute(rowElement, NS.table, "style-name"),
defaultCellStyleName: attribute(
rowElement,
NS.table,
"default-cell-style-name",
),
cells,
});
rowIndex += rowRepeat;
@@ -146,12 +182,20 @@ function parseSheet(
name: attribute(table, NS.table, "name") ?? `Sheet ${sheetIndex + 1}`,
styleName: attribute(table, NS.table, "style-name"),
protected: attribute(table, NS.table, "protected") === "true",
visibility: visibility(attribute(table, NS.table, "visibility")),
columns,
rows,
expandedRowCount,
expandedCellCount,
};
}
function visibility(
value: string | undefined,
): "visible" | "collapse" | "filter" {
return value === "collapse" || value === "filter" ? value : "visible";
}
function isMeaningfulCell(cell: OdfSpreadsheetCell): boolean {
return (
cell.kind === "covered" ||
@@ -247,3 +291,16 @@ function collectSheetRows(parent: XmlElement, result: XmlElement[]): void {
}
}
}
function collectSheetColumns(parent: XmlElement, result: XmlElement[]): void {
for (const child of childElements(parent)) {
if (child.namespaceURI !== NS.table) continue;
if (child.localName === "table-column") result.push(child);
else if (
child.localName === "table-header-columns" ||
child.localName === "table-column-group" ||
child.localName === "table-columns"
)
collectSheetColumns(child, result);
}
}
+4 -1
View File
@@ -1,5 +1,5 @@
import { prepareOdfPackage, type PreparedOdfPackage } from "./package";
import { collectNotes, parseTextBlocks } from "./text";
import { collectAnnotations, collectNotes, parseTextBlocks } from "./text";
import type { OdfParseOptions, OdfTextDocument } from "./types";
import { OdfParseError } from "./types";
import { NS, firstChildNamed } from "./xml";
@@ -44,7 +44,10 @@ export function parsePreparedOdt(
styles: prepared.styles,
assets: prepared.context.assets,
warnings: prepared.context.warnings,
pageWidth: prepared.pageWidth,
pageHeight: prepared.pageHeight,
blocks,
notes: collectNotes(blocks),
annotations: collectAnnotations(blocks),
};
}
+57 -1
View File
@@ -2,6 +2,7 @@ import { OdfReadContext } from "./context";
import { resolveLimits } from "./limits";
import type {
OdfFormat,
OdfLength,
OdfMetadata,
OdfParseOptions,
OdfStyle,
@@ -13,6 +14,7 @@ import {
childElements,
descendantsNamed,
parseXmlPart,
parseLength,
type XmlDocument,
type XmlElement,
} from "./xml";
@@ -32,6 +34,8 @@ export interface PreparedOdfPackage {
context: OdfReadContext;
metadata: OdfMetadata;
styles: OdfStyle[];
pageWidth?: OdfLength;
pageHeight?: OdfLength;
}
export function prepareOdfPackage(
@@ -88,6 +92,24 @@ export function prepareOdfPackage(
`${omittedObjects} embedded ${omittedObjects === 1 ? "object was" : "objects were"} kept inert and omitted`,
);
}
const trackedChanges = descendantsNamed(
content.documentElement!,
NS.text,
"tracked-changes",
).length;
if (trackedChanges > 0)
context.warnings.push(
"Tracked-change metadata is kept inert; the semantic view shows the stored document text without an accept/reject workflow",
);
const forms = descendantsNamed(
content.documentElement!,
NS.office,
"forms",
).length;
if (forms > 0)
context.warnings.push(
"Interactive form controls are kept inert and omitted from the semantic view",
);
const metaBytes = archive.entries.get("meta.xml");
const metadata = metaBytes
? parseMetadata(parseXmlPart(metaBytes, "meta.xml", limits), context)
@@ -97,7 +119,8 @@ export function prepareOdfPackage(
if (stylesBytes)
styleDocuments.unshift(parseXmlPart(stylesBytes, "styles.xml", limits));
const styles = parseStyles(styleDocuments);
return { format, mediaType, content, context, metadata, styles };
const pageSize = parsePageSize(styleDocuments);
return { format, mediaType, content, context, metadata, styles, ...pageSize };
}
function requireEntry(
@@ -241,6 +264,21 @@ function parseMetadata(
function parseStyles(documents: readonly XmlDocument[]): OdfStyle[] {
const styles = new Map<string, OdfStyle>();
for (const document of documents) {
for (const element of descendantsNamed(
document.documentElement!,
NS.style,
"default-style",
)) {
const family = attribute(element, NS.style, "family");
if (!family) continue;
const style: OdfStyle = {
name: `__default__:${family}`,
isDefault: true,
family,
properties: readStyleProperties(element),
};
styles.set(`${family}\0${style.name}`, style);
}
for (const element of descendantsNamed(
document.documentElement!,
NS.style,
@@ -265,6 +303,24 @@ function parseStyles(documents: readonly XmlDocument[]): OdfStyle[] {
return [...styles.values()];
}
function parsePageSize(documents: readonly XmlDocument[]): {
pageWidth?: OdfLength;
pageHeight?: OdfLength;
} {
for (const document of documents) {
const properties = descendantsNamed(
document.documentElement!,
NS.style,
"page-layout-properties",
)[0];
if (!properties) continue;
const pageWidth = parseLength(attribute(properties, NS.fo, "page-width"));
const pageHeight = parseLength(attribute(properties, NS.fo, "page-height"));
if (pageWidth || pageHeight) return { pageWidth, pageHeight };
}
return {};
}
function readStyleProperties(style: XmlElement): Record<string, string> {
const properties: Record<string, string> = {};
for (const child of childElements(style)) {
+62 -1
View File
@@ -2,6 +2,7 @@ import { normalizeResourcePath, OdfReadContext } from "./context";
import type {
OdfHeading,
OdfImage,
OdfAnnotation,
OdfList,
OdfNote,
OdfParagraph,
@@ -29,6 +30,7 @@ interface InlineResult {
runs: OdfTextRun[];
images: OdfImage[];
notes: OdfNote[];
annotations: OdfAnnotation[];
}
export function parseTextBlocks(
@@ -110,7 +112,12 @@ function parseInline(
inheritedStyle?: string,
inheritedLink?: string,
): InlineResult {
const result: InlineResult = { runs: [], images: [], notes: [] };
const result: InlineResult = {
runs: [],
images: [],
notes: [],
annotations: [],
};
const append = (
text: string,
styleName = inheritedStyle,
@@ -162,6 +169,13 @@ function parseInline(
if (note.citation) append(note.citation);
continue;
}
if (
element.namespaceURI === NS.office &&
element.localName === "annotation"
) {
result.annotations.push(parseAnnotation(element, context));
continue;
}
if (element.namespaceURI === NS.draw && element.localName === "frame") {
const image = parseFrameImage(element, context);
if (image) result.images.push(image);
@@ -190,10 +204,35 @@ function parseInline(
}
result.images.push(...nested.images);
result.notes.push(...nested.notes);
result.annotations.push(...nested.annotations);
}
return result;
}
function parseAnnotation(
element: XmlElement,
context: OdfReadContext,
): OdfAnnotation {
const creator = firstChildNamed(
element,
NS.dc,
"creator",
)?.textContent?.trim();
const createdAt = firstChildNamed(
element,
NS.dc,
"date",
)?.textContent?.trim();
if (creator) context.consumeText(creator);
if (createdAt) context.consumeText(createdAt);
return {
id: attribute(element, NS.office, "name"),
creator: creator || undefined,
createdAt: createdAt || undefined,
blocks: parseTextBlocks(element, context),
};
}
function safeHyperlink(href: string | undefined): string | undefined {
if (!href) return undefined;
const value = href.trim();
@@ -370,3 +409,25 @@ export function collectNotes(blocks: readonly OdfTextBlock[]): OdfNote[] {
for (const block of blocks) visit(block);
return result;
}
export function collectAnnotations(
blocks: readonly OdfTextBlock[],
): OdfAnnotation[] {
const result: OdfAnnotation[] = [];
const visit = (block: OdfTextBlock) => {
if (block.kind === "paragraph" || block.kind === "heading") {
result.push(...block.annotations);
for (const annotation of block.annotations)
for (const child of annotation.blocks) visit(child);
} else if (block.kind === "list") {
for (const item of block.items)
for (const child of item.blocks) visit(child);
} else if (block.kind === "table") {
for (const row of block.rows)
for (const cell of row.cells)
for (const child of cell.blocks) visit(child);
}
};
for (const block of blocks) visit(block);
return result;
}
+24
View File
@@ -59,6 +59,7 @@ export interface OdfMetadata {
export interface OdfStyle {
name: string;
isDefault?: boolean;
family?: string;
displayName?: string;
parentName?: string;
@@ -102,6 +103,13 @@ export interface OdfTextRun {
link?: string;
}
export interface OdfAnnotation {
id?: string;
creator?: string;
createdAt?: string;
blocks: OdfTextBlock[];
}
export interface OdfParagraph {
kind: "paragraph";
styleName?: string;
@@ -109,6 +117,7 @@ export interface OdfParagraph {
runs: OdfTextRun[];
images: OdfImage[];
notes: OdfNote[];
annotations: OdfAnnotation[];
}
export interface OdfHeading {
@@ -119,6 +128,7 @@ export interface OdfHeading {
runs: OdfTextRun[];
images: OdfImage[];
notes: OdfNote[];
annotations: OdfAnnotation[];
}
export interface OdfNote {
@@ -182,13 +192,24 @@ export interface OdfSpreadsheetRow {
index: number;
rowRepeat: number;
styleName?: string;
defaultCellStyleName?: string;
cells: OdfSpreadsheetCell[];
}
export interface OdfSpreadsheetColumn {
index: number;
repeat: number;
styleName?: string;
defaultCellStyleName?: string;
visibility: "visible" | "collapse" | "filter";
}
export interface OdfSpreadsheetSheet {
name: string;
styleName?: string;
protected: boolean;
visibility: "visible" | "collapse" | "filter";
columns: OdfSpreadsheetColumn[];
rows: OdfSpreadsheetRow[];
expandedRowCount: number;
expandedCellCount: number;
@@ -239,12 +260,15 @@ interface OdfBaseModel {
styles: OdfStyle[];
assets: OdfAsset[];
warnings: string[];
pageWidth?: OdfLength;
pageHeight?: OdfLength;
}
export interface OdfTextDocument extends OdfBaseModel {
format: "odt";
blocks: OdfTextBlock[];
notes: OdfNote[];
annotations: OdfAnnotation[];
}
export interface OdfSpreadsheetDocument extends OdfBaseModel {
+1
View File
@@ -23,6 +23,7 @@ export const NS = Object.freeze({
meta: "urn:oasis:names:tc:opendocument:xmlns:meta:1.0",
manifest: "urn:oasis:names:tc:opendocument:xmlns:manifest:1.0",
svg: "urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0",
form: "urn:oasis:names:tc:opendocument:xmlns:form:1.0",
} as const);
const XML_DECLARATION_ATTACK = /<!\s*(?:DOCTYPE|ENTITY)|<\?xml-stylesheet/iu;