Infrequent, Insufficient or Inadequate?

All processing runs locally
Select Files
0 files
Exclude Extensions
Comma-separated. Press Enter or click Add.
No exclusions — all file types included.

Click to toggle.
Options
Files grouped until character limit is reached.

Skip binary files
Auto-detect and skip unreadable binary content
Include placeholders for skipped
Add [Skipped: filename] entries in output
Show code preview
Display syntax-highlighted preview for each batch
Client-Side API

III JavaScript API

Embed III's batching engine directly into your own website. No server required. Everything runs in the browser.

Quick Start

Add this single script tag to your HTML and you're ready to go:

<script src="https://infrequent.pages.dev/main.js"></script>

Then use the API:

const batcher = new IIIBatcher();

// Add files from a file input
const input = document.querySelector('input[type="file"]');
input.addEventListener('change', async () => {
  const result = await batcher.process(input.files, {
    labelMode: 'filename',
    splitMode: 'chars',
    maxChars: 50000
  });

  console.log(result.batches);    // Array of batch strings
  console.log(result.included);   // Number of files included
  console.log(result.skipped);    // Number of files skipped
});
Full Documentation

III API Documentation

Complete Reference
Overview Installation API Reference Options Response Events Examples PostMessage API iFrame Embed Advanced Error Handling FAQ

Overview

The III API lets you integrate III's file batching engine into any website. It is 100% client-side — no files are ever uploaded to any server. Everything runs in the user's browser.

Key concept: III reads files using the browser's File API, formats them into labeled text blocks, splits them into batches based on your configuration, and returns the result as an array of strings.

How it works

  1. User selects files via <input type="file"> or drag-and-drop
  2. You pass the FileList to batcher.process()
  3. III reads each file, detects binary files, applies exclusions
  4. Files are formatted with labels and concatenated
  5. The output is split into batches based on your split configuration
  6. You receive an array of batch strings to display, copy, or send

Features

  • Zero dependencies — single JS file
  • Works in all modern browsers
  • Binary file detection
  • Extension-based exclusion
  • 3 label modes: numbered, filename, both
  • 3 split modes: by character limit, by batch count, no split
  • PostMessage API for cross-origin iFrame communication
  • Event callbacks for progress tracking

Installation

Option 1: Script Tag (CDN)

Add this to your HTML <head> or before </body>:

<script src="https://infrequent.pages.dev/main.js"></script>

Option 2: Download and Self-Host

Download main.js from infrequent.pages.dev/main.js and include it locally:

<script src="./main.js"></script>

Option 3: ES Module Import

import { IIIBatcher } from 'https://infrequent.pages.dev/main.js';
Note: The API is exposed both as a global window.IIIBatcher and as a named export for ES modules.

API Reference

new IIIBatcher(defaultOptions?)

Creates a new batcher instance with optional default options.

const batcher = new IIIBatcher({
  labelMode: 'filename',
  splitMode: 'chars',
  maxChars: 50000,
  skipBinary: true,
  excludeExtensions: ['.png', '.jpg', '.zip'],
  includePlaceholders: false
});

batcher.process(files, options?)

Processes a FileList or array of File objects. Returns a Promise.

const result = await batcher.process(fileInput.files, {
  labelMode: 'both',
  splitMode: 'count',
  batchCount: 3
});

// result.batches     → string[]
// result.included    → number
// result.skipped     → number
// result.totalChars  → number
// result.fileList    → { name, size, status }[]

batcher.processText(filesArray)

Process an array of { name, content } objects directly (no File API needed).

const result = await batcher.processText([
  { name: 'index.html', content: '<html>...</html>' },
  { name: 'style.css', content: 'body { color: red; }' },
  { name: 'app.js', content: 'console.log("hello")' }
]);

batcher.on(event, callback)

Listen to processing events. See Events section.

batcher.destroy()

Cleans up event listeners and internal state.

Options

Options can be passed to the constructor (as defaults) or to .process() (per-call override).

OptionTypeDefaultDescription
labelModestring'numbered''numbered', 'filename', or 'both'
splitModestring'chars''chars', 'count', or 'none'
maxCharsnumber50000Max characters per batch (when splitMode is 'chars')
batchCountnumber2Number of batches (when splitMode is 'count')
skipBinarybooleantrueSkip files detected as binary
includePlaceholdersbooleanfalseInclude [Skipped: file] entries for excluded files
excludeExtensionsstring[][]Array of extensions to exclude, e.g. ['.png', '.zip']
sortFilesbooleantrueSort files alphabetically before processing

Response Object

The .process() method returns a Promise that resolves to:

PropertyTypeDescription
batchesstring[]Array of formatted batch strings
includednumberNumber of files that were included
skippednumberNumber of files that were skipped
totalCharsnumberTotal character count across all batches
totalFilesnumberTotal files received (included + skipped)
fileListobject[]Array of { name, size, status } for each file
optionsobjectThe resolved options used for this run

