Text Recipes

Small tasks. Ready-made buttons.

Copy a little JavaScript and make it your own in Textwhisk. These rules run locally on your Mac, with no AI key or generation needed.

Choose an action

Open a row to see an example and copy the code. Browse by task below.

Everyday actions

Replace spaces with underscores

Replace every space with an underscore. Repeated spaces become repeated underscores; tabs and line breaks stay unchanged.

Before

hello world
next line

After

hello_world
next_line

Suggested button name: Underscores

JavaScript

function (text) {
  return text.replace(/ /g, '_');
}

Sort lines ascending

Sort lines A to Z, ignoring letter case. Keep duplicates, blank lines and a final newline. Windows line endings become standard line breaks.

Before

banana
Apple
cherry

After

Apple
banana
cherry

Suggested button name: Sort A-Z

JavaScript

function (text) {
  var value = text.replace(/\r\n/g, '\n');
  var finalNewline = value.endsWith('\n');
  var lines = value.split('\n');
  if (finalNewline) lines.pop();
  lines.sort(function (a, b) {
    return a.localeCompare(b, 'en', { sensitivity: 'base' });
  });
  return lines.join('\n') + (finalNewline ? '\n' : '');
}

Sort lines descending

Sort lines Z to A, ignoring letter case. Keep duplicates, blank lines and a final newline. Windows line endings become standard line breaks.

Before

banana
Apple
cherry

After

cherry
banana
Apple

Suggested button name: Sort Z-A

JavaScript

function (text) {
  var value = text.replace(/\r\n/g, '\n');
  var finalNewline = value.endsWith('\n');
  var lines = value.split('\n');
  if (finalNewline) lines.pop();
  lines.sort(function (a, b) {
    return b.localeCompare(a, 'en', { sensitivity: 'base' });
  });
  return lines.join('\n') + (finalNewline ? '\n' : '');
}

Clean up text

Remove duplicate lines

Keep the first occurrence of each exact line. Matching is case-sensitive. Windows line endings become standard line breaks.

Before

apple
banana
apple

After

apple
banana

Suggested button name: Deduplicate

JavaScript

function (text) {
if (!text) return '';
  var value = text.replace(/\r\n/g, '\n');
  var finalNewline = value.endsWith('\n');
  var lines = value.split('\n');
  if (finalNewline) lines.pop();
  lines = Array.from(new Set(lines));
  return lines.join('\n') + (finalNewline && lines.length ? '\n' : '');
}

Remove empty lines

Remove blank and whitespace-only lines without trimming the lines you keep. Preserve a final newline if any content remains.

Before

apple

banana

After

apple
banana

Suggested button name: No blank lines

JavaScript

function (text) {
if (!text) return '';
  var value = text.replace(/\r\n/g, '\n');
  var finalNewline = value.endsWith('\n');
  var lines = value.split('\n');
  if (finalNewline) lines.pop();
  lines = lines.filter(function (line) { return line.trim().length > 0; });
  return lines.join('\n') + (finalNewline && lines.length ? '\n' : '');
}

Trim each line

Remove leading and trailing whitespace from every line. Keep the line structure; Windows line endings become standard line breaks.

Before

  apple  
 banana 

After

apple
banana

Suggested button name: Trim lines

JavaScript

function (text) {
return text.replace(/\r\n/g, '\n').split('\n').map(function (line) { return line.trim(); }).join('\n');
}

Collapse repeated spaces

Turn repeated ordinary spaces into one space. Tabs and line breaks stay unchanged.

Before

hello   world

After

hello world

Suggested button name: Single spaces

JavaScript

function (text) {
return text.replace(/ {2,}/g, ' ');
}

Replace underscores with spaces

Replace every underscore with an ordinary space. Everything else stays unchanged.

Before

hello_world

After

hello world

Suggested button name: Spaces

JavaScript

function (text) {
return text.replace(/_/g, ' ');
}

Remove line breaks

Join wrapped lines into a single paragraph, replacing surrounding line-break whitespace with one space and trimming the result.

Before

A thought
on two lines

After

A thought on two lines

Suggested button name: One paragraph

JavaScript

function (text) {
return text.replace(/[ \t]*(?:\r?\n[ \t]*)+/g, ' ').trim();
}

