@bendyline/squisq-formats

中文文档
2.8.16发布于 2026/11/08MITTypeScriptES Module构建来源已验证近 30 天下载 6718

Document format converters — DOCX, PDF, OOXML import/export

原始使用文档
文档目录19 节
  1. Install
  2. How Conversion Works
  3. Supported Formats
  4. Quick Examples
  5. DOCX
  6. PDF
  7. Diagrams as pictures
  8. HTML
  9. EPUB
  10. PPTX
  11. CSV
  12. XLSX
  13. Container ZIP
  14. Archive import safety
  15. Doc-level Convenience Functions
  16. Programmatic convert()
  17. Subpath Exports
  18. Related Packages
  19. License

@bendyline/squisq-formats

Document format converters for Squisq. Import and export between Squisq's Markdown-based document model and common file formats — DOCX, PDF, HTML, EPUB, PPTX, XLSX, and CSV. All converters run in the browser or Node with no server or native binaries required.

Part of the Squisq monorepo.

npm MIT License

Install

npm install @bendyline/squisq-formats @bendyline/squisq

How Conversion Works

All converters use Squisq's MarkdownDocument AST (from @bendyline/squisq/markdown) as the pivot format:

  • Import parses a file into a MarkdownDocument. The xxxToContainer variants (DOCX, PDF, PPTX) also extract embedded images into a ContentContainer alongside the markdown.
  • Export serializes a MarkdownDocument out to the target format. Each format also has a docToXxx / xxxToDoc wrapper that converts through MarkdownDocument internally.

Conversions preserve document structure and most of the flavor of the source — headings, text formatting, lists, tables, links, images — but they are not lossless round-trips. Per-format fidelity limits are listed below.

All exports that accept a themeId (DOCX, PDF, EPUB, PPTX, plain HTML) apply Squisq theme colors/typography, and fall back to the doc's frontmatter theme (squisq-theme / themeId / theme keys) when the option is omitted.

For a uniform, format-agnostic entry point over every converter, use the format registry and convert() (@bendyline/squisq-formats/registry, also re-exported from the root) — see Programmatic convert() below.

Supported Formats

FormatImportExportThemeSubpath
DOCX (Word)✅✅✅@bendyline/squisq-formats/docx
PDF✅✅✅@bendyline/squisq-formats/pdf
HTML✅✅✅@bendyline/squisq-formats/html
EPUB (e-book)—✅✅@bendyline/squisq-formats/epub
PPTX (PowerPoint)✅✅✅@bendyline/squisq-formats/pptx
XLSX (Excel)✅✅¹—@bendyline/squisq-formats/xlsx
CSV✅✅—@bendyline/squisq-formats/csv
Container ZIP✅✅—@bendyline/squisq-formats/container

¹ XLSX export is tables-only (v1.5) — see the XLSX section.

Quick Examples

DOCX

import {
  markdownDocToDocx,
  docxToMarkdownDoc,
  docxToContainer,
} from '@bendyline/squisq-formats/docx';

// Export: MarkdownDocument → DOCX (ArrayBuffer)
const docxBytes = await markdownDocToDocx(markdownDoc, { title: 'Report', themeId: 'documentary' });

// Import: DOCX (ArrayBuffer | Blob) → MarkdownDocument
const imported = await docxToMarkdownDoc(docxBuffer, { extractImages: true });

// Import with assets: DOCX → ContentContainer (markdown + images/ files)
const container = await docxToContainer(docxBuffer);

Fidelity: import covers headings, paragraphs, inline formatting (bold, italic, strikethrough, inline code), hyperlinks, lists, tables, blockquotes, code blocks, footnotes, and embedded images (opt-in via extractImages; always on for docxToContainer). Export embeds images only when provided via options.images; otherwise they appear as placeholder text.

PDF

import {
  markdownDocToPdf,
  pdfToMarkdownDoc,
  pdfToContainer,
  configurePdfWorker,
} from '@bendyline/squisq-formats/pdf';

// Configure the PDF.js worker (needed for import in some environments)
configurePdfWorker('/pdf.worker.min.mjs');

// Export: MarkdownDocument → PDF (ArrayBuffer)
const pdfBytes = await markdownDocToPdf(markdownDoc, { pageSize: 'a4' });

// Import: PDF → MarkdownDocument (heuristic structure detection)
const imported = await pdfToMarkdownDoc(pdfBuffer);