Example response

{
  batches: [
    "index.html:\n<html>...</html>\n\nstyle.css:\nbody { }",
    "app.js:\nconsole.log('hello')"
  ],
  included: 3,
  skipped: 0,
  totalChars: 1542,
  totalFiles: 3,
  fileList: [
    { name: "index.html", size: 892, status: "included" },
    { name: "style.css", size: 234, status: "included" },
    { name: "app.js", size: 416, status: "included" }
  ],
  options: { labelMode: "filename", splitMode: "chars", maxChars: 50000 }
}

Events

Track progress with event callbacks:

EventCallback ArgsDescription
fileRead{ name, index, total, status }Fired after each file is read
progress{ percent, current, total }Progress percentage update
completeresultFired when processing is done
error{ file, error }Fired when a file fails to read
const batcher = new IIIBatcher();

batcher.on('progress', ({ percent, current, total }) => {
  console.log(`Processing: ${percent}% (${current}/${total})`);
  progressBar.style.width = percent + '%';
});

batcher.on('fileRead', ({ name, status }) => {
  console.log(`${name}: ${status}`);
});

batcher.on('complete', (result) => {
  console.log(`Done! ${result.batches.length} batches created.`);
});

batcher.on('error', ({ file, error }) => {
  console.error(`Failed to read ${file}: ${error}`);
});

Full Examples

Example 1: Basic File Upload Form

<!DOCTYPE html>
<html>
<head>
  <title>My Batcher</title>
  <script src="https://infrequent.pages.dev/main.js"></script>
</head>
<body>
  <input type="file" id="files" multiple />
  <button id="go">Process</button>
  <div id="output"></div>

  <script>
    const batcher = new IIIBatcher({
      labelMode: 'filename',
      splitMode: 'chars',
      maxChars: 30000,
      excludeExtensions: ['.png', '.jpg']
    });

    document.getElementById('go').addEventListener('click', async () => {
      const files = document.getElementById('files').files;
      const result = await batcher.process(files);

      const output = document.getElementById('output');
      output.innerHTML = '';

      result.batches.forEach((batch, i) => {
        const pre = document.createElement('pre');
        pre.textContent = batch;
        output.appendChild(pre);

        const btn = document.createElement('button');
        btn.textContent = 'Copy Batch ' + (i + 1);
        btn.onclick = () => navigator.clipboard.writeText(batch);
        output.appendChild(btn);
      });
    });
  </script>
</body>
</html>

Example 2: Drag and Drop

<div id="dropzone" style="border:2px dashed #444;padding:40px;text-align:center;">
  Drop files here
</div>

<script>
  const batcher = new IIIBatcher();
  const drop = document.getElementById('dropzone');

  drop.addEventListener('dragover', e => { e.preventDefault(); });
  drop.addEventListener('drop', async e => {
    e.preventDefault();
    const files = e.dataTransfer.files;
    const result = await batcher.process(files);

    result.batches.forEach((batch, i) => {
      console.log(`--- Batch ${i + 1} ---`);
      console.log(batch);
    });
  });
</script>

Example 3: Process Text Directly (No File Input)

const batcher = new IIIBatcher({ labelMode: 'both' });

const result = await batcher.processText([
  { name: 'server.py', content: 'from flask import Flask\napp = Flask(__name__)' },
  { name: 'config.json', content: '{ "debug": true }' },
  { name: 'README.md', content: '# My Project\nThis is a readme.' }
]);

// Copy first batch
navigator.clipboard.writeText(result.batches[0]);

Example 4: With Progress Bar

