

Embed III's batching engine directly into your own website. No server required. Everything runs in the browser.
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
});

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.
<input type="file"> or drag-and-dropFileList to batcher.process()Add this to your HTML <head> or before </body>:
<script src="https://infrequent.pages.dev/main.js"></script>
Download main.js from infrequent.pages.dev/main.js and include it locally:
<script src="./main.js"></script>
import { IIIBatcher } from 'https://infrequent.pages.dev/main.js';
window.IIIBatcher and as a named export for ES modules.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 can be passed to the constructor (as defaults) or to .process() (per-call override).
| Option | Type | Default | Description |
|---|---|---|---|
labelMode | string | 'numbered' | 'numbered', 'filename', or 'both' |
splitMode | string | 'chars' | 'chars', 'count', or 'none' |
maxChars | number | 50000 | Max characters per batch (when splitMode is 'chars') |
batchCount | number | 2 | Number of batches (when splitMode is 'count') |
skipBinary | boolean | true | Skip files detected as binary |
includePlaceholders | boolean | false | Include [Skipped: file] entries for excluded files |
excludeExtensions | string[] | [] | Array of extensions to exclude, e.g. ['.png', '.zip'] |
sortFiles | boolean | true | Sort files alphabetically before processing |
The .process() method returns a Promise that resolves to:
| Property | Type | Description |
|---|---|---|
batches | string[] | Array of formatted batch strings |
included | number | Number of files that were included |
skipped | number | Number of files that were skipped |
totalChars | number | Total character count across all batches |
totalFiles | number | Total files received (included + skipped) |
fileList | object[] | Array of { name, size, status } for each file |
options | object | The resolved options used for this run |
{
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 }
}
Track progress with event callbacks:
| Event | Callback Args | Description |
|---|---|---|
fileRead | { name, index, total, status } | Fired after each file is read |
progress | { percent, current, total } | Progress percentage update |
complete | result | Fired 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}`);
});
<!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>
<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>
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]);
<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>
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\\\`\\\`\\\`\`
})
});
}
If you embed III in an iFrame, you can communicate with it using postMessage.
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);
}
});
| Direction | Type | Data |
|---|---|---|
| Parent to iFrame | III_CONFIG | { options } |
| Parent to iFrame | III_PROCESS | { files: [{name, content}] } |
| iFrame to Parent | III_RESULT | { result } |
| iFrame to Parent | III_PROGRESS | { percent, current, total } |
| iFrame to Parent | III_ERROR | { error } |
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>
allow="clipboard-write" so the copy buttons work inside the iFrame.const batcher = new IIIBatcher({
binaryThreshold: 0.1 // 10% suspicious bytes = binary (default 0.15)
});
const batcher = new IIIBatcher({
sortFn: (a, b) => a.size - b.size // Sort by file size
});
const batcher = new IIIBatcher({
filterFn: (file) => {
// Only include files under 100KB
return file.size < 100 * 1024;
}
});
const batcher = new IIIBatcher({
labelFormatter: (index, name) => {
return `=== [${index}] ${name} ===`;
}
});
const batcher = new IIIBatcher(); const result1 = await batcher.processText(frontendFiles); const result2 = await batcher.processText(backendFiles); const allBatches = [...result1.batches, ...result2.batches];
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}`);
});
.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.| Code | Meaning |
|---|---|
NO_FILES | No files were provided to process() |
BINARY_DETECTED | File was detected as binary and skipped |
EXTENSION_EXCLUDED | File extension is in the exclusion list |
READ_FAILED | File could not be decoded as text |
FILTER_REJECTED | File was rejected by custom filterFn |
No. Everything runs in the browser. Files are read using the browser's FileReader / ArrayBuffer API. Nothing leaves the user's device.
All modern browsers: Chrome, Firefox, Safari, Edge, and Chromium-based mobile browsers. IE is not supported.
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.
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>
</>
);
}
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.
Add allow="clipboard-write" to the iFrame tag. Without it, the copy buttons inside the iFrame will fail.
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]
Yes! III is made by BufferClick. Reach out on GitHub.