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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
}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
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
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
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.