<style>
  .progress { width: 100%; height: 8px; background: #222; border-radius: 4px; }
  .progress-bar { height: 100%; background: #fff; border-radius: 4px; width: 0%; transition: width 0.1s; }
</style>

<input type="file" id="files" multiple webkitdirectory />
<div class="progress"><div class="progress-bar" id="bar"></div></div>
<div id="status"></div>

<script>
  const batcher = new IIIBatcher();
  const bar = document.getElementById('bar');
  const status = document.getElementById('status');

  batcher.on('progress', ({ percent }) => {
    bar.style.width = percent + '%';
  });

  batcher.on('fileRead', ({ name, status: s }) => {
    status.textContent = `Reading: ${name} (${s})`;
  });

  batcher.on('complete', (result) => {
    status.textContent = `Done! ${result.included} files in ${result.batches.length} batches.`;
  });

  document.getElementById('files').addEventListener('change', async (e) => {
    await batcher.process(e.target.files);
  });
</script>

Example 5: Send Batches to Discord Webhook

const batcher = new IIIBatcher({
  splitMode: 'chars',
  maxChars: 1900  // Discord message limit ~2000
});

const result = await batcher.process(fileInput.files);

for (const [i, batch] of result.batches.entries()) {
  await fetch('YOUR_WEBHOOK_URL', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      content: \`\\\`\\\`\\\`\\nBatch \${i + 1}:\\n\${batch}\\n\\\`\\\`\\\`\`
    })
  });
}

PostMessage API (Cross-Origin)

If you embed III in an iFrame, you can communicate with it using postMessage.

Send files to III iFrame

const iframe = document.getElementById('iii-frame');

// Send configuration
iframe.contentWindow.postMessage({
  type: 'III_CONFIG',
  options: {
    labelMode: 'filename',
    splitMode: 'chars',
    maxChars: 40000
  }
}, 'https://infrequent.pages.dev');

// Listen for results
window.addEventListener('message', (e) => {
  if (e.origin !== 'https://infrequent.pages.dev') return;
  if (e.data.type === 'III_RESULT') {
    console.log('Batches:', e.data.result.batches);
    console.log('Included:', e.data.result.included);
  }
});

Message types

DirectionTypeData
Parent to iFrameIII_CONFIG{ options }
Parent to iFrameIII_PROCESS{ files: [{name, content}] }
iFrame to ParentIII_RESULT{ result }
iFrame to ParentIII_PROGRESS{ percent, current, total }
iFrame to ParentIII_ERROR{ error }

iFrame Embed

You can embed the full III app as an iFrame in your site:

<iframe
  id="iii-frame"
  src="https://infrequent.pages.dev"
  width="100%"
  height="800"
  style="border: 1px solid #27272a; border-radius: 12px;"
  allow="clipboard-write"
></iframe>
Tip: Add allow="clipboard-write" so the copy buttons work inside the iFrame.

Advanced Usage

Custom binary detection threshold

const batcher = new IIIBatcher({
  binaryThreshold: 0.1  // 10% suspicious bytes = binary (default 0.15)
});

Custom file sorter

const batcher = new IIIBatcher({
  sortFn: (a, b) => a.size - b.size  // Sort by file size
});

Filter files programmatically

const batcher = new IIIBatcher({
  filterFn: (file) => {
    // Only include files under 100KB
    return file.size < 100 * 1024;
  }
});

Custom label formatter

const batcher = new IIIBatcher({
  labelFormatter: (index, name) => {
    return `=== [${index}] ${name} ===`;
  }
});

Chaining multiple processText calls

const batcher = new IIIBatcher();

const result1 = await batcher.processText(frontendFiles);
const result2 = await batcher.processText(backendFiles);

const allBatches = [...result1.batches, ...result2.batches];

Error Handling

try {
  const result = await batcher.process(files);
} catch (err) {
  console.error('Processing failed:', err.message);
}

// Or with the error event
batcher.on('error', ({ file, error }) => {
  console.warn(`Could not read ${file}: ${error}`);
});
Important: The .process() method will never throw for individual file read failures. It will skip or placeholder them based on your options. It only throws if no files are provided or a critical internal error occurs.

Error codes

CodeMeaning
NO_FILESNo files were provided to process()
BINARY_DETECTEDFile was detected as binary and skipped
EXTENSION_EXCLUDEDFile extension is in the exclusion list
READ_FAILEDFile could not be decoded as text
FILTER_REJECTEDFile was rejected by custom filterFn

FAQ

Are files uploaded to a server?

No. Everything runs in the browser. Files are read using the browser's FileReader / ArrayBuffer API. Nothing leaves the user's device.

What browsers are supported?

All modern browsers: Chrome, Firefox, Safari, Edge, and Chromium-based mobile browsers. IE is not supported.

Is there a file size limit?

No hard limit, but very large files (100MB+) may cause the browser tab to slow down since everything runs in memory. For best results, use the filterFn option to skip very large files, or set a reasonable maxChars.

Can I use this with React / Vue / Svelte?

Yes. Import the script and use new IIIBatcher() in your component. It's framework-agnostic.

// React example
import { useRef } from 'react';

function App() {
  const inputRef = useRef();
  const batcher = new IIIBatcher({ labelMode: 'filename' });

  const handleProcess = async () => {
    const result = await batcher.process(inputRef.current.files);
    console.log(result.batches);
  };

  return (
    <>
      <input ref={inputRef} type="file" multiple />
      <button onClick={handleProcess}>Process</button>
    </>
  );
}

Can I style the iFrame embed?

The iFrame content has its own styles. You can only style the iFrame container itself (border, size, etc.). For full control, use the JavaScript API instead of the iFrame.

How do I handle clipboard in an iFrame?

Add allow="clipboard-write" to the iFrame tag. Without it, the copy buttons inside the iFrame will fail.

What's the output format?

Each batch is a plain text string. Files are separated by two newlines. Each file entry looks like:

filename.ext:
[file contents here]

another-file.js:
[file contents here]

Can I contribute?

Yes! III is made by BufferClick. Reach out on GitHub.

Copied to clipboard