// Import with assets: PDF → ContentContainer (markdown + extracted images)
const container = await pdfToContainer(pdfBuffer);

Fidelity: export uses pdf-lib's standard 14 fonts (themeId affects colors only). Text they cannot encode — Greek, Cyrillic, Vietnamese, arrows, math, box drawing, symbols — is drawn with embedded Unicode fonts: the bundled DejaVu Sans faces, plus monochrome Noto Emoji, Noto Sans SC and Noto Sans KR for emoji, Chinese, Japanese and Korean, each loaded and subset only for a document that needs it (unicodeFonts supplies your own faces, or false turns this off). Scripts none of these cover, such as Thai, are still replaced, and reported once through onWarning. Chinese and Japanese lines break between characters. Lines break at a <br> (the only line break a table cell can hold), table columns are sized to their content, and a word too long for its line or cell breaks between characters. Pages stay white for printing. Theme text, heading, and link colors are retained when they meet 4.5:1 contrast against both white paper and the shaded table header; otherwise the exporter uses its default dark ink for that role. This also applies to inherited and custom themes. Pass images (keyed by the markdown URL, as for DOCX) to draw PNG and JPEG images that stand alone in a paragraph; other images print as an [Image: alt] note. PDF has no semantic structure, so import is heuristic and best-effort: headings are detected by font size, and tables / code blocks / blockquotes / links via the detectTables / detectCodeBlocks / detectBlockquotes / detectLinks options (all default true). pdfToMarkdownDoc is text-only; pdfToContainer also extracts embedded XObject and inline raster images in browsers and Node, placed by page — each image is inserted after the last content block from its page (image-only pages fall back to the nearest preceding page with content, else the document end). Placement is page-level only; masks, vector artwork, and images that PDF.js cannot expose as decoded pixels are not emitted.

Diagrams as pictures

DOCX, EPUB, PDF and PPTX cannot draw Mermaid or Squisq diagrams, so on their own they export diagram source as text. rasterizeDiagrams finds every diagram — Mermaid fences, ASCII diagram/timeline/tree fences, and {[drawing]}/{[layout]}/{[diagram]} heading blocks — asks a renderer for a PNG of each, and returns the document with each diagram replaced by an image plus the bytes for the exporters' images option. Drawing needs a browser; @bendyline/squisq-react/diagram-pictures provides the renderer.

import { rasterizeDiagrams } from '@bendyline/squisq-formats/diagrams';
import { createDiagramPictureRenderer } from '@bendyline/squisq-react/diagram-pictures';

const pictured = await rasterizeDiagrams(markdownDoc, createDiagramPictureRenderer());
const docx = await markdownDocToDocx(pictured.markdownDoc, { images: pictured.images });
const pdf = await markdownDocToPdf(pictured.markdownDoc, { images: pictured.images });

// PPTX keeps drawings, timelines and ASCII diagrams as native shapes;
// only Mermaid needs a picture.
const pptx = await docToPptx(doc, { diagramImages: pictured.mermaid });

A diagram the renderer cannot draw stays as source; the export never fails on it. Image entries carry their display size (width/height in CSS pixels), so a picture rendered at 2× prints at 1×.

HTML

import { PLAYER_BUNDLE } from '@bendyline/squisq-react/standalone-source';
import {
  docToHtml,
  docToHtmlZip,
  markdownDocToPlainHtml,
  htmlToMarkdownDoc,
} from '@bendyline/squisq-formats/html';

// Interactive player export: Doc → single self-contained HTML string
// (player inlined, images as data URIs, timer-based playback — no audio)
const html = docToHtml(doc, { playerScript: PLAYER_BUNDLE, images });

// Interactive player export: Doc → ZIP Blob (external assets + optional audio)
const zipBlob = await docToHtmlZip(doc, { playerScript: PLAYER_BUNDLE, images, audio });

// Static export: MarkdownDocument → semantic HTML page (no player)
const page = markdownDocToPlainHtml(markdownDoc, { themeId: 'warm-earth' });

// Import: HTML → MarkdownDocument (sanitized by default)
const imported = await htmlToMarkdownDoc(htmlString);

To place a document inside your own page — a site header, navigation, footer, and stylesheet — write the page as an HTML template with standard <slot> elements and fill it:

import { applyHtmlPageTemplate, markdownDocToPlainHtmlParts } from '@bendyline/squisq-formats/html';

