Skip to content

Latest commit

 

History

History

README.md

Clipboard Table

Copyable JavaScript functions for spreadsheet paste fields. Keep multiline cells, double quotes, empty columns, whitespace and leading zeros intact in the plain-text data you receive. Free under the MIT license, with no runtime dependencies and no build step.

Using Python and Streamlit? The separate decimal paste example keeps numeric input as text, then converts it with explicit decimal and grouping marks.

For identifier columns in CSV or XLSX files, Identifier Column preserves literal text and reports empty, missing or nontext entries before further processing.

Try it

Open the free browser playground to try it immediately. Choose TSV (tabs) for spreadsheet clipboard text or CSV (commas), then paste text or choose Load example. Select CSV, JSON or Markdown table, and copy the complete output or download a .csv, .json or .md file. Changing either format reparses the current input; it does not rewrite it. Delimiters are explicit, never guessed. If automatic copying is unavailable, the page selects the complete output for manual copying. The preview shows up to 30 data rows and 8 columns; output, copy and download include the complete result. Everything runs in the page with no uploads or external runtime assets.

CSV uses commas and requires double quotes around cells containing commas, line breaks or quotes; double any quote inside a quoted cell. Malformed quoting reports the row and column. Semicolon-separated CSV and file uploads are not supported by this page; paste the file's text instead.

JSON accepts up to 1,000,000 UTF-16 code units, 10,000 rows and 256 columns. Its first-row option produces objects with unique, nonblank keys; turning it off produces arrays and permits ragged rows. Markdown accepts up to 100,000 UTF-16 code units, 1,000 rows and 64 columns, including input delimiters and quotes. Markdown always requires equal-width rows; headings may be blank or repeated. Turning headings off generates Column 1, Column 2, and so on while retaining every input row as data. Markdown escapes punctuation and represents cell line breaks as <br>; rendered whitespace and HTML line-break support depend on the Markdown renderer.

CSV output includes every parsed row, including the first one. Its first-row option affects the preview only; blank or duplicate headings and unequal row widths are kept without padding or dropping cells. Extra preview columns beyond the first row receive generated Column N labels. Every field is quoted, embedded quotes are doubled, and records are separated by CRLF. Empty fields, spaces, leading zeros and line breaks within cells remain text. CSV uses the JSON input limits listed above. The downloaded CSV adds one final CRLF; copying uses the content without a final record separator. Manual copying from the output text box may normalize line breaks, so use Download CSV to retain the serialized output. Receiving applications can still infer numeric types or evaluate formula-looking text; CSV quoting does not prevent that.

For offline use, download the local browser playground v0.4.0, extract it and open index.html in a modern browser. Clipboard permissions can vary for local files; manual copying and file downloads remain available.

The CSV output addresses the small-selection workflow described in PowerToys issue #34934: copy the selected cells, choose TSV input and CSV output here, then copy or download the result for your receiving application. This is a separate browser step, not an installed PowerToys action. Native Windows/Excel clipboard interoperability has not been tested.

For the module, examples and tests in one download, get the standalone v0.1.1 archive. After extracting it, run node example.mjs or node --test test.mjs from its directory.

Copy index.mjs and LICENSE into your project. TypeScript projects can also copy index.d.mts beside it. This module runs independently of the Python tools in this repository. It is distributed as source here, not as an npm package.

import { parseClipboard, toRecords } from './index.mjs';

const text = 'SKU\tNotes\r\n00123\t"First line\nSecond line"\r\n';
const rows = parseClipboard(text);
console.log(toRecords(rows));
// [{ SKU: '00123', Notes: 'First line\nSecond line' }]

From a repository checkout, run the synthetic example and tests with Node.js 22 or newer:

node javascript/clipboard-table/example.mjs
node --test javascript/clipboard-table/test.mjs

The module uses standard modern JavaScript features including Object.hasOwn and String.replaceAll.

The playground source lives in playground.html, playground.mjs and the pure output adapter, which reuses this TSV parser and the CSV parser and Markdown formatter. To rebuild the self-contained HTML from these source modules, run node javascript/clipboard-table/build-playground.mjs from the repository root. The result is downloads/clipboard-table-playground.html. Pasting retains line endings in the received text; editing the text box uses the browser's normalized line endings. The standalone parseClipboard module remains TSV-only.

Run the output adapter's checks with node --test javascript/clipboard-table/playground-output.test.mjs. They cover both input formats, all output modes, quoted cells, header behavior, limits and complete copy/download payloads. These checks supplement the parser suite; they do not establish clipboard-permission behavior in every browser.

Connect a paste field

For a page containing <textarea id="paste-input"></textarea> and <pre id="result"></pre>, serve the copied module alongside your page and use this module script:

import { parseClipboard } from './index.mjs';

const input = document.querySelector('#paste-input');
const output = document.querySelector('#result');
input.addEventListener('paste', event => {
  if (!event.clipboardData?.types.includes('text/plain')) return;
  event.preventDefault();
  try {
    const rows = parseClipboard(event.clipboardData.getData('text/plain'));
    output.textContent = JSON.stringify(rows, null, 2);
  } catch (error) {
    output.textContent = error.message;
  }
});

