DEV Community

Alexander
Alexander

Posted on

JS Tools — Five content utilities in one HTML file

⚡ 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

  1. Download jstools.html
  2. 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
└───────────────────────────┴──────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

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

FAQ2Accordion
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.
Enter fullscreen mode Exit fullscreen mode
[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]
Enter fullscreen mode Exit fullscreen mode
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 faqfaq-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élloHello), depunctuated, lowercased, spaces → hyphens, with a numeric suffix (-2, -3) on collisions. Double quotes in questions are escaped to &quot; so they can't break out of title="".

The input pill shows how many Q/A pairs were found. 0 items means your questions don't end in ? or start with Q:. Questions with no answer beneath them are discarded.


Tool 2 — HTML Cleaner

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.

  1. Smart quotes → plain (off) — curly quotes → straight, en/em dashes → -/--, .... For CSV imports, older PHP templates, some email clients.
  2. Remove &nbsp; (on) — both the entity and the literal U+00A0. The single biggest source of "why is there a weird gap in my paragraph".
  3. 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.
  4. 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).
  5. Strip ALL tags (off) — nuclear option, leaves plain text.
  6. Drop empty lines (on) — cleans up after the steps above.
  7. Single line (off) — collapses whitespace runs, for shortcode attributes and JSON fields.

Presets: Word/Docs cleanup = style + &nbsp; + 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

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

Slug Maker

Titles → clean, URL-safe slugs. NFD-normalises and strips combining marks (Héllo WörldHello 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

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, not 1
  • 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 = () => { /* ... */ };
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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'));
};
Enter fullscreen mode Exit fullscreen mode

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-for wiring 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;
}
Enter fullscreen mode Exit fullscreen mode

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 conditionalnavigator.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ïvee.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:.
  • DOMParser normalises 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's if(!$('#sl_lower').checked === false) works but should be if($('#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 (#links deep-links a tool)
  • [ ] Export/import option presets as JSON

JS Tools on GitHub

Top comments (0)