// template: <title><slot name="title">Post</slot> · My Blog</title> … <h1><slot name="title"></slot></h1> <slot></slot>
const parts = markdownDocToPlainHtmlParts(markdownDoc);
const page = applyHtmlPageTemplate(
  template,
  {
    bodyHtml: parts.bodyHtml,
    headHtml: parts.featureHeadHtml,
    title: 'Hello',
    values: { date: '2026-10-28' },
  },
  { templatePath: '_templates/post.html', outputPath: 'posts/hello.html' },
);

The unnamed <slot> receives the body (between <!-- squisq:content --> markers, which htmlToMarkdownDoc honors on re-import), named slots receive text values or keep their fallback children, relative URLs are rebased from the template's folder to the page's, and everything else in the template is copied byte for byte.

The subpath also exports markdownDocsToPlainHtmlBundle / markdownDocsToHtmlBundle (recursive multi-doc ZIP bundles with .md links rewritten to .html), htmlToMarkdownDocSync / htmlToMarkdown, and helpers (collectImagePaths, collectLinkRefs, inferMimeType).

EPUB

import { markdownDocToEpub } from '@bendyline/squisq-formats/epub';

// Export: MarkdownDocument → EPUB 3 (ArrayBuffer). No import path.
const epubBytes = await markdownDocToEpub(markdownDoc, {
  title: 'My Book',
  author: 'Jane Doe',
  images, // Map<string, ArrayBuffer> — embedded when provided
  coverImage, // optional JPEG/PNG
});

Chapters split at H1/H2 boundaries. Providing audio + audioSegments generates EPUB 3 Media Overlays (SMIL) for synchronized narration.

PPTX

import {
  docToPptx,
  markdownDocToPptx,
  pptxToMarkdownDoc,
  pptxToContainer,
} from '@bendyline/squisq-formats/pptx';

// Export: MarkdownDocument → PPTX (ArrayBuffer)
// slideBreak: 'h1' | 'h2' (default — H1 and H2 both break) | 'heading'
const pptxBytes = await markdownDocToPptx(markdownDoc, { slideBreak: 'h2', images });

// Export: player-ready Doc -> 16:9 PPTX using the slideshow's canonical
// block expansion and materialized visual layers (managed cover included by default)
const visualDeck = await docToPptx(slideshowDoc, { images });

// Import: PPTX → MarkdownDocument (add { extractImages: true } to reference slide images)
const imported = await pptxToMarkdownDoc(pptxBuffer);

// Import with assets: PPTX → ContentContainer (markdown + extracted slide images)
const container = await pptxToContainer(pptxBuffer);

Fidelity: markdownDocToPptx preserves inline formatting as DrawingML runs. docToPptx treats a template-backed/player-ready Doc as a visual deck: it uses the same renderable-block flattening, timing expansion, managed-cover policy, theme, transitions, and materialized layer stack as slideshow view, so the PPTX has the same slide count and a matching 16:9 canvas. Text, shapes, tables, paths, and supplied images become editable native PowerPoint objects; browser-only map/video/Mermaid layers degrade to a static image when supplied or a labeled text fallback. Import reads slide order from ppt/presentation.xml and converts each slide's title (→ H2), body text (→ bullet list), and tables. Slide-image extraction (v1.5): import can now extract slide-level <p:pic> bitmaps into images/ — pptxToContainer returns a container with those files and forces extractImages: true, while pptxToMarkdownDoc leaves it off by default. Honest limit: only slide-level <p:pic> bitmaps are extracted — layout/master images, charts, SmartArt, and picture-fills are not.

CSV

import { csvToMarkdownDoc, markdownDocToCsv, parseCsv } from '@bendyline/squisq-formats/csv';

// Import: CSV (string | ArrayBuffer | Blob) → single-table MarkdownDocument
const tableDoc = await csvToMarkdownDoc(csvText, { delimiter: ',', hasHeader: true });

// Export: serializes one table node to CSV text (the first by default)
const csv = markdownDocToCsv(markdownDoc);
// Pick another table in a multi-table document:
const second = markdownDocToCsv(markdownDoc, { tableIndex: 1 });

Self-contained RFC-4180 parser/serializer (not OOXML). Cell formatting is flattened to plain text on export. Export emits a single table — the first, or tableIndex (zero-based). An out-of-range tableIndex throws; a table-less document exports to an empty string.

XLSX

import { xlsxToMarkdownDoc, markdownDocToXlsx } from '@bendyline/squisq-formats/xlsx';