Use textContent for the preview. The handler reads the current paste event only and processes its plain text locally. The module has no network requests or storage. The surrounding application determines what happens to its returned values.

A React paste handler uses the same functions:

import { useState } from 'react';
import { parseClipboard } from './index.mjs';

export function PasteField() {
  const [preview, setPreview] = useState('');
  function onPaste(event) {
    if (!event.clipboardData.types.includes('text/plain')) return;
    event.preventDefault();
    try {
      setPreview(JSON.stringify(parseClipboard(
        event.clipboardData.getData('text/plain')
      ), null, 2));
    } catch (error) {
      setPreview(error.message);
    }
  }
  return <>
    <label>Paste a spreadsheet range <textarea onPaste={onPaste} /></label>
    <pre aria-live="polite">{preview}</pre>
  </>;
}

Browser event behavior is documented in MDN's clipboardData and paste event references. These are integration examples; test a real copy/paste on the spreadsheet and browser versions your application supports.

For Sanity's _key/_type/cells row shape, the separate grid update helper and worked example preserve existing row keys and surrounding cells while growing a pasted rectangle. This source-only companion does not send Studio patches and is not included in the v0.1.1 parser archive or browser playground.

API

parseClipboard(text, limits?) → string[][]

  • Tab separates cells; LF, CRLF and CR separate records.
  • A field starting with " is quoted. Within it, "" represents one quote and tabs/newlines remain in the cell. A quote inside an unquoted field is literal.
  • An initial unquoted UTF-8 BOM, represented as \uFEFF in JavaScript, is removed. All other characters, including spaces and non-breaking spaces, are preserved.
  • Empty input produces []. A single empty quoted cell produces [['']].
  • One terminal record separator ends the preceding row. Further separators preserve explicit blank rows. No .trim() is applied.
  • Values remain strings. 00123, a long identifier, a date-looking value and true are not converted.
  • Ragged rows are preserved by the parser. Use toRecords when you need a rectangular table with a header.
parseClipboard(text, { maxChars: 500_000, maxRows: 2_000, maxColumns: 80 });

Defaults: 1,000,000 UTF-16 code units including a leading BOM, 10,000 rows and 256 columns. Overrides must be positive safe integers; unknown option names are rejected. Bounds apply to parsing, not to the input array of the other functions.

formatClipboard(rows) → string

Serializes arrays of strings with tab-separated cells and CRLF-separated records. Quotes are escaped by doubling. Empty cells and cells containing quotes, tabs, line breaks or BOM characters are quoted. There is no terminal record separator. Within the parser's size limits, parseClipboard(formatClipboard(rows)) preserves cell contents and row lengths. Empty rows ([]) and non-string cells are rejected; an empty table ([]) is allowed.

toRecords(rows) → Record<string, string>[]

Uses the first row as exact, case-sensitive headers. It rejects blank/whitespace-only headers, exact duplicate headers and rows of a different width, so values are not silently dropped. Headers are not trimmed or renamed. Special names such as __proto__ become own data properties. The input arrays are not changed.

Errors

Wrong argument types, non-string cells and invalid limits throw TypeError. Structural errors throw ClipboardTableError with code, one-based table row and column, and a zero-based UTF-16 offset for parser errors. Embedded line breaks do not increment the table row. Error messages do not include source values.

Codes: MAX_CHARS, MAX_ROWS, MAX_COLUMNS, UNCLOSED_QUOTE, UNEXPECTED_CHARACTER, EMPTY_HEADER, DUPLICATE_HEADER, RAGGED_ROW.

What it covers

This parses the quoted TSV dialect described above. It does not read XLSX files, interpret HTML clipboard data, preserve merged cells or styles, evaluate formulas, or recover zeros/precision already lost in the source application. formatClipboard preserves formula-looking text: pasting that text into spreadsheet software can evaluate it. Quoting a field does not disable spreadsheet formulas or prevent the receiving application from converting types.

The suite checks 31 cases, including 144 deterministic round trips. This is not a claim of live interoperability testing across every Excel, Sheets and browser version. For large file imports or additional delimiter dialects, an established CSV/TSV parser may suit your application better.

The design notes and seven community examples explain the cases behind the module. Feedback with a small synthetic input and expected output is welcome through GitHub issues.

Buy me a coffee, if this helped

If this saved you a little time, you're welcome to buy me a coffee. Please don't feel obliged — using the code, reporting an issue, or sharing it is appreciated too.

  • USDC / SOL · Solana: 9tY6D9mwcFaJwwzEHvw2v7nhSpdjqjNBYtuooyBN6rYy
  • USDC / ETH · Base: 0x568Ab98578d682FB0B0b45619BE73EbFfbf5a6eA
  • USDT · BNB Smart Chain (BEP20): 0x568Ab98578d682FB0B0b45619BE73EbFfbf5a6eA

Please match the asset and network exactly. Fees depend on your wallet or exchange. Thank you! — Tevinch