Work with lists

Add bullet points

Trim each line, skip empty lines and put a bullet before every remaining item.

Before

apple
banana

After

• apple
• banana

Suggested button name: Bullet list

JavaScript

function (text) {
var lines = text.split(/\r?\n/).map(function (line) { return line.trim(); }).filter(Boolean);
  return lines.map(function (line) { return '• ' + line; }).join('\n');
}

Number a list

Trim each line, skip empty lines and number the remaining items from the start.

Before

apple
banana

After

1. apple
2. banana

Suggested button name: Number list

JavaScript

function (text) {
var lines = text.split(/\r?\n/).map(function (line) { return line.trim(); }).filter(Boolean);
  return lines.map(function (line, i) { return (i + 1) + '. ' + line; }).join('\n');
}

Join lines with commas

Trim each line, skip empty lines and join the items with a comma and a space.

Before

apple
banana

After

apple, banana

Suggested button name: Join commas

JavaScript

function (text) {
var lines = text.split(/\r?\n/).map(function (line) { return line.trim(); }).filter(Boolean);
  return lines.join(', ');
}

Split commas into lines

Split on every comma, trim each item and skip empty items. This is not a full CSV parser: quoted commas are split too.

Before

apple, banana, cherry

After

apple
banana
cherry

Suggested button name: Split commas

JavaScript

function (text) {
return text.split(',').map(function (item) { return item.trim(); }).filter(Boolean).join('\n');
}

Reverse line order

Put the last line first without alphabetically sorting. Keep blank lines and a final newline; normalize Windows line endings.

Before

first
second
third

After

third
second
first

Suggested button name: Reverse lines

JavaScript

function (text) {
if (!text) return '';
  var value = text.replace(/\r\n/g, '\n');
  var finalNewline = value.endsWith('\n');
  var lines = value.split('\n');
  if (finalNewline) lines.pop();
  lines.reverse();
  return lines.join('\n') + (finalNewline && lines.length ? '\n' : '');
}

Natural sort

Sort English text with numbers in natural order, ignoring case. Equal keys keep their order. Keep duplicates, blank lines and a final newline.

Before

item10
item2
item1

After

item1
item2
item10

Suggested button name: Natural sort

JavaScript

function (text) {
if (!text) return '';
  var value = text.replace(/\r\n/g, '\n');
  var finalNewline = value.endsWith('\n');
  var lines = value.split('\n');
  if (finalNewline) lines.pop();
  lines.sort(function (a, b) { return a.localeCompare(b, 'en', { numeric: true, sensitivity: 'base' }); });
  return lines.join('\n') + (finalNewline && lines.length ? '\n' : '');
}

Names and formatting

Create a URL slug

Make a lowercase, hyphen-separated slug from Latin letters and digits. Fold common accents. Other scripts are not transliterated.

Before

My Blog Post!

After

my-blog-post

Suggested button name: URL slug

JavaScript

function (text) {
var out = text.normalize('NFKD').replace(/[\u0300-\u036f]/g, '').toLowerCase()
    .replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
  return out || 'No Latin letters or digits found.';
}

Convert to snake_case

Join Latin words with underscores in lowercase. Recognize common camel/acronym boundaries and fold accents; no general transliteration.

Before

My Blog Post

After

my_blog_post

Suggested button name: snake_case

JavaScript

function (text) {
var words = text.normalize('NFKD').replace(/[\u0300-\u036f]/g, '')
    .replace(/([a-z0-9])([A-Z])/g, '$1 $2')
    .replace(/([A-Z])([A-Z][a-z])/g, '$1 $2').match(/[A-Za-z0-9]+/g) || [];
  if (!words.length) return 'No Latin letters or digits found.';
  return words.map(function (word) { return word.toLowerCase(); }).join('_');
}

Convert to camelCase

Join Latin words in camelCase. Recognize common camel/acronym boundaries and fold accents; no general transliteration.

Before

My Blog Post

After

myBlogPost

Suggested button name: camelCase

JavaScript

