⚡ JS Tools
A single-file, dependency-free toolkit of five content utilities for editors, SEOs and developers.
FAQ lists → WPBakery accordion shortcodes · strip inline styles and stray <p> tags from Word/Docs paste · bulk-edit link classes and rel · generate slugs · check SEO character limits. Live as you type, entirely in your browser. No uploads, no build step, no network requests.
MIT · 0 dependencies · 1 file
Contents: Quick start · Interface · FAQ → Accordion · HTML Cleaner · Link Tool · Slug Maker · Counter · Shared controls · Internals · Add your own tool · Theming · Clipboard caveats · Limitations · Roadmap
Why this exists
Content teams repeat the same five chores daily: reformatting an FAQ doc into a page-builder accordion, scrubbing Word paste-in garbage, adding rel="nofollow noopener" to a batch of links, slugifying a title, checking a meta description against Google's truncation point.
Every one of those has a dozen ad-supported websites offering to help — right after you paste your client's unpublished content onto someone else's server. JS Tools does all five locally. Open it from a USB stick on a plane if you want.
Quick start
- Download
jstools.html - Double-click it
No npm install, no bundler, no CDN links — all HTML, CSS and JS is inline.
Hosting (optional): drop it on GitHub Pages, Netlify, or a /tools/ folder on your own site. HTTPS unlocks the Paste buttons — see clipboard caveats. Nothing persists between refreshes, so keep it in a pinned tab.
Interface
┌──────────────────────────────────────────────────────────┐
│ ⚡ JS Tools 🌙 Theme │ header
├──────────────────────────────────────────────────────────┤
│ [FAQ] [Cleaner] [Links] [Slug] [Counter] │ tabs
├──────────────────────────────────────────────────────────┤
│ ☑ option ☑ option [field] [▶ Convert] │ toolbar
├───────────────────────────┬──────────────────────────────┤
│ INPUT 📋 ⧉ ✕ │ OUTPUT ⇄ ⭳ ⧉ ✕ │
│ textarea │ textarea │
│ 120 chars · 22 words │ 340 chars · 48 words │ live stats
└───────────────────────────┴──────────────────────────────┘
Same three-part layout everywhere: a toolbar (changing any option re-runs instantly — you never press Convert twice), an input card (Paste / Copy / Clear, plus a count pill on some tools), and an output card (Swap-to-input / Download / Copy / Clear). Every textarea has a live chars · words · lines footer.
Tool 1 — FAQ → Accordion

Plain Q/A list → WPBakery Page Builder shortcodes.
A line is a question if it ends with ? or starts with Q: / Q. / Q) / Q- (case-insensitive). Every non-blank line after it, until the next question, becomes the answer — optional A: prefixes and leading list numbers (1., 2)) are stripped, blank lines ignored, so double-spaced documents work as-is. Press ✨ Sample to load a working example.
What is your return policy?
You can return any unused item within 30 days for a full refund.
Q: Do you ship internationally?
A: Yes — over 40 countries, 5–10 business days.
[vc_tta_accordion]
[vc_tta_section title="What is your return policy?" tab_id="what-is-your-return-policy"][vc_column_text]You can return any unused item within 30 days for a full refund.[/vc_column_text][/vc_tta_section]
[/vc_tta_accordion]
| Option | Effect |
|---|---|
| Shortcode |
vc_tta_accordion (default), vc_tta_tabs or vc_tta_tour. Inner sections are identical, so you can switch layout without redoing content. |
| Single line output | On by default — WPBakery's classic editor injects stray <br> tags into multi-line shortcodes. Turn off for readable, indented output. |
Wrap answer in <p> |
For themes whose accordion body has no default paragraph spacing. |
| tab_id prefix |
faq → faq-what-is-your-return-policy. Essential with two accordions on one page — duplicate tab_ids break open/close behaviour. |
tab_id generation: accent-folded (Héllo → Hello), depunctuated, lowercased, spaces → hyphens, with a numeric suffix (-2, -3) on collisions. Double quotes in questions are escaped to " so they can't break out of title="".
The input pill shows how many Q/A pairs were found.
0 itemsmeans your questions don't end in?or start withQ:. Questions with no answer beneath them are discarded.
Tool 2 — HTML Cleaner

Strips the junk out of HTML pasted from Word, Google Docs, Outlook or another CMS.
Independent toggles, applied in this fixed order — order matters, e.g. removing attributes after stripping all tags is pointless.
-
Smart quotes → plain (off) — curly quotes → straight, en/em dashes →
-/--,…→.... For CSV imports, older PHP templates, some email clients. -
Remove
(on) — both the entity and the literal U+00A0. The single biggest source of "why is there a weird gap in my paragraph". -
Remove style attrs (on) + Remove class/id (off) — one pass via
DOMParser, not regex, so attributes inside comments or strings can't be mangled and malformed markup is normalised rather than corrupted. Class/id removal is aggressive, hence off by default. -
Remove
<p>tags (on) — drops opening tags, converts</p>to a newline so paragraph breaks survive. For fields that add their own wrapping (wpautop, most page builders). - Strip ALL tags (off) — nuclear option, leaves plain text.
- Drop empty lines (on) — cleans up after the steps above.
- Single line (off) — collapses whitespace runs, for shortcode attributes and JSON fields.
Presets: Word/Docs cleanup = style + + empty lines · Plain text = strip all + smart quotes + empty lines · Page-builder prep = <p> + style + class/id · One-line value = strip all + single line
Tool 3 — Link Tool

