</> ScriptJerk Online

Copy paste code, notes, and receipts into dedicated pages.

Drop text here, create a clean reference link, and come back to it later. Simple on purpose: no account wall, no database ceremony, no dashboard maze.

this page so far

php / May 14, 2026 1:02 PM / 41,857 bytes
Raw
<?php
declare(strict_types=1);

$storageDir = __DIR__ . DIRECTORY_SEPARATOR . '_pastes';
$maxBytes = 512 * 1024;

if (!is_dir($storageDir)) {
    mkdir($storageDir, 0755, true);
}

function h(string $value): string {
    return htmlspecialchars($value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}

function slug(): string {
    return bin2hex(random_bytes(5));
}

function paste_path(string $id): string {
    global $storageDir;
    return $storageDir . DIRECTORY_SEPARATOR . $id . '.json';
}

function valid_id(string $id): bool {
    return (bool) preg_match('/^[a-f0-9]{10}$/', $id);
}

function load_paste(string $id): ?array {
    if (!valid_id($id)) {
        return null;
    }

    $path = paste_path($id);
    if (!is_file($path)) {
        return null;
    }

    $data = json_decode((string) file_get_contents($path), true);
    return is_array($data) ? $data : null;
}

function language_options(): array {
    return [
        'text' => 'Text',
        'html' => 'HTML',
        'css' => 'CSS',
        'javascript' => 'JavaScript',
        'php' => 'PHP',
        'python' => 'Python',
        'powershell' => 'PowerShell',
        'markdown' => 'Markdown',
        'json' => 'JSON',
        'sql' => 'SQL',
    ];
}

function language_label(string $language): string {
    $options = language_options();
    return $options[$language] ?? ucfirst($language);
}

function normalize_tags(string $value): array {
    $rawTags = preg_split('/\s*,\s*/', $value) ?: [];
    $tags = [];

    foreach ($rawTags as $tag) {
        $tag = strtolower(trim($tag));
        $tag = ltrim($tag, '#');
        $tag = preg_replace('/[^a-z0-9_-]+/', '-', $tag) ?? '';
        $tag = trim($tag, '-_');

        if ($tag !== '' && !in_array($tag, $tags, true)) {
            $tags[] = mb_substr($tag, 0, 40, 'UTF-8');
        }
    }

    return array_slice($tags, 0, 20);
}

function paste_tags(array $data): array {
    $rawTags = $data['tags'] ?? [];
    if (is_string($rawTags)) {
        return normalize_tags($rawTags);
    }
    if (!is_array($rawTags)) {
        return [];
    }

    return normalize_tags(implode(',', array_map('strval', $rawTags)));
}

function all_pastes(string $pasteDir): array {
    $items = [];
    $files = glob($pasteDir . DIRECTORY_SEPARATOR . '*.json') ?: [];

    foreach ($files as $file) {
        $data = json_decode((string) file_get_contents($file), true);
        if (!is_array($data) || empty($data['id']) || !valid_id((string) $data['id'])) {
            continue;
        }

        $created = strtotime((string) ($data['created_at'] ?? '')) ?: filemtime($file);
        $body = (string) ($data['body'] ?? '');
        $items[] = [
            'id' => (string) $data['id'],
            'title' => (string) ($data['title'] ?? 'Untitled paste'),
            'language' => strtolower((string) ($data['language'] ?? 'text')),
            'tags' => paste_tags($data),
            'body' => $body,
            'created_at' => gmdate('c', $created),
            'created_ts' => $created,
            'bytes' => (int) ($data['bytes'] ?? strlen($body)),
        ];
    }

    return $items;
}

function clamp_choice(string $value, array $allowed, string $fallback): string {
    return in_array($value, $allowed, true) ? $value : $fallback;
}

function current_url_with(array $changes): string {
    $query = $_GET;
    foreach ($changes as $key => $value) {
        if ($value === null || $value === '') {
            unset($query[$key]);
        } else {
            $query[$key] = (string) $value;
        }
    }

    $qs = http_build_query($query);
    return '/list' . ($qs === '' ? '' : '?' . $qs);
}

function paste_preview(string $body, int $limit = 260): string {
    $text = trim(preg_replace('/\s+/', ' ', $body) ?? '');
    if (mb_strlen($text, 'UTF-8') <= $limit) {
        return $text;
    }

    return mb_substr($text, 0, $limit - 3, 'UTF-8') . '...';
}

$error = '';
$createdUrl = '';
$paste = null;
$pasteId = '';
$isList = false;
$listPastes = [];
$filteredPastes = [];
$pagePastes = [];
$typeCounts = [];
$tagCounts = [];
$selectedType = '';
$selectedTag = '';
$selectedSort = 'newest';
$selectedView = 'detailed';
$perPage = 10;
$page = 1;
$totalPages = 1;
$totalPastes = 0;

$path = parse_url((string) ($_SERVER['REQUEST_URI'] ?? '/'), PHP_URL_PATH) ?: '/';
if (preg_match('#^/raw/([a-f0-9]{10})/?$#', $path, $matches)) {
    $paste = load_paste($matches[1]);
    if ($paste === null) {
        http_response_code(404);
        header('Content-Type: text/plain; charset=utf-8');
        echo 'Paste not found.';
        exit;
    }

    header('Content-Type: text/plain; charset=utf-8');
    header('X-Content-Type-Options: nosniff');
    echo (string) $paste['body'];
    exit;
}

if (preg_match('#^/p/([a-f0-9]{10})/?$#', $path, $matches)) {
    $pasteId = $matches[1];
}

if ($path === '/list' || $path === '/list/') {
    $isList = true;
}

if ($_SERVER['REQUEST_METHOD'] === 'POST' && !$isList) {
    $title = trim((string) ($_POST['title'] ?? ''));
    $language = trim((string) ($_POST['language'] ?? 'text'));
    $tags = normalize_tags((string) ($_POST['tags'] ?? ''));
    $body = (string) ($_POST['body'] ?? '');

    if ($title === '') {
        $error = 'Add a title first.';
    } elseif ($body === '') {
        $error = 'Paste something first.';
    } elseif (strlen($body) > $maxBytes) {
        $error = 'That paste is too large. Keep it under 512 KB for now.';
    } else {
        do {
            $pasteId = slug();
            $path = paste_path($pasteId);
        } while (is_file($path));

        $record = [
            'id' => $pasteId,
            'title' => mb_substr($title, 0, 120, 'UTF-8'),
            'language' => mb_substr($language, 0, 40, 'UTF-8'),
            'tags' => $tags,
            'body' => $body,
            'created_at' => gmdate('c'),
            'bytes' => strlen($body),
        ];

        file_put_contents(
            $path,
            json_encode($record, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE),
            LOCK_EX
        );

        $scheme = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
        $host = $_SERVER['HTTP_HOST'] ?? 'scriptjerk.online';
        $createdUrl = $scheme . '://' . $host . '/p/' . $pasteId;
        $paste = $record;
    }
} elseif (!$isList) {
    if ($pasteId === '') {
        $pasteId = (string) ($_GET['p'] ?? '');
    }
    if ($pasteId !== '') {
        $paste = load_paste($pasteId);
        if ($paste === null) {
            http_response_code(404);
            $error = 'Paste not found.';
        }
    }
}

if ($isList) {
    $listPastes = all_pastes($storageDir);
    foreach ($listPastes as $item) {
        $type = (string) $item['language'];
        $typeCounts[$type] = ($typeCounts[$type] ?? 0) + 1;
        foreach ($item['tags'] as $tag) {
            $tagCounts[$tag] = ($tagCounts[$tag] ?? 0) + 1;
        }
    }
    ksort($typeCounts);
    ksort($tagCounts);

    $selectedType = strtolower(trim((string) ($_GET['type'] ?? '')));
    $selectedTag = strtolower(trim((string) ($_GET['tag'] ?? '')));
    $selectedTag = ltrim($selectedTag, '#');
    $selectedSort = clamp_choice((string) ($_GET['sort'] ?? 'newest'), ['newest', 'oldest', 'title', 'type', 'tag'], 'newest');
    $selectedView = clamp_choice((string) ($_GET['view'] ?? 'detailed'), ['detailed', 'simple', 'preview'], 'detailed');
    $perPage = (int) ($_GET['per_page'] ?? 10);
    if (!in_array($perPage, [10, 25, 50, 100], true)) {
        $perPage = 10;
    }

    $filteredPastes = array_values(array_filter($listPastes, static function (array $item) use ($selectedType, $selectedTag): bool {
        $typeMatches = $selectedType === '' || $item['language'] === $selectedType;
        $tagMatches = $selectedTag === '' || in_array($selectedTag, $item['tags'], true);
        return $typeMatches && $tagMatches;
    }));

    usort($filteredPastes, static function (array $a, array $b) use ($selectedSort): int {
        if ($selectedSort === 'oldest') {
            return $a['created_ts'] <=> $b['created_ts'];
        }
        if ($selectedSort === 'title') {
            return strcasecmp((string) $a['title'], (string) $b['title']);
        }
        if ($selectedSort === 'type') {
            return strcasecmp((string) $a['language'], (string) $b['language'])
                ?: ($b['created_ts'] <=> $a['created_ts']);
        }
        if ($selectedSort === 'tag') {
            return strcasecmp((string) ($a['tags'][0] ?? ''), (string) ($b['tags'][0] ?? ''))
                ?: ($b['created_ts'] <=> $a['created_ts']);
        }

        return $b['created_ts'] <=> $a['created_ts'];
    });

    $totalPastes = count($filteredPastes);
    $totalPages = max(1, (int) ceil($totalPastes / $perPage));
    $page = max(1, min((int) ($_GET['page'] ?? 1), $totalPages));
    $offset = ($page - 1) * $perPage;
    $pagePastes = array_slice($filteredPastes, $offset, $perPage);
}

$pageTitle = $isList
    ? 'All Pastes | ScriptJerk Online'
    : ($paste ? ($paste['title'] . ' | ScriptJerk Online') : 'ScriptJerk Online | Copy Paste Pages');
?>
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title><?= h($pageTitle) ?></title>
  <meta name="description" content="ScriptJerk Online is a simple copy-paste reference tool for saving code, notes, and text as dedicated pages.">
  <style>
    :root {
      color-scheme: dark;
      --bg: #0d1013;
      --ink: #f4efe7;
      --muted: #aeb7ba;
      --panel: #171c20;
      --panel-2: #20272c;
      --line: #354047;
      --green: #65d68f;
      --teal: #55d6cf;
      --blue: #78a8ff;
      --amber: #f3bf5a;
      --red: #ff7a70;
      --code: #0a0d0f;
      --shadow: 0 24px 70px rgba(0, 0, 0, .35);
    }

    * {
      box-sizing: border-box;
    }

    html {
      scroll-behavior: smooth;
    }

    body {
      margin: 0;
      min-height: 100vh;
      background:
        radial-gradient(circle at 12% 8%, rgba(85, 214, 207, .11), transparent 31%),
        radial-gradient(circle at 88% 0%, rgba(120, 168, 255, .12), transparent 28%),
        linear-gradient(140deg, #0d1013, #121719 44%, #0b0e10);
      color: var(--ink);
      font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
      letter-spacing: 0;
    }

    a {
      color: inherit;
    }

    .shell {
      width: min(1180px, calc(100% - 32px));
      margin: 0 auto;
      padding: 24px 0 44px;
    }

    header {
      display: grid;
      grid-template-columns: minmax(0, 1fr) auto;
      gap: 18px;
      align-items: end;
      margin-bottom: 24px;
    }

    .brand {
      display: flex;
      align-items: center;
      gap: 11px;
      color: var(--muted);
      font-weight: 800;
      margin-bottom: 28px;
    }

    .mark {
      display: grid;
      place-items: center;
      width: 36px;
      height: 36px;
      border: 1px solid rgba(101, 214, 143, .5);
      border-radius: 8px;
      background: linear-gradient(135deg, rgba(85, 214, 207, .28), rgba(120, 168, 255, .18));
      color: var(--green);
      box-shadow: 0 0 28px rgba(85, 214, 207, .12);
      font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    }

    h1 {
      margin: 0;
      max-width: 820px;
      font-size: clamp(2.15rem, 5vw, 5.1rem);
      line-height: .95;
      letter-spacing: 0;
    }

    .lede {
      max-width: 760px;
      margin: 18px 0 0;
      color: var(--muted);
      font-size: 1.05rem;
      line-height: 1.7;
    }

    .nav {
      display: flex;
      gap: 10px;
      flex-wrap: wrap;
      justify-content: flex-end;
    }

    .btn,
    button {
      display: inline-flex;
      align-items: center;
      justify-content: center;
      min-height: 42px;
      border: 1px solid var(--line);
      border-radius: 8px;
      background: var(--panel-2);
      color: var(--ink);
      padding: 0 14px;
      font: inherit;
      font-weight: 800;
      text-decoration: none;
      cursor: pointer;
    }

    .btn.primary,
    button.primary {
      border-color: rgba(101, 214, 143, .7);
      background: linear-gradient(135deg, var(--green), var(--teal));
      color: #06100d;
    }

    .grid {
      display: grid;
      grid-template-columns: minmax(0, 1fr) 360px;
      gap: 18px;
      align-items: start;
    }

    section,
    aside {
      border: 1px solid var(--line);
      border-radius: 8px;
      background: rgba(23, 28, 32, .9);
      box-shadow: var(--shadow);
    }

    .panel {
      padding: 18px;
    }

    label {
      display: block;
      margin-bottom: 8px;
      color: var(--muted);
      font-size: .82rem;
      font-weight: 900;
      text-transform: uppercase;
    }

    input,
    textarea,
    select {
      width: 100%;
      border: 1px solid var(--line);
      border-radius: 8px;
      background: #0b0f12;
      color: var(--ink);
      padding: 12px 13px;
      font: inherit;
      outline: none;
    }

    input:focus,
    textarea:focus,
    select:focus {
      border-color: var(--teal);
      box-shadow: 0 0 0 3px rgba(85, 214, 207, .14);
    }

    textarea {
      min-height: 430px;
      resize: vertical;
      font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
      line-height: 1.55;
      tab-size: 2;
    }

    .row {
      display: grid;
      grid-template-columns: minmax(0, 1fr) 190px;
      gap: 12px;
      margin-bottom: 14px;
    }

    .actions {
      display: flex;
      gap: 10px;
      flex-wrap: wrap;
      align-items: center;
      margin-top: 14px;
    }

    .note,
    .error,
    .success {
      border-radius: 8px;
      padding: 12px 13px;
      line-height: 1.55;
    }

    .note {
      border: 1px solid var(--line);
      color: var(--muted);
      background: rgba(255, 255, 255, .03);
    }

    .error {
      border: 1px solid rgba(255, 122, 112, .55);
      color: #ffd4d0;
      background: rgba(255, 122, 112, .08);
      margin-bottom: 14px;
    }

    .success {
      border: 1px solid rgba(101, 214, 143, .55);
      color: #d9ffe4;
      background: rgba(101, 214, 143, .08);
      margin-bottom: 14px;
    }

    .linkbox {
      display: grid;
      grid-template-columns: minmax(0, 1fr) auto;
      gap: 8px;
      margin-top: 10px;
    }

    .paste-head {
      display: flex;
      justify-content: space-between;
      gap: 14px;
      align-items: start;
      margin-bottom: 14px;
    }

    .view-actions {
      display: flex;
      gap: 8px;
      flex-wrap: wrap;
      justify-content: flex-end;
    }

    .paste-title {
      margin: 0;
      font-size: 1.3rem;
    }

    .meta {
      color: var(--muted);
      font-size: .9rem;
      line-height: 1.5;
    }

    pre {
      overflow: auto;
      margin: 0;
      border: 1px solid var(--line);
      border-radius: 8px;
      background: var(--code);
      padding: 16px;
      min-height: 360px;
      line-height: 1.55;
      white-space: pre-wrap;
      word-break: break-word;
    }

    code {
      font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
      font-size: .94rem;
    }

    .side-list {
      display: grid;
      gap: 12px;
      margin: 0;
      padding: 0;
      list-style: none;
    }

    .side-list li {
      border-top: 1px solid var(--line);
      padding-top: 12px;
      color: var(--muted);
      line-height: 1.55;
    }

    .side-list li:first-child {
      border-top: 0;
      padding-top: 0;
    }

    footer {
      margin-top: 24px;
      color: var(--muted);
      font-size: .9rem;
    }

    @media (max-width: 880px) {
      header,
      .grid,
      .row {
        grid-template-columns: 1fr;
      }

      .nav {
        justify-content: flex-start;
      }

      .paste-head,
      .view-actions {
        align-items: stretch;
        flex-direction: column;
      }

      textarea {
        min-height: 320px;
      }
    }
  </style>
  <link rel="stylesheet" href="/style.css?v=20260514-tags1">
</head>
<body data-theme="dark">
  <div class="shell">
    <header>
      <div>
        <a class="brand" href="/">
          <span class="mark">&lt;/&gt;</span>
          <span>ScriptJerk Online</span>
        </a>
        <h1>Copy paste code, notes, and receipts into dedicated pages.</h1>
        <p class="lede">Drop text here, create a clean reference link, and come back to it later. Simple on purpose: no account wall, no database ceremony, no dashboard maze.</p>
      </div>
      <nav class="nav" aria-label="Site links">
        <a class="btn" href="/">Create</a>
        <a class="btn" href="/list">Browse</a>
        <a class="btn" href="https://scriptjerk.fun/">Fun</a>
        <a class="btn" href="https://scriptjerk.org/">Brain</a>
        <a class="btn" href="https://scriptjerk.store/">Store</a>
        <label class="theme-picker" for="themeSelect">
          <span>Theme</span>
          <select id="themeSelect" aria-label="Theme">
            <option value="dark">Dark</option>
            <option value="middle">Inbetween</option>
            <option value="light">Light</option>
            <option value="contrast">High Contrast</option>
          </select>
        </label>
      </nav>
    </header>

    <?php if ($error !== ''): ?>
      <div class="error"><?= h($error) ?></div>
    <?php endif; ?>

    <?php if ($createdUrl !== ''): ?>
      <div class="success">
        Saved. This paste now has its own page:
        <div class="linkbox">
          <input id="createdUrl" value="<?= h($createdUrl) ?>" readonly>
          <button type="button" onclick="copyValue('createdUrl')">Copy</button>
        </div>
      </div>
    <?php endif; ?>

    <?php if ($isList): ?>
      <main class="list-layout">
        <section class="panel list-panel" aria-label="Paste list">
          <div class="list-head">
            <div>
              <h2 class="paste-title">Saved pastes</h2>
              <div class="meta">
                <?= number_format($totalPastes) ?> shown /
                <?= number_format(count($listPastes)) ?> total
              </div>
            </div>
            <a class="btn primary" href="/">New Paste</a>
          </div>

          <form class="list-controls" method="get" action="/list">
            <div>
              <label for="type">Type</label>
              <select id="type" name="type">
                <option value="">All types</option>
                <?php foreach ($typeCounts as $type => $count): ?>
                  <option value="<?= h((string) $type) ?>" <?= $selectedType === $type ? 'selected' : '' ?>>
                    <?= h(language_label((string) $type)) ?> (<?= number_format((int) $count) ?>)
                  </option>
                <?php endforeach; ?>
              </select>
            </div>
            <div>
              <label for="tag">Tag</label>
              <select id="tag" name="tag">
                <option value="">All tags</option>
                <?php foreach ($tagCounts as $tag => $count): ?>
                  <option value="<?= h((string) $tag) ?>" <?= $selectedTag === $tag ? 'selected' : '' ?>>
                    #<?= h((string) $tag) ?> (<?= number_format((int) $count) ?>)
                  </option>
                <?php endforeach; ?>
              </select>
            </div>
            <div>
              <label for="sort">Sort</label>
              <select id="sort" name="sort">
                <option value="newest" <?= $selectedSort === 'newest' ? 'selected' : '' ?>>Newest first</option>
                <option value="oldest" <?= $selectedSort === 'oldest' ? 'selected' : '' ?>>Oldest first</option>
                <option value="title" <?= $selectedSort === 'title' ? 'selected' : '' ?>>Title A-Z</option>
                <option value="type" <?= $selectedSort === 'type' ? 'selected' : '' ?>>Type A-Z</option>
                <option value="tag" <?= $selectedSort === 'tag' ? 'selected' : '' ?>>First tag A-Z</option>
              </select>
            </div>
            <div>
              <label for="view">View</label>
              <select id="view" name="view">
                <option value="detailed" <?= $selectedView === 'detailed' ? 'selected' : '' ?>>Detailed list</option>
                <option value="simple" <?= $selectedView === 'simple' ? 'selected' : '' ?>>Simple list</option>
                <option value="preview" <?= $selectedView === 'preview' ? 'selected' : '' ?>>Simple preview</option>
              </select>
            </div>
            <div>
              <label for="per_page">Per page</label>
              <select id="per_page" name="per_page">
                <?php foreach ([10, 25, 50, 100] as $choice): ?>
                  <option value="<?= $choice ?>" <?= $perPage === $choice ? 'selected' : '' ?>><?= $choice ?></option>
                <?php endforeach; ?>
              </select>
            </div>
            <div class="control-action">
              <button class="primary" type="submit">Apply</button>
            </div>
          </form>

          <?php if ($totalPastes === 0): ?>
            <div class="empty-state">
              <h2 class="paste-title">No pastes found</h2>
              <p class="meta">Create a paste first, or clear the current type or tag filter.</p>
            </div>
          <?php else: ?>
            <div class="paste-list paste-list-<?= h($selectedView) ?>">
              <?php foreach ($pagePastes as $item): ?>
                <article class="paste-item">
                  <div class="paste-item-main">
                    <a class="paste-item-title" href="/p/<?= h((string) $item['id']) ?>">
                      <?= h((string) $item['title']) ?>
                    </a>
                    <?php if ($selectedView !== 'simple'): ?>
                      <div class="meta">
                        <?= h(language_label((string) $item['language'])) ?> /
                        <?= h(date('M j, Y g:i A', (int) $item['created_ts'])) ?> /
                        <?= number_format((int) $item['bytes']) ?> bytes
                      </div>
                      <?php if ($item['tags'] !== []): ?>
                        <div class="tag-row">
                          <?php foreach ($item['tags'] as $tag): ?>
                            <a class="tag-chip" href="<?= h(current_url_with(['tag' => $tag, 'page' => 1])) ?>">#<?= h((string) $tag) ?></a>
                          <?php endforeach; ?>
                        </div>
                      <?php endif; ?>
                    <?php endif; ?>
                    <?php if ($selectedView === 'preview'): ?>
                      <p class="paste-preview"><?= h(paste_preview((string) $item['body'])) ?></p>
                    <?php endif; ?>
                  </div>
                  <div class="paste-item-actions">
                    <span class="type-pill"><?= h(language_label((string) $item['language'])) ?></span>
                    <?php if ($selectedView === 'simple' && $item['tags'] !== []): ?>
                      <span class="type-pill">#<?= h((string) $item['tags'][0]) ?></span>
                    <?php endif; ?>
                    <a class="btn" href="/raw/<?= h((string) $item['id']) ?>">Raw</a>
                  </div>
                </article>
              <?php endforeach; ?>
            </div>

            <nav class="pager" aria-label="Paste pages">
              <a class="btn <?= $page <= 1 ? 'disabled' : '' ?>" href="<?= h($page <= 1 ? '#' : current_url_with(['page' => $page - 1])) ?>">Previous</a>
              <span class="page-status">Page <?= number_format($page) ?> of <?= number_format($totalPages) ?></span>
              <a class="btn <?= $page >= $totalPages ? 'disabled' : '' ?>" href="<?= h($page >= $totalPages ? '#' : current_url_with(['page' => $page + 1])) ?>">Next</a>
            </nav>
          <?php endif; ?>
        </section>
      </main>
    <?php else: ?>
    <main class="grid">
      <section class="panel" aria-label="<?= $paste ? 'Paste viewer' : 'Create paste' ?>">
        <?php if ($paste): ?>
          <div class="paste-head">
            <div>
              <h2 class="paste-title"><?= h((string) $paste['title']) ?></h2>
              <div class="meta">
                <?= h((string) ($paste['language'] ?? 'text')) ?> /
                <?= h(date('M j, Y g:i A', strtotime((string) $paste['created_at']))) ?> /
                <?= number_format((int) ($paste['bytes'] ?? 0)) ?> bytes
              </div>
              <?php $detailTags = paste_tags($paste); ?>
              <?php if ($detailTags !== []): ?>
                <div class="tag-row">
                  <?php foreach ($detailTags as $tag): ?>
                    <a class="tag-chip" href="/list?tag=<?= h(urlencode($tag)) ?>">#<?= h($tag) ?></a>
                  <?php endforeach; ?>
                </div>
              <?php endif; ?>
            </div>
            <div class="view-actions">
              <label class="code-picker" for="codeThemeSelect">
                <span>Code View</span>
                <select id="codeThemeSelect" aria-label="Code view">
                  <option value="auto">Auto</option>
                  <option value="text">Plain Text</option>
                  <option value="javascript">JavaScript</option>
                  <option value="javascript:contrast">JavaScript High Vis</option>
                  <option value="python">Python</option>
                  <option value="python:contrast">Python High Vis</option>
                  <option value="html">HTML5</option>
                  <option value="html:contrast">HTML5 High Vis</option>
                  <option value="css">CSS</option>
                  <option value="css:contrast">CSS High Vis</option>
                  <option value="php">PHP</option>
                  <option value="php:contrast">PHP High Vis</option>
                  <option value="powershell">PowerShell</option>
                  <option value="powershell:contrast">PowerShell High Vis</option>
                  <option value="json">JSON</option>
                  <option value="json:contrast">JSON High Vis</option>
                  <option value="sql">SQL</option>
                  <option value="sql:contrast">SQL High Vis</option>
                  <option value="markdown">Markdown</option>
                  <option value="markdown:contrast">Markdown High Vis</option>
                </select>
              </label>
              <a class="btn" href="/raw/<?= h((string) $paste['id']) ?>">Raw</a>
              <button type="button" onclick="copyText('pasteBody')">Copy Text</button>
            </div>
          </div>
          <pre id="pasteBody" data-language="<?= h((string) ($paste['language'] ?? 'text')) ?>"><code><?= h((string) $paste['body']) ?></code></pre>
        <?php else: ?>
          <form method="post" action="/">
            <div class="row">
              <div>
                <label for="title">Title</label>
                <input id="title" name="title" maxlength="120" required placeholder="Example: VPS deploy notes">
              </div>
              <div>
                <label for="language">Type</label>
                <select id="language" name="language">
                  <option value="text">Text</option>
                  <option value="html">HTML</option>
                  <option value="css">CSS</option>
                  <option value="javascript">JavaScript</option>
                  <option value="php">PHP</option>
                  <option value="python">Python</option>
                  <option value="powershell">PowerShell</option>
                  <option value="markdown">Markdown</option>
                  <option value="json">JSON</option>
                  <option value="sql">SQL</option>
                </select>
              </div>
            </div>

            <label for="tagInput">Tags</label>
            <div class="tag-editor" id="tagEditor">
              <div class="tag-stack" id="tagStack" aria-live="polite"></div>
              <input id="tagInput" autocomplete="off" placeholder="Type hashtags, then comma">
            </div>
            <input type="hidden" id="tags" name="tags">
            <p class="field-help">Use comma or comma + space to lock in tags. Example: html, landing-page, vps</p>

            <label for="body">Paste</label>
            <textarea id="body" name="body" required spellcheck="false" placeholder="Paste code, notes, prompts, commands, or anything you want to reference later."></textarea>

            <div class="actions">
              <button class="primary" type="submit">Create Page</button>
              <button type="button" onclick="clearPaste()">Clear</button>
            </div>
          </form>
        <?php endif; ?>
      </section>

      <aside class="panel">
        <h2 class="paste-title">How this works</h2>
        <ul class="side-list">
          <li>Every save creates a random private-by-obscurity link like <code>/p/a1b2c3d4e5</code>.</li>
          <li>Storage is file-based, so it is easy to back up or move later.</li>
          <li>Pastes are escaped before display, so HTML and scripts show as text instead of running.</li>
          <li>This is for useful reference material. Do not paste passwords, private keys, bank info, or anything that would hurt if shared.</li>
        </ul>
      </aside>
    </main>
    <?php endif; ?>

    <footer>
      <span>ScriptJerk Online is the quick-reference shelf for code, notes, snippets, and project evidence.</span>
      <span class="donate-line">
        Donate SOL:
        <code id="solAddress">3aVj5JcfXvvWpfLCfuXrcQPaZp36r6K35HG34k3FTsZV</code>
        <button class="mini-btn" type="button" onclick="copyValue('solAddress')">Copy</button>
      </span>
    </footer>
  </div>

  <script>
    const themeSelect = document.getElementById('themeSelect');
    const savedTheme = localStorage.getItem('scriptjerk-online-theme') || 'dark';

    function setTheme(theme) {
      const allowed = ['dark', 'middle', 'light', 'contrast'];
      const nextTheme = allowed.includes(theme) ? theme : 'dark';
      document.body.dataset.theme = nextTheme;
      localStorage.setItem('scriptjerk-online-theme', nextTheme);
      if (themeSelect) themeSelect.value = nextTheme;
    }

    setTheme(savedTheme);

    if (themeSelect) {
      themeSelect.addEventListener('change', (event) => {
        setTheme(event.target.value);
      });
    }

    const languageAliases = {
      html5: 'html',
      js: 'javascript',
      ps1: 'powershell',
      md: 'markdown'
    };

    const keywordSets = {
      javascript: ['async', 'await', 'break', 'case', 'catch', 'class', 'const', 'continue', 'default', 'delete', 'do', 'else', 'export', 'extends', 'false', 'finally', 'for', 'from', 'function', 'if', 'import', 'in', 'instanceof', 'let', 'new', 'null', 'return', 'static', 'super', 'switch', 'this', 'throw', 'true', 'try', 'typeof', 'undefined', 'var', 'void', 'while', 'yield'],
      php: ['array', 'as', 'break', 'case', 'catch', 'class', 'const', 'continue', 'declare', 'default', 'echo', 'else', 'elseif', 'extends', 'false', 'finally', 'for', 'foreach', 'function', 'global', 'if', 'implements', 'interface', 'namespace', 'new', 'null', 'private', 'protected', 'public', 'require', 'return', 'static', 'switch', 'throw', 'trait', 'true', 'try', 'use', 'while'],
      python: ['and', 'as', 'assert', 'async', 'await', 'break', 'class', 'continue', 'def', 'elif', 'else', 'except', 'False', 'finally', 'for', 'from', 'global', 'if', 'import', 'in', 'is', 'lambda', 'None', 'nonlocal', 'not', 'or', 'pass', 'raise', 'return', 'True', 'try', 'while', 'with', 'yield'],
      powershell: ['Begin', 'Break', 'Catch', 'Class', 'Continue', 'Data', 'Do', 'DynamicParam', 'Else', 'ElseIf', 'End', 'Exit', 'Filter', 'Finally', 'For', 'ForEach', 'From', 'Function', 'If', 'In', 'Param', 'Process', 'Return', 'Switch', 'Throw', 'Trap', 'Try', 'Until', 'Using', 'Var', 'While'],
      css: ['align-items', 'background', 'border', 'box-shadow', 'color', 'display', 'font', 'gap', 'grid', 'height', 'justify-content', 'margin', 'max-width', 'min-height', 'padding', 'position', 'text-decoration', 'transform', 'transition', 'width'],
      sql: ['ALTER', 'AND', 'AS', 'ASC', 'BETWEEN', 'BY', 'CREATE', 'DELETE', 'DESC', 'DROP', 'FROM', 'GROUP', 'HAVING', 'IN', 'INSERT', 'INTO', 'JOIN', 'LEFT', 'LIKE', 'LIMIT', 'NOT', 'NULL', 'OR', 'ORDER', 'RIGHT', 'SELECT', 'SET', 'TABLE', 'UPDATE', 'VALUES', 'WHERE'],
      json: ['true', 'false', 'null']
    };

    function escapeHtml(value) {
      return value
        .replace(/&/g, '&amp;')
        .replace(/</g, '&lt;')
        .replace(/>/g, '&gt;')
        .replace(/"/g, '&quot;')
        .replace(/'/g, '&#039;');
    }

    function tokenSpan(type, value) {
      return `<span class="tok-${type}">${value}</span>`;
    }

    function highlightKeywords(html, language) {
      const words = keywordSets[language] || [];
      if (words.length === 0) return html;
      const pattern = new RegExp(`\\b(${words.map((word) => word.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|')})\\b`, language === 'sql' ? 'g' : 'gi');
      return html
        .split(/(<span class="tok-[^"]+">[\s\S]*?<\/span>|<[^>]+>)/g)
        .map((chunk) => chunk.startsWith('<') ? chunk : chunk.replace(pattern, (match) => tokenSpan('keyword', match)))
        .join('');
    }

    function highlightCode(text, language) {
      let html = escapeHtml(text);
      const requestedLang = String(language || 'text').toLowerCase();
      const lang = languageAliases[requestedLang] || requestedLang;

      if (lang === 'text') return html;

      if (lang === 'html') {
        html = html.replace(/(&lt;!--[\s\S]*?--&gt;)/g, (_, value) => tokenSpan('comment', value));
        html = html.replace(/(&lt;\/?)([a-zA-Z][\w:-]*)([\s\S]*?)(\/?&gt;)/g, (_, open, tag, attrs, close) => {
          const attrHtml = attrs.replace(/([\w:-]+)(=)(&quot;.*?&quot;|&#039;.*?&#039;|[^\s&]+)/g, (_m, name, eq, value) => `${tokenSpan('attr', name)}${eq}${tokenSpan('string', value)}`);
          return `${tokenSpan('punct', open)}${tokenSpan('tag', tag)}${attrHtml}${tokenSpan('punct', close)}`;
        });
        return html;
      }

      if (lang === 'css') {
        html = html.replace(/(\/\*[\s\S]*?\*\/)/g, (_, value) => tokenSpan('comment', value));
        html = html.replace(/([.#]?[a-zA-Z][\w-]*)(\s*\{)/g, (_, selector, brace) => `${tokenSpan('selector', selector)}${brace}`);
        html = html.replace(/([\w-]+)(\s*:)/g, (_, prop, colon) => `${tokenSpan('property', prop)}${colon}`);
        html = html.replace(/(#(?:[0-9a-fA-F]{3}){1,2}\b|rgba?\([^)]+\)|hsla?\([^)]+\))/g, (_, value) => tokenSpan('number', value));
        return highlightKeywords(html, lang);
      }

      if (lang === 'json') {
        html = html.replace(/(&quot;[^&]*?&quot;)(\s*:)/g, (_, key, colon) => `${tokenSpan('property', key)}${colon}`);
        html = html.replace(/(:\s*)(&quot;[^&]*?&quot;)/g, (_, colon, value) => `${colon}${tokenSpan('string', value)}`);
        html = html.replace(/\b(-?\d+(?:\.\d+)?)\b/g, (_, value) => tokenSpan('number', value));
        return highlightKeywords(html, lang);
      }

      html = html.replace(/(&quot;.*?&quot;|&#039;.*?&#039;|`.*?`)/g, (_, value) => tokenSpan('string', value));
      html = html.replace(/\b(-?\d+(?:\.\d+)?)\b/g, (_, value) => tokenSpan('number', value));

      if (['javascript', 'php', 'python', 'powershell'].includes(lang)) {
        html = html.replace(/(\/\/.*?$|#.*?$|\/\*[\s\S]*?\*\/)/gm, (_, value) => tokenSpan('comment', value));
        html = html.replace(/(\$[a-zA-Z_]\w*)/g, (_, value) => tokenSpan('variable', value));
        return highlightKeywords(html, lang);
      }

      if (lang === 'sql') {
        html = html.replace(/(--.*?$|\/\*[\s\S]*?\*\/)/gm, (_, value) => tokenSpan('comment', value));
        return highlightKeywords(html, lang);
      }

      if (lang === 'markdown') {
        html = html.replace(/^(#{1,6}\s.*)$/gm, (_, value) => tokenSpan('keyword', value));
        html = html.replace(/(\*\*[^*]+\*\*|__[^_]+__)/g, (_, value) => tokenSpan('strong', value));
        html = html.replace(/(`[^`]+`)/g, (_, value) => tokenSpan('string', value));
      }

      return html;
    }

    function applyCodeView(block, value) {
      const code = block.querySelector('code');
      if (!code) return false;

      const originalText = block.dataset.sourceText || code.innerText;
      block.dataset.sourceText = originalText;

      const savedLanguage = block.dataset.savedLanguage || block.dataset.language || 'text';
      const [selectedLanguage, selectedMode] = String(value || 'auto').split(':');
      const resolvedLanguage = selectedLanguage === 'auto' ? savedLanguage : selectedLanguage;

      block.dataset.language = resolvedLanguage;
      block.dataset.codeMode = selectedMode === 'contrast' ? 'contrast' : 'normal';
      code.innerHTML = highlightCode(originalText, resolvedLanguage);
      block.classList.add('is-highlighted');
      return true;
    }

    document.querySelectorAll('pre[data-language]').forEach((block) => {
      block.dataset.savedLanguage = block.dataset.language || 'text';
      applyCodeView(block, 'auto');
    });

    const codeThemeSelect = document.getElementById('codeThemeSelect');
    const pasteBlock = document.getElementById('pasteBody');
    if (codeThemeSelect && pasteBlock) {
      codeThemeSelect.addEventListener('change', (event) => {
        applyCodeView(pasteBlock, event.target.value);
      });
    }

    const tagInput = document.getElementById('tagInput');
    const tagStack = document.getElementById('tagStack');
    const tagsField = document.getElementById('tags');
    const activeTags = [];

    function cleanTag(value) {
      return String(value || '')
        .trim()
        .replace(/^#+/, '')
        .toLowerCase()
        .replace(/[^a-z0-9_-]+/g, '-')
        .replace(/^[-_]+|[-_]+$/g, '')
        .slice(0, 40);
    }

    function syncTags() {
      if (!tagsField || !tagStack) return;
      tagsField.value = activeTags.join(', ');
      tagStack.innerHTML = '';

      activeTags.forEach((tag) => {
        const chip = document.createElement('span');
        chip.className = 'tag-chip';
        chip.textContent = `#${tag}`;

        const remove = document.createElement('button');
        remove.type = 'button';
        remove.setAttribute('aria-label', `Remove ${tag}`);
        remove.textContent = 'x';
        remove.addEventListener('click', () => {
          const index = activeTags.indexOf(tag);
          if (index >= 0) activeTags.splice(index, 1);
          syncTags();
          tagInput?.focus();
        });

        chip.appendChild(remove);
        tagStack.appendChild(chip);
      });
    }

    function addTag(value) {
      const tag = cleanTag(value);
      if (tag && !activeTags.includes(tag) && activeTags.length < 20) {
        activeTags.push(tag);
      }
      if (tagInput) tagInput.value = '';
      syncTags();
    }

    if (tagInput) {
      tagInput.addEventListener('keydown', (event) => {
        if (event.key === ',') {
          event.preventDefault();
          addTag(tagInput.value);
        } else if (event.key === 'Enter' && tagInput.value.trim() !== '') {
          event.preventDefault();
          addTag(tagInput.value);
        } else if (event.key === 'Backspace' && tagInput.value === '' && activeTags.length > 0) {
          activeTags.pop();
          syncTags();
        }
      });

      tagInput.addEventListener('input', () => {
        if (tagInput.value.includes(',')) {
          const parts = tagInput.value.split(',');
          parts.slice(0, -1).forEach(addTag);
          tagInput.value = parts[parts.length - 1].trimStart();
        }
      });

      tagInput.form?.addEventListener('submit', () => {
        if (tagInput.value.trim() !== '') {
          addTag(tagInput.value);
        }
        syncTags();
      });
    }

    async function copyValue(id) {
      const input = document.getElementById(id);
      await navigator.clipboard.writeText(input.value);
      input.select();
    }

    async function copyText(id) {
      const node = document.getElementById(id);
      await navigator.clipboard.writeText(node.innerText);
    }

    function clearPaste() {
      const body = document.getElementById('body');
      if (body) body.value = '';
      activeTags.splice(0, activeTags.length);
      if (tagInput) tagInput.value = '';
      syncTags();
    }
  </script>
</body>
</html>