// Import: XLSX → MarkdownDocument (one table per sheet; or pick one via `sheet`)
const imported = await xlsxToMarkdownDoc(xlsxBuffer, { sheet: 0 });

// Export: MarkdownDocument → XLSX (ArrayBuffer) — one worksheet per markdown table
const xlsxBytes = await markdownDocToXlsx(markdownDoc);

Export is implemented (v1.5) with tables-only fidelity: every markdown table becomes one worksheet, named after the nearest preceding heading (auto-named Sheet1, Sheet2, … otherwise). All non-table content is dropped. Numeric-looking cells are written as numbers; everything else as inline strings. A document with no tables yields a single empty (but valid) sheet — export never throws. markdownDocToXlsx / docToXlsx now return Promise<ArrayBuffer> (previously threw / returned Blob).

Container ZIP

import {
  containerToZip,
  zipToContainer,
  ZipSafetyError,
} from '@bendyline/squisq-formats/container';

// ContentContainer → ZIP Blob (paths preserved)
const zipBlob = await containerToZip(container);

// ZIP → MemoryContentContainer (directories skipped, path traversal rejected)
const container2 = await zipToContainer(zipData, {
  maxUncompressedBytes: 128 * 1024 * 1024,
});

Archive import safety

Every production ZIP read—container ZIP/DBK sniffing and OOXML-backed DOCX, PPTX, and XLSX import—uses one JSZip-backed bounded read session. The session checks paths and central-directory metadata before inflation, then consumes JSZip's incremental member stream while sharing an actual emitted-byte budget across all members. Repeated OOXML part reads are cached and charged once; concurrent streams are all paused on the first failure. [Content_Types].xml is capped at 1 MiB and each relationship part at 4 MiB before DOM parsing. OoxmlPackage is opaque: create it with openPackage() and access parts through the bounded getters rather than reaching into a raw JSZip archive.

ZipSafetyLimits is accepted by zipToContainer and inherited by the OOXML import option types:

FieldDefaultMeaning
maxEntries10_000Total central records, including directory records
maxEntryUncompressedBytesmaxUncompressedBytesMaximum emitted bytes retained for one member
maxUncompressedBytes512 * 1024 * 1024Aggregate emitted bytes across the archive
maxCompressionRatio1_000Maximum uncompressed/compressed ratio for one member

Failures are ZipSafetyError instances, exported from the package root and the /container and /ooxml subpaths. They expose code, and where relevant path, limit, actual, and cause. Stable codes are invalid-limit, invalid-archive, unsafe-path, invalid-entry-metadata, too-many-entries, entry-too-large, archive-too-large, compression-ratio-exceeded, size-mismatch, crc-mismatch, and decompression-failed.

JSZip's public pause is cooperative: it prevents the next compressed-input tick, but pako may finish producing chunks already in the current synchronous tick. Once a limit fails, Squisq discards those chunks immediately—they are not retained, CRC-counted, or written to a container.

Doc-level Convenience Functions

Each format also exports Doc-level wrappers that handle the Markdown ↔ Doc conversion internally:

import { docToDocx, docxToDoc } from '@bendyline/squisq-formats/docx';
import { docToPdf, pdfToDoc } from '@bendyline/squisq-formats/pdf';
import { docToPptx, pptxToDoc } from '@bendyline/squisq-formats/pptx';
import { docToEpub } from '@bendyline/squisq-formats/epub';
import { csvToDoc } from '@bendyline/squisq-formats/csv';
import { xlsxToDoc, docToXlsx } from '@bendyline/squisq-formats/xlsx';

Programmatic convert()

@bendyline/squisq-formats/registry (also re-exported from the package root) is a uniform, format-agnostic front door over every converter. Each format is a FormatDefinition that knows how to import raw bytes into the MarkdownDocument pivot and/or export back out; convert() normalizes any source into a Doc, optionally applies a theme/transform, and hands off to the target exporter. Every result comes back as the same ConversionResult (bytes + mimeType + suggestedFilename + warnings). Converter modules load lazily, so importing the registry never eagerly bundles a heavy converter.

import { convert } from '@bendyline/squisq-formats/registry';

const result = await convert(
  { kind: 'markdown', markdown: '# Hello', baseName: 'greeting' },
  'docx',
);
// result.bytes, result.mimeType, result.suggestedFilename === 'greeting.docx'