Applies classes, target and rel to every <a> in a block of HTML at once. Also DOM-parsed and re-serialised, so it can never emit a broken tag.
| Option | Effect |
|---|---|
| Add class | Space-separated, e.g. btn btn-primary. Added via classList.add() — duplicates impossible, existing classes preserved. |
| Replace existing classes | Wipes class before adding yours. For re-skinning links carrying an old theme's classes. |
target="_blank" |
Added to every matched link. |
| rel |
noopener noreferrer (best practice for _blank) · nofollow noopener (paid/untrusted) · nofollow sponsored noopener (Google's sponsored markup) · ugc noopener · — keep — leaves existing values alone. |
| External only | Limits changes to hrefs starting http:// or https://. |
| domain field | With External only: links containing this domain count as internal and are skipped. Enter bare, e.g. mysite.com — leading www. stripped automatically. |
The pill reads 12 links · 4 updated. That second number is the sanity check — 0 updated with External-only on means your links are relative paths (/about), or your domain filter is matching everything.
Typical run: paste a legacy post, tick External only, enter your domain, pick nofollow sponsored noopener, tick target="_blank", Apply, copy back. Thirty links fixed in five seconds.
Tool 4 — Slug Maker
Titles → clean, URL-safe slugs. NFD-normalises and strips combining marks (Héllo Wörld → Hello World, not hll-wrld), removes HTML entities, removes anything that isn't a word character / whitespace / hyphen, collapses separator runs, trims the ends.
| Option | Effect |
|---|---|
| Separator |
- (SEO standard) or _ (file names, some CMS taxonomies). |
| Lowercase | On by default. Off preserves casing for case-sensitive asset paths. |
| One slug per line | Batch mode. Off = whole input squashed into one slug. |
| Max length |
0 = unlimited. Otherwise truncates then trims the trailing separator, so you never get my-great-post-. |
Héllo Wörld — My Great Post! (2024) → hello-world-my-great-post-2024
Tool 5 — Counter

Live text metrics plus SEO length guides — one full-width input with a stats dashboard beneath.
- Characters — including spaces and newlines · No spaces — all whitespace removed
-
Chars + offset — count plus a configurable Offset (default
22). For when a template appends a fixed suffix (site name in a title tag, tracking string, prefix code) and you need to budget for it while writing. -
Words — whitespace-delimited tokens · Lines — empty box reports
0, not1 -
Sentences — runs ending
.,!or?. Approximate;e.g.inflates it. -
Read time — 200 wpm, seconds under a minute, otherwise
Xm Ys
Two progress bars track the conventional truncation limits — SEO title 60 and meta description 160. Fill is green within budget, red the moment you exceed it, capped at 100% width so long strings don't blow out the layout. Both read the same textarea, so check title and description one at a time.
Shared controls
Every control is a real <button> — focusable, keyboard-activatable, driven by one delegated click handler. No dead hrefs.
| Button | Behaviour |
|---|---|
| 📋 Paste | Reads the clipboard and re-runs the tool. Needs a secure context; if blocked, focuses the field and toasts "use Ctrl+V". |
| ⧉ Copy | Async Clipboard API with a hidden-textarea execCommand fallback, so it works on file:// too. Flashes ✓ Copied for 1.2 s; warns on an empty field rather than doing nothing. |
| ✕ Clear | Empties, re-runs, refocuses the textarea. |
| ⇄ To input | Sends output back to input — chain operations or re-run with different settings. |
| ⭳ Save | Downloads .txt / .html via a Blob URL. Nothing touches a server. |
| ✨ Sample | (FAQ) Loads demo content. |
| ▶ Convert / Clean / Apply / Slugify | Forces a run. Rarely needed — useful after a paste that didn't fire an input event. |
| 🌙 Theme | Dark/light, stored in localStorage as jstools-theme. |
Toasts appear bottom-right for 2.2 s: success (green), warn (amber), error (red), info (indigo).
Internals
~250 lines of JavaScript held together by four conventions. Learn these and you can change anything.
1. The RUN registry — every tool is a plain function on one object. Nothing else knows what a tool does; callers just invoke RUN[name]().
const RUN = {};
RUN.faq = () => { /* read inputs, write output */ };
RUN.clean = () => { /* ... */ };
2. Declarative buttons (data-act) — exactly one click listener, on document, reading attributes off whatever was clicked:
<button data-act="copy" data-target="faqOut">⧉ Copy</button>
<button data-act="swap" data-from="faqOut" data-to="faqIn">⇄</button>
<button data-act="download" data-target="faqOut" data-file="accordion.txt">⭳</button>
<button data-act="run" data-run="faq">▶ Convert</button>
Event delegation means buttons added later work automatically, and a typo in one can't break the others — the core failure mode of the original inline-onclick version.
3. Live conversion (data-live) — data-live="faq" re-runs RUN.faq 150 ms after you stop typing (debounced, so a 5,000-word paste doesn't re-parse per keystroke). Any input or select inside a .toolbar also re-runs its panel's tool on change.
4. Stats (data-stats-for) — <span data-stats-for="faqIn"> is auto-populated with #faqIn's char/word/line count. Add the span, get the stats.
Plus: no dependencies — jQuery, Bootstrap, toastr and CKEditor are gone (Bootstrap's grid → ~40 lines of CSS Grid, toastr → a 6-line toast(), jQuery → $/$$ helpers). DOM parsing over regex — regex-based HTML editing is exactly what produces the mangled markup these tools exist to fix. CSS custom properties for theming.
Add your own tool
1. Tab button
<button class="tab" data-tab="case">🔠 Case Converter</button>
2. Panel — id must be tab- + the data-tab value. Copy an existing panel's structure; the only wiring that matters is data-live on the input, data-stats-for in the footers, and matching ids:
<section class="panel" id="tab-case">
<div class="toolbar">
<div class="field">Style
<select id="cs_mode">
<option value="upper">UPPERCASE</option>
<option value="title">Title Case</option>
</select>
</div>
<div class="btns" style="margin-left:auto">
<button class="btn primary" data-act="run" data-run="case">▶ Convert</button>
</div>
</div>
<div class="grid">
<!-- input card -->
<textarea id="caseIn" class="editor" data-live="case"></textarea>
<span data-stats-for="caseIn">0 chars</span>
<!-- output card -->
<textarea id="caseOut" class="editor"></textarea>
<span data-stats-for="caseOut">0 chars</span>
</div>
</section>
3. Register the function
RUN.case = () => {
const v = $('#caseIn').value;
$('#caseOut').value = $('#cs_mode').value === 'upper'
? v.toUpperCase()
: v.replace(/\w\S*/g, w => w[0].toUpperCase() + w.slice(1).toLowerCase());
stats($('#caseOut'));
};
4. There is no step 4. Tabs, buttons, live typing, stats and toasts all pick it up from the markup.
⚠️ The
data-live/data-stats-forwiring runs once at load. Inject a panel at runtime and you must re-run the two$$(...)setup loops.
Theming
Colours are CSS variables declared twice — :root (dark) and html[data-theme="light"]:
:root{
--bg:#0e1320; --bg2:#141b2b; --card:#19212f; --line:#2a3446;
--txt:#e7edf7; --muted:#93a1b8;
--accent:#6366f1; --accent2:#22d3ee;
--ok:#22c55e; --warn:#f59e0b; --err:#ef4444;
--radius:14px;
}
To rebrand, change --accent / --accent2 and the .app-header gradient. For a light-only build, set data-theme="light" on <html> and delete the toggle.
Browser support & clipboard caveats
Current Chrome, Edge, Firefox and Safari. Uses DOMParser, classList and template literals — ES2019, no transpilation, no IE.
Copy works everywhere: navigator.clipboard.writeText() first, hidden textarea + document.execCommand('copy') as fallback, explicit "press Ctrl+C" toast if both fail.
Paste is conditional — navigator.clipboard.readText() is far more restricted:
| Context | Result |
|---|---|
https:// in Chrome/Edge |
✅ Works (may prompt once) |
http://localhost |
✅ Works |
file:// (double-clicked) |
❌ Insecure context |
| Firefox | ❌ Blocked for regular pages by design |
| Safari | ⚠️ Genuine user gesture only; often still blocked |
On failure the app focuses the textarea and toasts "Browser blocked paste — use Ctrl+V". Manual pasting always works and fires the same live conversion, so nothing is lost. Host over HTTPS if you want the Paste buttons.
Limitations
- Counter's two SEO bars read one field — check title and description separately.
-
Sentence counting is naïve —
e.g.,Dr.and decimals each count as a sentence end. -
FAQ parsing needs a marker — statement headings ("Shipping information") get absorbed into the previous answer. Prefix with
Q:. -
DOMParsernormalises markup — self-closing tags, attribute quoting and tag casing may change. Valid HTML, just not byte-identical. - Nothing persists except the theme. Refresh clears every textarea — intentional, but worth knowing.
- No undo — ⇄ To input overwrites without confirmation.
-
Code smell:
RUN.slug'sif(!$('#sl_lower').checked === false)works but should beif($('#sl_lower').checked).
Roadmap
- [ ] Persist textareas in
localStorage(+ Clear-all) - [ ] Separate title/description fields in the Counter
- [ ] Markdown ⇄ HTML tab
- [ ] Find & replace with regex and match count
- [ ] Gutenberg block output alongside WPBakery
- [ ] Keyboard shortcuts (
Ctrl+1..5,Ctrl+Enter) - [ ] URL-hash routing (
#linksdeep-links a tool) - [ ] Export/import option presets as JSON

Top comments (0)