function (text) {
var words = text.normalize('NFKD').replace(/[\u0300-\u036f]/g, '')
    .replace(/([a-z0-9])([A-Z])/g, '$1 $2')
    .replace(/([A-Z])([A-Z][a-z])/g, '$1 $2').match(/[A-Za-z0-9]+/g) || [];
  if (!words.length) return 'No Latin letters or digits found.';
  return words.map(function (word, i) {
    word = word.toLowerCase();
    return i ? word.charAt(0).toUpperCase() + word.slice(1) : word;
  }).join('');
}

Create Markdown checkboxes

Trim and skip blank lines, then turn every item into an unchecked Markdown task.

Before

apple
banana

After

- [ ] apple
- [ ] banana

Suggested button name: Checklist

JavaScript

function (text) {
var lines = text.split(/\r?\n/).map(function (line) { return line.trim(); }).filter(Boolean);
  return lines.map(function (line) { return '- [ ] ' + line; }).join('\n');
}

Create a Markdown quote

Put a Markdown quote marker before each line, including blank lines. Preserve a final newline; normalize Windows line endings.

Before

A thought
to keep

After

> A thought
> to keep

Suggested button name: Quote

JavaScript

function (text) {
if (!text) return '';
  var value = text.replace(/\r\n/g, '\n');
  var finalNewline = value.endsWith('\n');
  var lines = value.split('\n');
  if (finalNewline) lines.pop();
  lines = lines.map(function (line) { return '> ' + line; });
  return lines.join('\n') + (finalNewline && lines.length ? '\n' : '');
}

Format JSON

Format valid JSON with two-space indentation. Keep the original number spelling, escaped strings and duplicate keys. Invalid JSON returns an explanation.

Before

{"name":"Textwhisk","local":true}

After

{
  "name": "Textwhisk",
  "local": true
}

Suggested button name: Format JSON

JavaScript

function (text) {
try { JSON.parse(text); }
  catch (err) { return 'Invalid JSON. Select a complete JSON object, array or value.'; }
  var tokens = text.match(/"(?:\\[\s\S]|[^"\\])*"|[{}\[\],:]|[^\s{}\[\],:]+/g) || [];
  var out = '', depth = 0;
  function newline() { out += '\n' + '  '.repeat(depth); }
  for (var i = 0; i < tokens.length; i++) {
    var t = tokens[i];
    if (t === '{' || t === '[') {
      out += t; depth++;
      if (tokens[i + 1] !== (t === '{' ? '}' : ']')) newline();
    } else if (t === '}' || t === ']') {
      depth--;
      if (tokens[i - 1] !== (t === '}' ? '{' : '[')) newline();
      out += t;
    } else if (t === ',') { out += t; newline(); }
    else if (t === ':') out += ': ';
    else out += t;
  }
  return out;
}

Extract from text

Extract email addresses

Collect common ASCII email-like addresses, one per line. Deduplicate ignoring case while keeping the first spelling. This extracts text; it does not validate deliverability or every email syntax.

Before

Contact hello@example.com or team@example.org.

After

hello@example.com
team@example.org

Suggested button name: Emails

JavaScript

function (text) {
var matches = text.match(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/gi) || [];
  var seen = new Set();
  var out = matches.filter(function (email) {
    var key = email.toLowerCase();
    if (seen.has(key)) return false;
    seen.add(key); return true;
  });
  return out.length ? out.join('\n') : 'No email addresses found.';
}

Extract web links

Collect HTTP and HTTPS links, one per line, with exact duplicate removal. Trim prose punctuation and unmatched closing brackets. Check results when punctuation is part of the URL; this is a text extractor, not a full markup parser.

Before

Read https://example.com and https://example.org.

After

https://example.com
https://example.org

Suggested button name: Web links

