Skip to content

Hex ↔ Text Converter — TypeScript source

Convert text to hexadecimal and hex back to text, with delimiter options (none, spaces, 0x, backslash-x) and full UTF-8 support. 100% client-side.

This is the TypeScript implementation — the same logic the interactive tool runs, in a shareable, citable form.

// Pure hex ↔ text conversion - no React, no DOM, deterministic.
// Implements UTF-8 by hand so it runs in plain Node without TextEncoder.
// Never throws; decode of invalid sequences yields U+FFFD.

export type Delimiter = 'none' | 'space' | '0x' | 'backslash-x';

export interface DecodeResult {
  ok: boolean;
  text: string;
  error: string | null;
}

/** UTF-8 encode a JS string into a list of byte values (0..255). */
export function utf8Encode(str: string): number[] {
  const bytes: number[] = [];
  for (const ch of str) {
    const cp = ch.codePointAt(0)!;
    if (cp <= 0x7f) {
      bytes.push(cp);
    } else if (cp <= 0x7ff) {
      bytes.push(0xc0 | (cp >> 6), 0x80 | (cp & 0x3f));
    } else if (cp <= 0xffff) {
      bytes.push(0xe0 | (cp >> 12), 0x80 | ((cp >> 6) & 0x3f), 0x80 | (cp & 0x3f));
    } else {
      bytes.push(
        0xf0 | (cp >> 18),
        0x80 | ((cp >> 12) & 0x3f),
        0x80 | ((cp >> 6) & 0x3f),
        0x80 | (cp & 0x3f),
      );
    }
  }
  return bytes;
}

/** UTF-8 decode bytes to a JS string; invalid sequences → U+FFFD. */
export function utf8Decode(bytes: number[]): string {
  let out = '';
  let i = 0;
  while (i < bytes.length) {
    const b = bytes[i++];
    let cp: number;
    if (b <= 0x7f) {
      cp = b;
    } else if (b >> 5 === 0b110) {
      const b1 = bytes[i++] ?? 0;
      cp = ((b & 0x1f) << 6) | (b1 & 0x3f);
    } else if (b >> 4 === 0b1110) {
      const b1 = bytes[i++] ?? 0;
      const b2 = bytes[i++] ?? 0;
      cp = ((b & 0x0f) << 12) | ((b1 & 0x3f) << 6) | (b2 & 0x3f);
    } else if (b >> 3 === 0b11110) {
      const b1 = bytes[i++] ?? 0;
      const b2 = bytes[i++] ?? 0;
      const b3 = bytes[i++] ?? 0;
      cp = ((b & 0x07) << 18) | ((b1 & 0x3f) << 12) | ((b2 & 0x3f) << 6) | (b3 & 0x3f);
    } else {
      cp = 0xfffd;
    }
    out += String.fromCodePoint(cp);
  }
  return out;
}

/** Convert text to hex with a delimiter. */
export function textToHex(text: string, delimiter: Delimiter = 'none', uppercase = false): string {
  const bytes = utf8Encode(text);
  let hexes = bytes.map((b) => b.toString(16).padStart(2, '0'));
  if (uppercase) hexes = hexes.map((h) => h.toUpperCase());
  switch (delimiter) {
    case 'none':
      return hexes.join('');
    case 'space':
      return hexes.join(' ');
    case '0x':
      return hexes.map((h) => '0x' + h).join(' ');
    case 'backslash-x':
      return hexes.map((h) => '\\x' + h).join('');
  }
}

/** Strip 0x, \x, spaces, commas, colons; lowercase; keep hex only. */
export function sanitizeHex(input: string): string {
  return (input || '')
    .replace(/0x/gi, '')
    .replace(/\\x/gi, '')
    .replace(/[\s,:]/g, '')
    .toLowerCase();
}

/** Convert hex to text. */
export function hexToText(hex: string, _delimiter: Delimiter = 'none'): DecodeResult {
  const cleaned = sanitizeHex(hex);
  if (cleaned.length === 0) {
    return { ok: true, text: '', error: null };
  }
  if (!/^[0-9a-f]+$/.test(cleaned)) {
    return { ok: false, text: '', error: 'Hex strings may only contain 0-9 and a-f.' };
  }
  if (cleaned.length % 2 !== 0) {
    return { ok: false, text: '', error: 'Hex must have an even number of digits.' };
  }
  const bytes: number[] = [];
  for (let i = 0; i < cleaned.length; i += 2) {
    bytes.push(parseInt(cleaned.slice(i, i + 2), 16));
  }
  return { ok: true, text: utf8Decode(bytes), error: null };
}

Also available in 13 other languages

Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →