Docs menu
DocsAPI reference

Documents

Generate real PDF, Word, Excel, PowerPoint, JPG, and PNG files through the OS — and the two content shapes.

Generate real PDF, Word, Excel, PowerPoint, JPG, and PNG files through the OS — no vendored library, no build step, and no window.print() (which is BLOCKED in the sandboxed iframe, since the sandbox grants no allow-modals). The OS bundles the generators (jspdf, xlsx, docx, pptxgenjs) and builds the bytes in the host; you write them to disk with the Files API — or use the one-call save() helper. Capability: documents (declared, never prompts).

// app.json: "capabilities": ["documents", "fileAccess"]

// The simplest PDF — give it text, get back bytes, save them:
const { bytesB64 } = await window.chatoss.documents.generate({
  type: 'pdf',
  content: { title: 'Report', paragraphs: ['First paragraph.', 'Second paragraph.'] }
});
const path = await window.chatoss.files.saveDialog({
  defaultPath: 'report.pdf',
  filters: [{ name: 'PDF', extensions: ['pdf'] }]
});
if (path) await window.chatoss.files.writeFile(path, window.chatoss.documents.base64ToBytes(bytesB64));

// Or the one-call helper (generate + save dialog + write → path | null):
const saved = await window.chatoss.documents.save({
  type: 'xlsx',
  content: { sheets: [{ name: 'Q1', rows: [['Item', 'Qty'], ['Widget', 42]] }] },
  defaultPath: 'q1.xlsx'
});

Supported types

type content notes
pdf / docx { title?, blocks?, paragraphs?, text? } see the two shapes below — blocks keeps formatting, paragraphs/text are plain
xlsx { sheets?: { name?, rows: (string|number|boolean|null)[][] }[] } one+ worksheets of rows
pptx { title?, slides?: { title?, bullets?: string[] }[] } one slide per entry
jpg / png { image: string } image = a data URL or http(s) URL, re-encoded
  • generate() returns { bytesB64, ext, mimeType }. Decode with documents.base64ToBytes(bytesB64) and pass to files.writeFile.
  • save({ type, content, defaultPath? }) does generate + save dialog + write in one call and resolves the chosen path (null if cancelled/denied). It needs BOTH documents and fileAccess.

PDF / DOCX content — two shapes

🔴 Use the one that matches how your content is structured, or the exported file's formatting will not match what your app's editor/preview shows — this is the most common document bug.

Shape 1 — plain text (simplest, no formatting): { paragraphs?: string[], text?: string }. Every paragraph becomes one unstyled line of body text. There is NO bold, italic, headings, or list support in this shape — pass markdown or HTML here and it will be rendered as literal text, NOT formatted. Use this only when your content is genuinely plain text.

content: { title: 'Notes', paragraphs: ['First paragraph.', 'Second paragraph.'] }
// or a single blob split on line breaks (a single "\n" or a blank line each
// end a paragraph — so a <textarea>'s newline-separated lines stay separate):
content: { title: 'Notes', text: 'First paragraph.\n\nSecond paragraph.' }
content: { title: 'Notes', text: 'Line one\nLine two\nLine three' } // → 3 paragraphs

Shape 2 — structured blocks (keeps formatting — use this for any rich content): { blocks: DocumentBlock[] }. This is how headings, inline bold/italic/underline, and bullet/numbered lists survive from your app's editor/preview into the exported file, so "what you see is what you get." blocks takes precedence over paragraphs/text when present.

A DocumentBlock is one of:

  • { type: 'heading', level: 1|2|3, text: string } — a section heading.
  • { type: 'paragraph', runs: TextRun[] } — a paragraph of styled inline runs.
  • { type: 'list', list: { type: 'bullet'|'number', items: (string|TextRun[])[] } } — a list; each item is a plain string or its own array of runs.

A TextRun is either a plain string (unstyled) or { text: string, bold?: true, italic?: true, underline?: true }. Build paragraphs and list items from runs to carry inline formatting.

