From 0b362bed4b67b82fec94745bf33121d1d0808ee2 Mon Sep 17 00:00:00 2001 From: Mo Elzubeir Date: Fri, 15 Aug 2025 00:19:06 -0500 Subject: [PATCH] initial commit --- .gitignore | 66 ++++++++++++++++++++++++ README.md | 24 +++++++++ package.json | 14 +++++ server.js | 142 +++++++++++++++++++++++++++++++++++++++++++++++++++ test.sh | 114 +++++++++++++++++++++++++++++++++++++++++ 5 files changed, 360 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 package.json create mode 100644 server.js create mode 100755 test.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cd0e284 --- /dev/null +++ b/.gitignore @@ -0,0 +1,66 @@ +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# next.js +/.next/ +/out/ +.next +next-env.d.ts + +# production +/build + +# misc +.DS_Store +*.pem +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# vercel +.vercel + +# typescript +*.tsbuildinfo + +# VSCode +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json + +# Emacs +*~ +\#*\# +/.emacs.desktop +/.emacs.desktop.lock +*.elc +auto-save-list +tramp +.\#* + +# OSX +.DS_Store +.AppleDouble +.LSOverride +Icon +._* +.Spotlight-V100 +.Trashes + +# Trae specific +.trae/ +.trae-cache/ \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..bec7206 --- /dev/null +++ b/README.md @@ -0,0 +1,24 @@ +# Em Dash Guide + +Don't you hate how em dashes ruin your carefully curated AI slop? + +I mean, you worked hard on that shit. You prompted and prompted and prompted. +Or maybe you have agents running amok, ruining everyone's feed with your +garbage. + +And then people start commenting, bot! Bot! Bot! + +Not cool, man.. not cool. + +Most of what I've seen online replace em dashes with one character or two +at the most. Not this! No sir! + +It has to make SENSE!!!! right? right? + +Alright, just, do the thing + + +``` +npm i +npm start +``` \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..85a3d40 --- /dev/null +++ b/package.json @@ -0,0 +1,14 @@ +{ + "name": "emdash-api", + "version": "1.0.0", + "description": "API to linguistically and grammatically replace em dashes.", + "main": "server.js", + "scripts": { + "start": "node server.js", + "dev": "node server.js" + }, + "dependencies": { + "express": "^4.18.2", + "cors": "^2.8.5" + } +} \ No newline at end of file diff --git a/server.js b/server.js new file mode 100644 index 0000000..ec2f19a --- /dev/null +++ b/server.js @@ -0,0 +1,142 @@ +const express = require('express'); +const cors = require('cors'); + +const app = express(); +const PORT = process.env.PORT || 3000; + +app.use(cors()); +app.use(express.json()); +app.use(express.text()); + +function removeEmDash(text) { + if (!text || typeof text !== 'string') { + return text; + } + + function isDateTimeOrPeriod(str) { + const dateTimePatterns = [ + /^\d+:\d+/, + /^\d+$/, // number.. could be date, who knows + /^(January|February|March|April|May|June|July|August|September|October|November|December|Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)$/i, // this is dumb + /^(Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday|Mon|Tue|Wed|Thu|Fri|Sat|Sun)$/i, // days + /^(Spring|Summer|Fall|Autumn|Winter)$/i, // seasons, did you see that, even autumn my zoul! + /^\d{4}$/, + /^Q[1-4]$/i, // quarters for that jerome powell post + ]; + return dateTimePatterns.some(pattern => pattern.test(str.trim())); + } + + let result = text + // 1. parenthetical/interruptive information (word — phrase — word) -> parentheses + .replace(/([a-zA-Z])\s*—\s*([^—]+?)\s*—\s*([a-zA-Z])/g, '$1 ($2) $3') + + // 2. date/time ranges (handle early to avoid conflicts) + .replace(/(\d+:\d+\s*[ap]\.?m\.?)\s*—\s*(\d+:\d+\s*[ap]\.?m\.?)/gi, '$1 to $2') + + // 3. interrupted speech in dialogue (word—") -> keep as interruption (but clean format) + .replace(/([a-zA-Z])\s*—\s*$/g, '$1 – ') + + // 4. attribution quotes ("quote" — Author) -> en dash + .replace(/"([^"]+)"\s*—\s*([A-Z][^.!?]*)/g, '"$1" – $2') + + // 5. dramatic pause/reveal (phrase — Word!) -> ellipsis + .replace(/([a-zA-Z\s]+)\s*—\s*([A-Z][a-zA-Z]*[!.])/g, '$1 ... $2') + + // 6. sudden break in thought (clause — but/and/or...) -> comma + .replace(/([a-zA-Z])\s*—\s*(but|and|or|yet|so)\s+/g, '$1, $2 ') + + // 7. appositive emphasis (noun — a descriptor) -> comma + .replace(/([a-zA-Z])\s*—\s*(a\s+[a-zA-Z][^.!?]*)/g, '$1, $2') + + // 8. summary/amplification at end (items — conclusion.) -> semicolon + .replace(/([a-zA-Z.,\s]*[a-zA-Z.,])\s*—\s*([a-z][^.!?]*\.)/g, '$1 ; $2') + + // 9. after sentence punctuation -> space + .replace(/([.!?:;])\s*—\s*([A-Za-z])/g, '$1 $2') + + // 10. after comma -> space + .replace(/([,])\s*—\s*([A-Za-z])/g, '$1 $2') + + // 11. before closing punctuation -> space + .replace(/([a-zA-Z])\s*—\s*([.,;:!?"'\)\]])/g, '$1 $2') + + // 12. after opening punctuation -> space + .replace(/(["'(\[])\s*—\s*([A-Za-z])/g, '$1 $2') + + // 13. between letters (simple cases) -> comma + .replace(/([a-zA-Z])\s*—\s*([a-zA-Z])/g, '$1, $2') + + // 14. leading em dash -> remove + .replace(/^\s*—\s*/g, '') + + // 15. trailing em dash -> remove + .replace(/\s*—\s*$/g, '') + + // 16. catch-all remaining -> check if it's a date/time range, otherwise en dash + .replace(/(\S+)\s*—\s*(\S+)/g, (_, left, right) => { + if (isDateTimeOrPeriod(left) && isDateTimeOrPeriod(right)) { + return `${left} – ${right}`; + } + return `${left} – ${right}`; + }); + + // sanitation step: clean up spacing issues + result = result + // remove spaces before colons and ellipses + .replace(/\s+\.\.\./g, '...') + .replace(/\s+:/g, ':') + .replace(/\s+;/g, ';') + // clean up multiple spaces + .replace(/\s+/g, ' ') + .trim(); + + return result; +} + +app.post('/emdash', (req, res) => { + try { + const text = typeof req.body === 'string' ? req.body : req.body.text; + + if (!text) { + return res.status(400).json({ error: 'Text is required, come on now' }); + } + + const result = removeEmDash(text); + res.json({ + original: text, + result: result + }); + } catch (error) { + res.status(500).json({ error: 'Internal server error' }); + } +}); + +app.get('/emdash', (req, res) => { + const text = req.query.text; + + if (!text) { + return res.status(400).json({ error: 'Text query parameter is required, come on now' }); + } + + const result = removeEmDash(text); + res.json({ + original: text, + result: result + }); +}); + +app.get('/', (req, res) => { + res.json({ + message: 'Em Dash API', + description: 'API to linguistically and grammatically replace em dashes.', + endpoints: { + 'POST /emdash': 'Send text in body (JSON or plain text)', + 'GET /emdash?text=your-text': 'Send text as query parameter' + } + }); +}); + + +app.listen(PORT, () => { + console.log(`Server running on port ${PORT}`); +}); \ No newline at end of file diff --git a/test.sh b/test.sh new file mode 100755 index 0000000..2aaefec --- /dev/null +++ b/test.sh @@ -0,0 +1,114 @@ +#!/bin/sh +# Portable test runner (no associative arrays required) + +API_URL="${API_URL:-http://localhost:3000/emdash}" + +# Detect a version-aware sort if available; fall back to plain sort +if sort -V /dev/null 2>&1; then + SORT_CMD='sort -V' +else + SORT_CMD='sort' +fi + +# --- Key/Value dataset (TAB-separated): keyvalue --- +DATA=' +parenthetical_1 My sister — who never cooks — made dinner last night. +parenthetical_2 The car — a bright red convertible — caught everyone’s attention. +parenthetical_3 This book — my all-time favorite — never gets old. +parenthetical_4 We met John — our old college roommate — at the reunion. +parenthetical_5 The plan — though risky — might just work. +suddenbreak_1 I thought it was a great idea — until I heard the cost. +suddenbreak_2 We were going to leave early — but the rain kept us inside. +suddenbreak_3 She almost told him the truth — and then decided against it. +suddenbreak_4 I started walking toward the door — then stopped. +suddenbreak_5 He was about to sign the contract — when the phone rang. +appositive_1 She is an expert — a true authority in her field. +appositive_2 He’s my best friend — the person I trust most. +appositive_3 This is the real problem — the lack of communication. +appositive_4 That’s the challenge — getting everyone to agree. +appositive_5 Here’s your reward — an extra day off. +summary_1 Long nights, endless rehearsals, countless cups of coffee — all for opening night. +summary_2 Broken glass, a smashed door, missing valuables — it was clearly a break-in. +summary_3 Late trains, heavy traffic, bad weather — today was not my day. +summary_4 Jeans, T-shirts, sneakers — his wardrobe never changed. +summary_5 Pain, sweat, and hard work — that’s what built this company. +listintro_1 We need several things — bread, milk, eggs, and butter. +listintro_2 She packed everything — sunscreen, towels, snacks, and water bottles. +listintro_3 The class will cover three topics — history, geography, and culture. +listintro_4 He ordered a variety of drinks — coffee, tea, soda, and juice. +listintro_5 They brought the essentials — maps, flashlights, and first-aid kits. +interrupt_1 “I was just about to—” +interrupt_2 “If you think I’m going to—” +interrupt_3 “Wait, I didn’t mean—” +interrupt_4 “Don’t you dare—” +interrupt_5 “I was trying to say—” +dramaticpause_1 And the winner is — Michael. +dramaticpause_2 The answer to your question is — yes. +dramaticpause_3 The person behind it all was — my own brother. +dramaticpause_4 The solution is simple — work together. +dramaticpause_5 Our biggest competitor is — ourselves. +internalcommas_1 She laughed, cried, and shouted — but she never gave up. +internalcommas_2 They came early, stayed late, and worked hard — yet still missed the deadline. +internalcommas_3 He was tired, hungry, and sore — and still kept running. +internalcommas_4 The team played well, passed accurately, and defended strongly — until the final minutes. +internalcommas_5 I planned, packed, and saved — only to have the trip canceled. +namely_1 There’s one thing I can’t stand — dishonesty. +namely_2 He has one true passion — music. +namely_3 Our goal is clear — success. +namely_4 This is my greatest fear — failure. +namely_5 She only wants one thing — respect. +pauseemphasis_1 The recipe — though simple — is delicious. +pauseemphasis_2 That trip — despite the rain — was unforgettable. +pauseemphasis_3 The meeting — if it happens — could change everything. +pauseemphasis_4 Her response — as expected — was sarcastic. +pauseemphasis_5 The project — while behind schedule — will still be completed. +listinlist_1 We traveled to Paris, France; Rome, Italy; and Berlin, Germany — all in one summer. +listinlist_2 The menu included pasta, salad, and breadsticks; steak, potatoes, and vegetables — everything we could want. +listinlist_3 They visited Chicago, Illinois; Denver, Colorado; and Austin, Texas — and still had time for more. +listinlist_4 The tour covered Madrid, Spain; Lisbon, Portugal; and Dublin, Ireland — a whirlwind trip. +listinlist_5 We met clients from Tokyo, Japan; Seoul, South Korea; and Beijing, China — all in the same week. +daterange_1 Office hours are 9:00 a.m.—5:00 p.m. +daterange_2 The sale runs June 1—June 15. +daterange_3 The conference is scheduled for March 10—March 14. +daterange_4 The course is offered September—December. +daterange_5 The store is open Monday—Saturday. +attribution_1 “The best way out is always through.” — Robert Frost +attribution_2 “Do or do not, there is no try.” — Yoda +attribution_3 “Injustice anywhere is a threat to justice everywhere.” — Martin Luther King Jr. +attribution_4 “I think, therefore I am.” — René Descartes +attribution_5 “The only thing we have to fear is fear itself.” — Franklin D. Roosevelt +punchend_1 I was ready to forgive — until he laughed. +punchend_2 We almost won — if not for that last-minute mistake. +punchend_3 She looked happy — until she saw the bill. +punchend_4 He was about to say yes — when the phone rang. +punchend_5 I thought we were friends — until you lied to me. +' + +echo "Running tests..." + +# Print DATA, strip empty lines, sort by key, then read lines +printf "%s" "$DATA" | awk 'NF' | $SORT_CMD | +while IFS="$(printf '\t')" read -r key text; do + # Safety: skip if no key or text + [ -z "$key" ] && continue + [ -z "$text" ] && continue + + echo "-----------------------------------" + echo "Testing [$key]: $text" + + # Hit API with URL-encoded text + response="$(curl -s -G --data-urlencode "text=$text" "$API_URL")" + + # Pretty-print JSON if jq exists AND response is valid JSON + if command -v jq >/dev/null 2>&1 && printf '%s' "$response" | jq -e . >/dev/null 2>&1; then + printf '%s' "$response" | jq '.original, .result' + else + printf '%s\n' "$response" + if ! command -v jq >/dev/null 2>&1; then + echo "(Install jq for better formatting)" + fi + fi +done + +echo "-----------------------------------" +echo "Tests complete."