function (text) {
var matches = text.match(/\bhttps?:\/\/[^\s<>"']+/gi) || [];
  var seen = new Set(), out = [];
  matches.forEach(function (url) {
    var previous, pairs = { ')': '(', ']': '[', '}': '{' };
    do {
      previous = url;
      url = url.replace(/[.,;!?]+$/g, '');
      var tail = url.slice(-1);
      while (pairs[tail] && url.split(tail).length > url.split(pairs[tail]).length) {
        url = url.slice(0, -1); tail = url.slice(-1);
      }
    } while (url !== previous);
    try { new URL(url); } catch (err) { return; }
    if (!seen.has(url)) { seen.add(url); out.push(url); }
  });
  return out.length ? out.join('\n') : 'No HTTP or HTTPS links found.';
}

Time zones

Convert between time zones

Choose source and destination time zones here, then copy the reusable code. Select one time in Textwhisk, optionally prefixed by YYYY-MM-DD. Without a date it uses today in the source zone and labels that assumption. Results include the destination date. Nonexistent or repeated clock-change times return an explanation.

Before

2026-07-15 10:30

After

2026-07-15 07:00 Europe/Vienna

Suggested button name: Convert time

JavaScript · India to Vienna

function (text) {
  var from = "Asia/Kolkata";
  var to = "Europe/Vienna";
    var value = String(text).trim();
    if (from === 'Asia/Kolkata' || from === 'Asia/Calcutta') {
      value = value.replace(/\s+IST$/i, '');
    }
    var m = /^(?:(\d{4})-(\d{2})-(\d{2})[T ]+)?(\d{1,2})(?::(\d{2}))?\s*(AM|PM)?$/i.exec(value);
    if (!m) return 'Select one time, like 10:30 AM or 2026-07-15 10:30. No sentence or extra time-zone label.';
    function formatter(zone) {
      return new Intl.DateTimeFormat('en-GB-u-ca-gregory-nu-latn', {
        timeZone: zone, year: 'numeric', month: '2-digit', day: '2-digit',
        hour: '2-digit', minute: '2-digit', second: '2-digit', hourCycle: 'h23'
      });
    }
    var source = formatter(from), target = formatter(to);
    function fields(at, fmt) {
      var obj = {};
      fmt.formatToParts(at).forEach(function (p) { obj[p.type] = p.value; });
      return ['year', 'month', 'day', 'hour', 'minute', 'second'].map(function (k) { return Number(obj[k]); });
    }
    function pad(n) { return String(n).padStart(2, '0'); }
    function dateOf(p) { return p[0] + '-' + pad(p[1]) + '-' + pad(p[2]); }
    var today = fields(Date.now(), source);
    var y = m[1] ? Number(m[1]) : today[0];
    var mo = m[1] ? Number(m[2]) : today[1];
    var d = m[1] ? Number(m[3]) : today[2];
    var h = Number(m[4]), minute = Number(m[5] || 0), period = m[6];
    if (minute > 59 || (period ? h < 1 || h > 12 : h > 23)) return 'Invalid time. Use 00:00 to 23:59, or 1:00 AM to 12:59 PM.';
    if (period) h = h % 12 + (period.toUpperCase() === 'PM' ? 12 : 0);
    var wall = Date.UTC(y, mo - 1, d, h, minute);
    var date = new Date(wall);
    if (y < 1000 || y > 9999 || date.getUTCFullYear() !== y || date.getUTCMonth() !== mo - 1 || date.getUTCDate() !== d) {
      return 'Invalid date. Use a real calendar date in YYYY-MM-DD format.';
    }
    var wanted = [y, mo, d, h, minute, 0].join(','), offsets = new Set(), found = [];
    for (var k = -48; k <= 48; k += 6) {
      var probe = wall + k * 3600000, p = fields(probe, source);
      offsets.add(Date.UTC(p[0], p[1] - 1, p[2], p[3], p[4], p[5]) - probe);
    }
    offsets.forEach(function (offset) {
      var at = wall - offset;
      if (fields(at, source).join(',') === wanted) found.push(at);
    });
    if (!found.length) return 'This local time does not exist in ' + from + ' because the clocks or calendar changed. Choose another local time.';
    if (found.length > 1) return 'This local time occurs twice in ' + from + '. Use a UTC-source rule with the intended UTC time to avoid guessing.';
    var output = fields(found[0], target);
    return dateOf(output) + ' ' + pad(output[3]) + ':' + pad(output[4]) + ' ' + to +
      (m[1] ? '' : '\nAssumed source date: ' + dateOf(today) + ' ' + from);
  
}

If a rule removes everything, Textwhisk may show “empty result” in its preview or result card. That does not mean the code is unsafe.

These snippets transform text only. They do not open websites, read files or make network requests.