content: {
  title: 'Project Brief',
  blocks: [
    { type: 'heading', level: 1, text: 'Overview' },
    { type: 'paragraph', runs: [
        { text: 'This is ' },
        { text: 'bold', bold: true },
        { text: ' and ' },
        { text: 'italic', italic: true },
        { text: ' and ' },
        { text: 'underlined', underline: true },
        { text: ' inline text.' }
    ]},
    { type: 'heading', level: 2, text: 'Tasks' },
    { type: 'list', list: { type: 'bullet', items: [
        'Design the UI',
        'Build the API',
        [{ text: 'Ship it: ', bold: true }, { text: 'by Friday' }]   // mixed-style item
    ]}},
    { type: 'list', list: { type: 'number', items: [
        'First step', 'Second step', 'Third step'
    ]}}
  ]
}

Why the exported file's formatting won't match your preview (and how to fix it)

Your app's editor/preview renders HTML/CSS in the iframe. The documents API does not take HTML — it takes the blocks/runs structure above and rebuilds the layout in its own renderer. So the export only matches the preview when you convert your editor's content to blocks before calling generate(). The four mistakes that make exports look wrong:

  1. Passing rich content as plain text. paragraphs: ['**Hello** world'] or text: '<b>Hello</b> world' — the API does NOT parse markdown/HTML, so **Hello** / <b>Hello</b> appears literally. Fix: convert to blocks{ type: 'paragraph', runs: [{ text: 'Hello', bold: true }, { text: ' world' }] }.
  2. Concatenating the whole document into one text blob. Headings and lists in a text blob are lost (everything is one unstyled paragraph run). Fix: emit one block per heading/paragraph/list.
  3. Dropping the space between styled runs. Two runs [{ text: 'bold', bold: true }, { text: 'word' }] render as "boldword" — there is no auto-space between runs. Put the space inside a run: [{ text: 'bold ', bold: true }, { text: 'word' }].
  4. Expecting styles the API doesn't support. Only bold, italic, underline (and headings/lists) carry over — no colors, fonts, alignment, or font sizes beyond the built-in heading/title sizes. For pixel-perfect layout, vendor your own library (jspdf/docx) and build the bytes yourself.

Converting your editor's HTML to blocks

Walk your editor's DOM and emit one DocumentBlock per element. A minimal converter:

function editorToBlocks(rootEl) {
  const blocks = [];
  for (const el of rootEl.childNodes) {
    if (el.nodeType === 3) { // text node
      const t = el.textContent;
      if (t.trim()) blocks.push({ type: 'paragraph', runs: [{ text: t }] });
      continue;
    }
    const tag = el.tagName.toLowerCase();
    if (tag === 'h1' || tag === 'h2' || tag === 'h3') {
      blocks.push({ type: 'heading', level: Number(tag[1]), text: el.textContent });
    } else if (tag === 'ul') {
      blocks.push({ type: 'list', list: { type: 'bullet', items: [...el.children].map(li => htmlToRuns(li)) } });
    } else if (tag === 'ol') {
      blocks.push({ type: 'list', list: { type: 'number', items: [...el.children].map(li => htmlToRuns(li)) } });
    } else if (tag === 'p') {
      blocks.push({ type: 'paragraph', runs: htmlToRuns(el) });
    } else if (el.textContent.trim()) {
      blocks.push({ type: 'paragraph', runs: [{ text: el.textContent }] });
    }
  }
  return blocks;
}
// Recursively turn an element into styled runs, mapping <b>/<strong>→bold,
// <i>/<em>→italic, <u>→underline.
function htmlToRuns(el) {
  const runs = [];
  for (const node of el.childNodes) {
    if (node.nodeType === 3) { runs.push({ text: node.textContent }); continue; }
    const child = htmlToRuns(node);
    const tag = node.tagName.toLowerCase();
    const style = tag === 'b' || tag === 'strong' ? { bold: true }
      : tag === 'i' || tag === 'em' ? { italic: true }
      : tag === 'u' ? { underline: true } : {};
    child.forEach(r => Object.assign(r, style));
    runs.push(...child);
  }
  return runs;
}
// Then: generate({ type: 'docx', content: { title, blocks: editorToBlocks(editor) } })

A heading becomes a real Word heading style (DOCX) / a bold larger line (PDF); a list becomes real Word bullet/numbering (DOCX) / a bullet or "1." marker (PDF).