ConvertOptions also accepts themeRegistry and transformRegistry for caller-owned custom definitions. transformStyle may be either a style id or a call-scoped TransformStyleConfig; these options never mutate process-global state.

Sources are { kind: 'bytes' | 'markdown' | 'doc', … }; byte sources are format-sniffed by magic bytes + extension (pass from to skip). Failures throw a structured ConversionError with a stable code (unknown-format, unsupported-input, unsupported-output, invalid-input, missing-dependency, conversion-failed) plus a hint.

Player-embedding HTML export (html / htmlzip) needs the standalone player bundle. Pass resolvePlayerScript, or you'll get a missing-dependency error:

const html = await convert({ kind: 'markdown', markdown: src }, 'html', {
  resolvePlayerScript: () =>
    import('@bendyline/squisq-react/standalone-source').then((m) => m.PLAYER_BUNDLE),
});

Also exported: createRegistry / defaultRegistry / defaultFormats, BUILTIN_FORMAT_IDS, ConversionError, and the ConvertSource / ConvertOptions / ConversionResult / FormatDefinition / FormatRegistry types.

Subpath Exports

SubpathContents
@bendyline/squisq-formats/docxDOCX import/export (+ docxToContainer)
@bendyline/squisq-formats/pdfPDF import/export (+ pdfToContainer, configurePdfWorker)
@bendyline/squisq-formats/htmlPlayer HTML export, static plain-HTML export + bundles, HTML import
@bendyline/squisq-formats/html-prepareprepareMarkdownDocForPlainHtml: data tables and pictures for plain-HTML pages
@bendyline/squisq-formats/html-iconsplainHtmlIcons: inline icons as SVG outlines for plain-HTML pages
@bendyline/squisq-formats/epubEPUB 3 e-book export (with optional Media Overlays)
@bendyline/squisq-formats/pptxPPTX export + import (text/lists/tables + slide images)
@bendyline/squisq-formats/xlsxXLSX import + tables-only export
@bendyline/squisq-formats/csvCSV import/export (RFC 4180)
@bendyline/squisq-formats/ooxmlShared OOXML infrastructure (package reader/writer, XML utilities, namespaces)
@bendyline/squisq-formats/diagramsrasterizeDiagrams / findDiagrams: diagrams as pictures for document exports
@bendyline/squisq-formats/containerContentContainer ↔ ZIP serialization
@bendyline/squisq-formats/registryFormat registry + programmatic convert() (also re-exported from the root)

The package root re-exports the common converters; ./container, the plain-HTML/bundle functions, docxToContainer, pdfToContainer, PdfPageSize, and the image utilities are subpath-only.

See the full API Reference for every signature and options interface.

PackageDescription
@bendyline/squisqHeadless core — schemas, templates, spatial, markdown, storage
@bendyline/squisq-reactReact components for rendering docs (+ PLAYER_BUNDLE)
@bendyline/squisq-clisquisq CLI — batch conversion + MP4 rendering
@bendyline/squisq-editor-reactReact editor with raw/WYSIWYG/preview modes

License

MIT

版本与发布记录

@bendyline/squisq-formats 共发布 84 个版本,首次发布于 2026/03/16;过去 12 个月发布了 84 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
2.8.16latest2026/11/08今天
2.8.152026/11/07今天
2.8.142026/11/052 天前
2.8.132026/11/053 天前
2.8.122026/11/017 天前
2.8.112026/10/308 天前

关于 @bendyline/squisq-formats 的常见问题

@bendyline/squisq-formats 是什么?

Document format converters — DOCX, PDF, OOXML import/export

如何安装 @bendyline/squisq-formats?

在项目目录运行 npm install @bendyline/squisq-formats,或使用 pnpm add / yarn add / bun add。

@bendyline/squisq-formats 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@bendyline/squisq-formats 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@bendyline/squisq-formats 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js ^22.22.2 || ^24.15.0 || >=26.0.0。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@bendyline/squisq-formats 可以商用吗?

@bendyline/squisq-formats 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@bendyline/squisq-formats 的下载量有多少?

根据 npm 官方下载统计,@bendyline/squisq-formats 在 2026/10/08 – 2026/11/06 这 30 天内被下载 6,718 次,日均约 224 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@bendyline/squisq-formats 还在维护吗?

最新版本 2.8.16 发布于 2026/11/08(今天);至今共发布 84 个版本,其中过去 12 个月发布了 84 个;首次发布于 2026/03/16。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。