Simple Search

Website Keyword Search Engine Script

Simple Search (v1.0) — Matt Wright's site search engine that indexed and searched plain text files on a web server. It scanned specified directories for keyword matches and returned results with page titles extracted from HTML <title> tags.

Free Download
Version 1.0
Perl CGI

Modern Search Solutions

Simple Search read files on every request — acceptable for a 50-page site in 1997, unusable at any real scale. Modern tools pre-build indexes and return results in under 50 ms.

Solution Type Best For Typo Tolerance Cost
Algolia SaaS E-commerce, apps with instant search Yes Free 10K req/mo, then ~$1/1K
Elasticsearch Self-hosted / Cloud Large datasets, log analytics, full-text search Yes Free (self-hosted), Cloud from $95/mo
Meilisearch Self-hosted / Cloud Apps, e-commerce (Algolia alternative) Yes Free (self-hosted), Cloud from $0
Typesense Self-hosted / Cloud High-traffic apps, faceted search Yes Free (self-hosted), $19+/mo cloud
Lunr.js Client-side JS (~8 KB) Static sites, offline-capable search Basic (stemming) Free (MIT)
Pagefind Static build + JS (~10 KB) Hugo, Jekyll, Astro — zero server Basic Free

For a small static site (under 500 pages), Lunr.js or Pagefind works well. For anything dynamic or over 10K documents, Meilisearch, Typesense, or Algolia are the practical choices.

Overview

Simple Search is a Perl CGI script that provides keyword search for static HTML and text files. On each request, it reads through files in the configured directories, matches keywords, and returns a list of links using the <title> tag of each matching document.

How It Works

The script opens every file in the specified directories, converts content to lowercase, and tests each search term against it. Boolean AND requires all terms to match; OR requires at least one. Results are displayed as a flat list of page-title links.

Package Contents
File Description
search.pl Main Perl script that performs the search and displays results
search.html HTML form template for the search interface
README Installation instructions and configuration guide
Search Demo

Features

Keyword Search

Search for single keywords or phrases across all specified documents on your website.

Boolean Operators

AND/OR operators to combine multiple keywords for refined results.

Title Extraction

Automatically extracts page titles from HTML documents to display meaningful result links.

Multiple Directories

Configure multiple directories to search, covering your entire site structure.

Text & HTML Support

Searches both plain text (.txt) and HTML files (.html, .htm).

Customizable Output

Modify the results page template to match your website's design.

Boolean Search

Option Description Example
Single Keyword Search for a single word in all documents perl
Multiple Keywords (AND) Find documents containing ALL specified keywords perl cgi script
Multiple Keywords (OR) Find documents containing ANY of the keywords perl OR php OR python
Case Insensitive All searches are case-insensitive by default PERL = perl = Perl

Installation

  1. Upload the Script
    Upload search.pl to your cgi-bin directory.
  2. Configure Perl Path
    Edit the first line to point to your Perl interpreter (usually #!/usr/bin/perl).
  3. Set Search Directories
    Configure the @directories array with the paths to directories you want to search.
  4. Set Base URL
    Configure the $baseurl variable to match your website's URL structure.
  5. Set Permissions
    Set the script permissions to 755: chmod 755 search.pl
  6. Create Search Form
    Upload or customize search.html to create your search interface.

Code Examples

Search Form HTML
<!DOCTYPE html>
<html>
<head>
    <title>Search Our Site</title>
</head>
<body>
    <h1>Search</h1>

    <form action="/cgi-bin/search.pl" method="GET">
        <p>
            <label for="keywords">Enter Keywords:</label><br>
            <input type="text" name="keywords" id="keywords" size="40">
        </p>

        <p>
            Search Type:<br>
            <input type="radio" name="boolean" value="AND" id="and" checked>
            <label for="and">Match ALL keywords (AND)</label><br>

            <input type="radio" name="boolean" value="OR" id="or">
            <label for="or">Match ANY keyword (OR)</label>
        </p>

        <p>
            <input type="submit" value="Search">
            <input type="reset" value="Clear">
        </p>
    </form>

    <!-- Bootstrap 5 version -->
    <form action="/cgi-bin/search.pl" method="GET" class="needs-validation">
        <div class="mb-3">
            <label for="keywords" class="form-label">Enter Keywords</label>
            <input type="text" class="form-control" name="keywords" id="keywords"
                   placeholder="Search..." required>
        </div>

        <div class="mb-3">
            <label class="form-label">Search Type</label>
            <div class="form-check">
                <input class="form-check-input" type="radio" name="boolean"
                       value="AND" id="and" checked>
                <label class="form-check-label" for="and">
                    Match ALL keywords (AND)
                </label>
            </div>
            <div class="form-check">
                <input class="form-check-input" type="radio" name="boolean"
                       value="OR" id="or">
                <label class="form-check-label" for="or">
                    Match ANY keyword (OR)
                </label>
            </div>
        </div>

        <button type="submit" class="btn btn-primary">
            <i class="bi bi-search"></i> Search
        </button>
    </form>
</body>
</html>
Perl Implementation
#!/usr/bin/perl
use strict;
use warnings;
use CGI;
use File::Find;

my $cgi = CGI->new;

# Configuration
my @directories = ('/var/www/html/docs', '/var/www/html/pages');
my $baseurl = 'http://example.com';
my @extensions = qw(html htm txt);

# Get search parameters
my $keywords = $cgi->param('keywords') || '';
my $boolean = $cgi->param('boolean') || 'AND';

# Security: sanitize input
$keywords =~ s/[^\w\s]//g;
my @terms = split(/\s+/, lc($keywords));

# Output HTML header
print $cgi->header('text/html');
print $cgi->start_html('Search Results');
print "

Search Results

\n"; if (!@terms) { print "

Please enter search keywords.

\n"; print $cgi->end_html; exit; } print "

Searching for: $keywords ($boolean)

\n"; # Search files my @results; find(sub { return unless -f; my $file = $File::Find::name; # Check extension my ($ext) = $file =~ /\.(\w+)$/; return unless $ext && grep { $_ eq lc($ext) } @extensions; # Read file content open(my $fh, '<', $file) or return; my $content = do { local $/; <$fh> }; close($fh); $content = lc($content); # Check for matches my $match = 0; if ($boolean eq 'AND') { $match = 1; for my $term (@terms) { unless ($content =~ /\b\Q$term\E\b/i) { $match = 0; last; } } } else { # OR for my $term (@terms) { if ($content =~ /\b\Q$term\E\b/i) { $match = 1; last; } } } if ($match) { # Extract title my ($title) = $content =~ /([^<]+)<\/title>/i; <meta name="twitter:card" content="summary"> $title ||= $file; # Convert path to URL my $url = $file; $url =~ s{^/var/www/html}{$baseurl}; push @results, { title => $title, url => $url }; } }, @directories); # Display results if (@results) { print "<p>Found " . scalar(@results) . " result(s):</p>\n"; print "<ul>\n"; for my $result (@results) { print qq{<li><a href="$result->{url}">$result->{title}</a></li>\n}; } print "</ul>\n"; } else { print "<p>No results found.</p>\n"; } print $cgi->end_html; exit 0;</code></pre> </div> <div class="tab-pane fade" id="php" role="tabpanel"> <h5>PHP Implementation</h5> <pre><code class="language-php"><?php /** * Simple Search - PHP Version */ // Configuration $config = [ 'directories' => [ '/var/www/html/docs', '/var/www/html/pages' ], 'baseurl' => 'https://example.com', 'extensions' => ['html', 'htm', 'txt', 'php'], 'max_results' => 100 ]; // Get search parameters $keywords = $_GET['keywords'] ?? ''; $boolean = $_GET['boolean'] ?? 'AND'; // Security: sanitize input $keywords = preg_replace('/[^\w\s]/u', '', $keywords); $terms = array_filter(explode(' ', strtolower($keywords))); function searchFiles($directories, $extensions, $baseurl) { $files = []; foreach ($directories as $dir) { if (!is_dir($dir)) continue; $iterator = new RecursiveIteratorIterator( new RecursiveDirectoryIterator($dir) ); foreach ($iterator as $file) { if (!$file->isFile()) continue; $ext = strtolower($file->getExtension()); if (!in_array($ext, $extensions)) continue; $files[] = [ 'path' => $file->getPathname(), 'url' => str_replace('/var/www/html', $baseurl, $file->getPathname()) ]; } } return $files; } function extractTitle($content, $fallback) { if (preg_match('/<title>([^<]+)<\/title>/i', $content, $matches)) { <meta name="twitter:card" content="summary"> return htmlspecialchars($matches[1]); } return htmlspecialchars(basename($fallback)); } function matchesSearch($content, $terms, $boolean) { $content = strtolower($content); if ($boolean === 'AND') { foreach ($terms as $term) { if (stripos($content, $term) === false) { return false; } } return true; } else { // OR foreach ($terms as $term) { if (stripos($content, $term) !== false) { return true; } } return false; } } // Perform search $results = []; if (!empty($terms)) { $files = searchFiles( $config['directories'], $config['extensions'], $config['baseurl'] ); foreach ($files as $file) { $content = @file_get_contents($file['path']); if ($content === false) continue; if (matchesSearch($content, $terms, $boolean)) { $results[] = [ 'title' => extractTitle($content, $file['path']), 'url' => $file['url'] ]; if (count($results) >= $config['max_results']) { break; } } } } ?> <!DOCTYPE html> <html> <head> <title>Search Results</title> </head> <body> <h1>Search Results</h1> <?php if (empty($terms)): ?> <p>Please enter search keywords.</p> <?php else: ?> <p>Searching for: <strong><?= htmlspecialchars($keywords) ?></strong> (<?= $boolean ?>)</p> <?php if (!empty($results)): ?> <p>Found <?= count($results) ?> result(s):</p> <ul> <?php foreach ($results as $result): ?> <li> <a href="<?= htmlspecialchars($result['url']) ?>"> <?= $result['title'] ?> </a> </li> <?php endforeach; ?> </ul> <?php else: ?> <p>No results found.</p> <?php endif; ?> <?php endif; ?> </body> </html></code></pre> </div> <div class="tab-pane fade" id="js" role="tabpanel"> <h5>JavaScript Client-Side Search</h5> <pre><code class="language-javascript">/** * Simple Search - JavaScript Version * Client-side search for static sites (requires pre-built index) */ class SimpleSearch { constructor(options = {}) { this.options = { indexUrl: '/search-index.json', inputSelector: '#search-input', resultsSelector: '#search-results', minChars: 2, maxResults: 20, highlightMatches: true, ...options }; this.index = []; this.init(); } async init() { await this.loadIndex(); this.bindEvents(); } async loadIndex() { try { const response = await fetch(this.options.indexUrl); this.index = await response.json(); } catch (error) { console.error('Failed to load search index:', error); } } bindEvents() { const input = document.querySelector(this.options.inputSelector); if (input) { input.addEventListener('input', (e) => this.handleSearch(e.target.value)); // Handle form submission input.closest('form')?.addEventListener('submit', (e) => { e.preventDefault(); this.handleSearch(input.value); }); } } handleSearch(query) { if (query.length < this.options.minChars) { this.displayResults([]); return; } const results = this.search(query); this.displayResults(results, query); } search(query) { const terms = query.toLowerCase().split(/\s+/).filter(t => t.length > 0); if (terms.length === 0) return []; return this.index .map(item => ({ ...item, score: this.calculateScore(item, terms) })) .filter(item => item.score > 0) .sort((a, b) => b.score - a.score) .slice(0, this.options.maxResults); } calculateScore(item, terms) { let score = 0; const titleLower = item.title.toLowerCase(); const contentLower = (item.content || '').toLowerCase(); for (const term of terms) { // Title matches are worth more if (titleLower.includes(term)) { score += 10; // Exact word match in title if (new RegExp(`\\b${term}\\b`).test(titleLower)) { score += 5; } } // Content matches const contentMatches = (contentLower.match(new RegExp(term, 'g')) || []).length; score += Math.min(contentMatches, 5); // Cap at 5 points per term } return score; } displayResults(results, query = '') { const container = document.querySelector(this.options.resultsSelector); if (!container) return; if (results.length === 0) { container.innerHTML = query.length >= this.options.minChars ? '<p class="text-muted">No results found.</p>' : ''; return; } const html = results.map(result => { let title = result.title; let snippet = result.snippet || ''; if (this.options.highlightMatches && query) { const terms = query.split(/\s+/); terms.forEach(term => { const regex = new RegExp(`(${term})`, 'gi'); title = title.replace(regex, '<mark>$1</mark>'); snippet = snippet.replace(regex, '<mark>$1</mark>'); }); } return ` <div class="search-result mb-3"> <h5><a href="${result.url}">${title}</a></h5> <p class="text-muted small mb-1">${result.url}</p> <p class="mb-0">${snippet}</p> </div> `; }).join(''); container.innerHTML = ` <p class="text-muted">Found ${results.length} result(s):</p> ${html} `; } } // Usage const search = new SimpleSearch({ indexUrl: '/search-index.json', inputSelector: '#search-input', resultsSelector: '#search-results' }); // Building a search index (Node.js build script example) /* const fs = require('fs'); const path = require('path'); const cheerio = require('cheerio'); function buildIndex(directory) { const index = []; const files = walkDir(directory); files.forEach(file => { if (!file.endsWith('.html')) return; const content = fs.readFileSync(file, 'utf-8'); const $ = cheerio.load(content); index.push({ url: file.replace(directory, ''), title: $('title').text() || path.basename(file), content: $('body').text().replace(/\s+/g, ' ').slice(0, 500), snippet: $('meta[name="description"]').attr('content') || '' }); }); return index; } fs.writeFileSync('search-index.json', JSON.stringify(buildIndex('./public'))); */</code></pre> </div> </div> </section> <!-- Download Section --> <section id="download" class="mb-5"> <h2 class="border-bottom pb-2 mb-4"><i class="bi bi-download"></i> Download</h2> <div class="row g-4"> <div class="col-md-6"> <div class="card h-100"> <div class="card-header bg-primary text-white"> <i class="bi bi-file-earmark-zip"></i> Compressed Archives </div> <div class="card-body"> <ul class="list-group list-group-flush"> <li class="list-group-item d-flex justify-content-between align-items-center"> <span><i class="bi bi-file-earmark-zip"></i> search.tar.gz</span> <span class="badge bg-secondary">3.9 KB</span> </li> <li class="list-group-item d-flex justify-content-between align-items-center"> <span><i class="bi bi-file-earmark-zip"></i> search.zip</span> <span class="badge bg-secondary">4.3 KB</span> </li> <li class="list-group-item d-flex justify-content-between align-items-center"> <span><i class="bi bi-file-earmark-zip"></i> search.tar.Z</span> <span class="badge bg-secondary">5.8 KB</span> </li> <li class="list-group-item d-flex justify-content-between align-items-center"> <span><i class="bi bi-file-earmark"></i> search.tar</span> <span class="badge bg-secondary">20.5 KB</span> </li> </ul> </div> </div> </div> <div class="col-md-6"> <div class="card h-100"> <div class="card-header bg-secondary text-white"> <i class="bi bi-file-code"></i> Individual Files </div> <div class="card-body"> <ul class="list-group list-group-flush"> <li class="list-group-item"> <strong><i class="bi bi-file-code"></i> search.pl</strong> <p class="mb-0 small text-muted">Main Perl script that performs keyword searches</p> </li> <li class="list-group-item"> <strong><i class="bi bi-filetype-html"></i> search.html</strong> <p class="mb-0 small text-muted">HTML form template for the search interface</p> </li> <li class="list-group-item"> <strong><i class="bi bi-file-text"></i> README</strong> <p class="mb-0 small text-muted">Installation instructions and configuration guide</p> </li> </ul> </div> </div> </div> </div> <div class="mt-4"> <a href="/scripts/downloads/search/" class="btn btn-primary btn-lg"><i class="bi bi-download"></i> Go to Downloads</a> </div> </section> <!-- Extras Section --> <section id="extras" class="mb-5"> <h2 class="border-bottom pb-2 mb-4"><i class="bi bi-plus-circle"></i> Extras</h2> <p>Community-contributed enhancements and localizations:</p> <div class="row g-4"> <div class="col-md-6"> <div class="card h-100"> <div class="card-body"> <h5 class="card-title"><i class="bi bi-bug text-warning"></i> Simple Search with Debug</h5> <p class="card-text">A modified version with built-in debugging options to help troubleshoot search issues. Created by the MSA help list community.</p> <span class="badge bg-secondary">Community Contribution</span> </div> </div> </div> <div class="col-md-6"> <div class="card h-100"> <div class="card-body"> <h5 class="card-title"><i class="bi bi-translate text-info"></i> Simple Search in Japanese</h5> <p class="card-text">A localized version modified to search Japanese character sets (Shift-JIS, EUC-JP).</p> <span class="badge bg-secondary">Localization</span> </div> </div> </div> </div> </section> <!-- FAQ Section --> <section id="faq" class="mb-5"> <h2 class="border-bottom pb-2 mb-4"><i class="bi bi-question-circle"></i> Frequently Asked Questions</h2> <div class="accordion" id="faqAccordion"> <div class="accordion-item"> <h2 class="accordion-header"> <button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#faq1"> How can I search recursively down directories? </button> </h2> <div id="faq1" class="accordion-collapse collapse show" data-bs-parent="#faqAccordion"> <div class="accordion-body"> The original script searches only the directories you specify. To enable recursive searching, modify the script to use Perl's <code>File::Find</code> module (as shown in the code examples), or list all subdirectories explicitly in the configuration. </div> </div> </div> <div class="accordion-item"> <h2 class="accordion-header"> <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#faq2"> How many files can this program search through? </button> </h2> <div id="faq2" class="accordion-collapse collapse" data-bs-parent="#faqAccordion"> <div class="accordion-body"> Simple Search reads files on each request, so performance depends on file count and server speed. It works well for under 100 files. For larger sites, build a search index that updates periodically, or use Elasticsearch / Algolia. </div> </div> </div> <div class="accordion-item"> <h2 class="accordion-header"> <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#faq3"> Can I exclude certain files or directories from search? </button> </h2> <div id="faq3" class="accordion-collapse collapse" data-bs-parent="#faqAccordion"> <div class="accordion-body"> Yes. Add an exclusion array such as <code>@exclude = ('admin', 'private', '*.bak');</code> and check each file against it before searching. </div> </div> </div> <div class="accordion-item"> <h2 class="accordion-header"> <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#faq4"> How do I display snippets with highlighted keywords? </button> </h2> <div id="faq4" class="accordion-collapse collapse" data-bs-parent="#faqAccordion"> <div class="accordion-body"> Find the keyword position in the content, extract 50–100 characters before and after, then wrap the keyword in <code><mark></code> tags. </div> </div> </div> <div class="accordion-item"> <h2 class="accordion-header"> <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#faq5"> Can I search PDF or Word documents? </button> </h2> <div id="faq5" class="accordion-collapse collapse" data-bs-parent="#faqAccordion"> <div class="accordion-body"> The original script only searches plain text and HTML. For PDFs, pipe through <code>pdftotext</code> first. For Word documents, convert to text with a library. For sites with many binary formats, a dedicated search engine (Elasticsearch, Solr) is more practical. </div> </div> </div> <div class="accordion-item"> <h2 class="accordion-header"> <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#faq6"> How can I make the search case-sensitive? </button> </h2> <div id="faq6" class="accordion-collapse collapse" data-bs-parent="#faqAccordion"> <div class="accordion-body"> Remove the <code>lc()</code> calls that convert text to lowercase before comparison. You can also add a checkbox to the search form and check that parameter in the script. </div> </div> </div> <div class="accordion-item"> <h2 class="accordion-header"> <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#faq7"> How do I add pagination to search results? </button> </h2> <div id="faq7" class="accordion-collapse collapse" data-bs-parent="#faqAccordion"> <div class="accordion-body"> Add a <code>page</code> parameter to the query string. Calculate offset from page number and results-per-page, slice the results array, and generate pagination links. Example: <code>?keywords=perl&page=2</code>. </div> </div> </div> </div> </section> <!-- Related Scripts Section --> <section id="related" class="mb-5"> <h2 class="border-bottom pb-2 mb-4"><i class="bi bi-collection"></i> Related Scripts</h2> <div class="row g-4"> <div class="col-md-4"> <div class="card h-100"> <div class="card-body"> <h5 class="card-title"><i class="bi bi-envelope text-primary"></i> FormMail</h5> <p class="card-text">Process web form submissions and send results via email.</p> <a href="/scripts/formmail.shtml" class="btn btn-outline-primary btn-sm">View Script</a> </div> </div> </div> <div class="col-md-4"> <div class="card h-100"> <div class="card-body"> <h5 class="card-title"><i class="bi bi-book text-primary"></i> Guestbook</h5> <p class="card-text">Allow visitors to leave messages and comments on your site.</p> <a href="/scripts/guestbook.shtml" class="btn btn-outline-primary btn-sm">View Script</a> </div> </div> </div> <div class="col-md-4"> <div class="card h-100"> <div class="card-body"> <h5 class="card-title"><i class="bi bi-chat-dots text-primary"></i> WWWBoard</h5> <p class="card-text">Threaded discussion forum for community interaction.</p> <a href="/scripts/wwwboard.shtml" class="btn btn-outline-primary btn-sm">View Script</a> </div> </div> </div> </div> </section> <!-- FAQPage Schema --> <script type="application/ld+json"> { "@context": "https://schema.org", "@type": "FAQPage", "mainEntity": [ { "@type": "Question", "name": "How can I search recursively down directories?", "acceptedAnswer": { "@type": "Answer", "text": "Modify the script to use Perl\u0027s File::Find module for automatic recursive directory traversal, or list all subdirectories explicitly in the configuration." } }, { "@type": "Question", "name": "How many files can this program search through?", "acceptedAnswer": { "@type": "Answer", "text": "Simple Search reads files on each request. For small sites under 100 files it works well. For larger sites, consider building a search index or using dedicated solutions like Elasticsearch." } }, { "@type": "Question", "name": "Can I exclude certain files or directories from search?", "acceptedAnswer": { "@type": "Answer", "text": "Yes, add exclusion patterns to the script. Create an array of patterns to skip and check each file against these patterns before searching." } }, { "@type": "Question", "name": "How do I display snippets with highlighted keywords?", "acceptedAnswer": { "@type": "Answer", "text": "Modify the script to extract text around matched keywords and wrap them in mark or strong tags for highlighting." } }, { "@type": "Question", "name": "Can I search PDF or Word documents?", "acceptedAnswer": { "@type": "Answer", "text": "The original script only searches plain text and HTML. To search PDFs or Word documents, you need additional tools or libraries for text extraction." } }, { "@type": "Question", "name": "How can I make the search case-sensitive?", "acceptedAnswer": { "@type": "Answer", "text": "Remove the lc() functions that convert text to lowercase before comparison, or add a user checkbox to toggle case sensitivity." } }, { "@type": "Question", "name": "How do I add pagination to search results?", "acceptedAnswer": { "@type": "Answer", "text": "Add a page parameter to the query string and modify the script to calculate offset, slice results, and generate pagination links." } } ] } </script> </div><!-- /.container --> <script type="application/ld+json">{ "@context": "https://schema.org", "@type": "BreadcrumbList", "itemListElement": [ { "@type": "ListItem", "position": 1, "name": "Home", "item": "https://www.worldwidemart.com/" }, { "@type": "ListItem", "position": 2, "name": "Scripts", "item": "https://www.worldwidemart.com/scripts/" }, { "@type": "ListItem", "position": 3, "name": "Simple Search" } ] }</script> <!-- Footer for worldwidemart.com - Bootstrap 5 --> <footer class="py-5 bg-dark text-light"> <div class="container"> <div class="row gy-4"> <!-- About Section --> <div class="col-lg-3 col-md-6 col-12"> <div class="d-flex flex-column gap-3"> <h5 class="fw-bold text-white mb-0">WorldWideMart</h5> <div class="fs-5 d-flex flex-row gap-3"> <a href="mailto:webarchive@gmail.com" class="text-secondary" title="Email"> <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="bi bi-envelope" viewBox="0 0 16 16"> <path d="M0 4a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V4Zm2-1a1 1 0 0 0-1 1v.217l7 4.2 7-4.2V4a1 1 0 0 0-1-1H2Zm13 2.383-4.708 2.825L15 11.105V5.383Zm-.034 6.876-5.64-3.471L8 9.583l-1.326-.795-5.64 3.47A1 1 0 0 0 2 13h12a1 1 0 0 0 .966-.741ZM1 11.105l4.708-2.897L1 5.383v5.722Z"/> </svg> </a> <!-- GitHub: no project repo yet --> </div> </div> </div> <!-- CGI Scripts - Main Category --> <div class="col-lg-3 col-md-6 col-6"> <div class="d-flex flex-column gap-2"> <h6 class="fw-bold text-white text-uppercase mb-1">CGI Scripts</h6> <ul class="list-unstyled mb-0 small"> <li class="mb-1"><a href="/scripts/" class="text-secondary text-decoration-none">All Scripts</a></li> <li class="mb-1"><a href="/scripts/formmail.shtml" class="text-secondary text-decoration-none">FormMail</a></li> <li class="mb-1"><a href="/scripts/wwwboard.shtml" class="text-secondary text-decoration-none">WWWBoard</a></li> <li class="mb-1"><a href="/scripts/guestbook.shtml" class="text-secondary text-decoration-none">Guestbook</a></li> <li class="mb-1"><a href="/scripts/search.shtml" class="text-secondary text-decoration-none">Simple Search</a></li> <li class="mb-1"><a href="/scripts/counter.shtml" class="text-secondary text-decoration-none">Counter</a></li> <li class="mb-1"><a href="/scripts/textcounter.shtml" class="text-secondary text-decoration-none">TextCounter</a></li> <li class="mb-1"><a href="/scripts/cookielib.shtml" class="text-secondary text-decoration-none">HTTP Cookie Library</a></li> <li class="mb-1"><a href="/scripts/links.shtml" class="text-secondary text-decoration-none">Free For All Links</a></li> <li class="mb-1"><a href="/scripts/C++/textcounter.shtml" class="text-secondary text-decoration-none">C++ TextCounter</a></li> </ul> </div> </div> <!-- Help & Documentation --> <div class="col-lg-2 col-md-4 col-6"> <div class="d-flex flex-column gap-2"> <h6 class="fw-bold text-white text-uppercase mb-1">Help & Docs</h6> <ul class="list-unstyled mb-0 small"> <li class="mb-1"><a href="/scripts/faq/" class="text-secondary text-decoration-none">FAQ</a></li> <li class="mb-1"><a href="/scripts/readme/" class="text-secondary text-decoration-none">Readme Files</a></li> <li class="mb-1"><a href="/scripts/help/" class="text-secondary text-decoration-none">Help Center</a></li> <li class="mb-1"><a href="/scripts/examples/" class="text-secondary text-decoration-none">Examples</a></li> <li class="mb-1"><a href="/scripts/demos/" class="text-secondary text-decoration-none">Demos</a></li> <li class="mb-1"><a href="/scripts/get_all.shtml" class="text-secondary text-decoration-none">Download Scripts</a></li> </ul> </div> </div> <!-- Blog & History --> <div class="col-lg-2 col-md-4 col-6"> <div class="d-flex flex-column gap-2"> <h6 class="fw-bold text-white text-uppercase mb-1">Blog & History</h6> <ul class="list-unstyled mb-0 small"> <li class="mb-1"><a href="/blog/" class="text-secondary text-decoration-none">Blog</a></li> <li class="mb-1"><a href="/history/" class="text-secondary text-decoration-none">Full History</a></li> <li class="mb-1"><a href="/history/matt-wright/" class="text-secondary text-decoration-none">Matt Wright</a></li> <li class="mb-1"><a href="/history/90s-web-design/" class="text-secondary text-decoration-none">90s Web Design</a></li> <li class="mb-1"><a href="/glossary/" class="text-secondary text-decoration-none">Glossary</a></li> <li class="mb-1"><a href="/blog/feed.xml" class="text-secondary text-decoration-none"><i class="bi bi-rss"></i> RSS Feed</a></li> </ul> </div> </div> </div> <!-- Divider --> <hr class="my-4 border-secondary"> <!-- Bottom Row --> <div class="row align-items-center"> <div class="col-md-6 text-center text-md-start"> <p class="mb-0 small text-secondary"> WorldWideMart.com — the original domain of Matt's Script Archive since 1995.<br> Scripts, documentation, and historical context preserved for reference.<br> © 1995 - <span id="currentYear"></span> WorldWideMart.com. Original scripts by Matt Wright and contributors. </p> </div> <div class="col-md-6 text-center text-md-end mt-2 mt-md-0"> <ul class="list-inline mb-0 small"> <li class="list-inline-item"><a href="/about/" class="text-secondary text-decoration-none">About</a></li> <li class="list-inline-item text-secondary">|</li> <li class="list-inline-item"><a href="/contact/" class="text-secondary text-decoration-none">Contact</a></li> <li class="list-inline-item text-secondary">|</li> <li class="list-inline-item"><a href="/hosting/" class="text-secondary text-decoration-none">Web Hosting</a></li> <li class="list-inline-item text-secondary">|</li> <li class="list-inline-item"><a href="/blog/" class="text-secondary text-decoration-none">Blog</a></li> </ul> </div> </div> </div> </footer> <script> document.getElementById('currentYear').textContent = new Date().getFullYear(); </script> <script defer src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js"></script> <script defer src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-perl.min.js"></script> <script defer src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-php.min.js"></script> <script defer src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-javascript.min.js"></script> <script defer src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-bash.min.js"></script> <script defer src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-markup.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js" integrity="sha384-FKyoEForCGlyvwx9Hj09JcYn3nv7wiPVlz7YYwJrWVcXK/BmnVDxM+D2scQbITxI" crossorigin="anonymous"></script> <script defer src="https://cdn.jsdelivr.net/npm/fuse.js@7.0.0/dist/fuse.min.js"></script> <script> // Copy code button for all pre>code blocks document.querySelectorAll('pre').forEach(function(pre) { if (!pre.querySelector('code')) return; var btn = document.createElement('button'); btn.className = 'btn btn-sm btn-outline-secondary position-absolute'; btn.style.cssText = 'top:8px;right:8px;font-size:12px;padding:2px 8px;opacity:0.6;z-index:1'; btn.textContent = 'Copy'; btn.addEventListener('mouseenter', function() { this.style.opacity = '1'; }); btn.addEventListener('mouseleave', function() { this.style.opacity = '0.6'; }); btn.addEventListener('click', function() { var code = pre.querySelector('code'); navigator.clipboard.writeText(code.textContent).then(function() { btn.textContent = 'Copied!'; btn.classList.replace('btn-outline-secondary', 'btn-success'); setTimeout(function() { btn.textContent = 'Copy'; btn.classList.replace('btn-success', 'btn-outline-secondary'); }, 1500); }); }); pre.style.position = 'relative'; pre.appendChild(btn); }); </script> <script> // Auto-generate IDs + anchor links with copy URL document.querySelectorAll('h2, h3').forEach(function(h) { if (!h.id) h.id = h.textContent.trim().toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, ''); var a = document.createElement('a'); a.href = '#' + h.id; a.className = 'heading-anchor'; a.textContent = '#'; a.title = 'Copy link'; a.addEventListener('click', function(e) { e.preventDefault(); navigator.clipboard.writeText(location.origin + location.pathname + '#' + h.id); a.textContent = '\u2713'; a.style.opacity = '1'; a.style.color = 'var(--wwm-green)'; setTimeout(function() { a.textContent = '#'; a.style.cssText = ''; }, 1500); history.replaceState(null, '', '#' + h.id); }); h.appendChild(a); }); // Auto Table of Contents for pages with 3+ headings (function() { var headings = document.querySelectorAll('h2[id], h3[id]'); if (headings.length < 5) return; var container = document.querySelector('.container'); if (!container) return; var firstH2 = container.querySelector('h2'); if (!firstH2) return; var toc = document.createElement('nav'); toc.className = 'card mb-4 d-none d-lg-block'; toc.innerHTML = '<div class="card-body py-2 px-3"><h6 class="card-title font-mono mb-2" style="font-size:0.8rem;text-transform:uppercase;letter-spacing:0.05em;color:var(--wwm-text-muted,#656d76)">On this page</h6><ul class="list-unstyled mb-0" id="toc-list"></ul></div>'; firstH2.parentNode.insertBefore(toc, firstH2); var list = document.getElementById('toc-list'); headings.forEach(function(h) { var li = document.createElement('li'); li.style.cssText = h.tagName === 'H3' ? 'padding-left:12px;' : ''; var a = document.createElement('a'); a.href = '#' + h.id; a.textContent = h.textContent.replace(/^#\s*/, ''); a.style.cssText = 'font-size:0.82rem;color:var(--wwm-text-muted,#656d76);text-decoration:none;display:block;padding:2px 0;'; a.addEventListener('mouseenter', function() { this.style.color = 'var(--wwm-green,#0a6640)'; }); a.addEventListener('mouseleave', function() { this.style.color = 'var(--wwm-text-muted,#656d76)'; }); li.appendChild(a); list.appendChild(li); }); })(); </script> <button class="back-to-top" id="backToTop" aria-label="Back to top"><i class="bi bi-arrow-up"></i></button> <script> (function() { var btn = document.getElementById('backToTop'); window.addEventListener('scroll', function() { btn.classList.toggle('visible', window.scrollY > 400); }); btn.addEventListener('click', function() { window.scrollTo({ top: 0, behavior: 'smooth' }); }); })(); </script> <script> (function() { var html = document.documentElement; var btn = document.getElementById("themeToggle"); var icon = document.getElementById("themeIcon"); var saved = localStorage.getItem("theme"); var prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches; var theme = saved || (prefersDark ? "dark" : "light"); function setTheme(t) { html.setAttribute("data-bs-theme", t); if (icon) { icon.className = t === "dark" ? "bi bi-sun-fill" : "bi bi-moon-fill"; } localStorage.setItem("theme", t); } setTheme(theme); if (btn) { btn.addEventListener("click", function() { var current = html.getAttribute("data-bs-theme"); setTheme(current === "dark" ? "light" : "dark"); }); } })(); </script> <!-- Search Modal --> <div class="modal fade" id="searchModal" tabindex="-1" aria-hidden="true"> <div class="modal-dialog modal-dialog-scrollable"> <div class="modal-content"> <div class="modal-body p-0"> <div class="p-3 border-bottom"> <div class="input-group"> <span class="input-group-text bg-transparent border-end-0"><i class="bi bi-search"></i></span> <input type="text" id="searchInput" class="form-control border-start-0" placeholder="Search scripts, articles, glossary..." autofocus> <button type="button" class="btn btn-sm btn-outline-secondary" data-bs-dismiss="modal" style="font-size:0.75rem;">ESC</button> </div> </div> <div id="searchResults" class="p-2" style="max-height:400px;overflow-y:auto;"></div> <div id="searchEmpty" class="text-center text-muted py-4 small" style="display:none;">No results found</div> <div id="searchHint" class="text-center text-muted py-3 small"><kbd>Ctrl+K</kbd> to search</div> </div> </div> </div> </div> <script> (function(){ var fuse,data,modal,input,results,empty,hint,sel=-1; var S={scripts:{l:"Scripts",i:"bi-code-slash"},history:{l:"History",i:"bi-clock-history"},blog:{l:"Blog",i:"bi-pencil-square"},faq:{l:"FAQ",i:"bi-question-circle"},reference:{l:"Reference",i:"bi-journal-text"},downloads:{l:"Downloads",i:"bi-download"},other:{l:"Page",i:"bi-file-text"}}; function load(cb){ if(data)return cb(); fetch("/api/search-index.php").then(function(r){return r.json()}).then(function(d){ data=d; fuse=new Fuse(d,{keys:[{name:"t",weight:2},{name:"d",weight:1}],threshold:0.35,minMatchCharLength:2}); cb(); }); } function render(q){ if(!fuse||!q||q.length<2){results.innerHTML="";empty.style.display="none";hint.style.display="";sel=-1;return;} var r=fuse.search(q,{limit:8}); hint.style.display="none";sel=-1; if(!r.length){results.innerHTML="";empty.style.display="";return;} empty.style.display="none"; results.innerHTML=r.map(function(x,i){ var it=x.item,s=S[it.s]||S.other; return "<a href=\""+it.u+"\" class=\"search-result d-flex align-items-start gap-2 p-2 rounded text-decoration-none\">" +"<i class=\"bi "+s.i+" mt-1 text-muted\"></i>" +"<div class=\"flex-grow-1 overflow-hidden\">" +"<div class=\"fw-medium text-truncate\" style=\"font-size:0.9rem\">"+it.t+"</div>" +(it.d?"<div class=\"text-muted text-truncate\" style=\"font-size:0.78rem\">"+it.d.substring(0,80)+"</div>":"") +"</div>" +"<span class=\"badge bg-light text-muted\" style=\"font-size:0.65rem\">"+s.l+"</span></a>"; }).join(""); } function pick(i){ var a=results.querySelectorAll(".search-result"); a.forEach(function(e){e.style.background="";}); if(i>=0&&i<a.length){a[i].style.background="var(--wwm-green-pale,#dafbe1)";a[i].scrollIntoView({block:"nearest"});} sel=i; } document.addEventListener("DOMContentLoaded",function(){ var el=document.getElementById("searchModal"); if(!el)return; modal=new bootstrap.Modal(el); input=document.getElementById("searchInput"); results=document.getElementById("searchResults"); empty=document.getElementById("searchEmpty"); hint=document.getElementById("searchHint"); el.addEventListener("shown.bs.modal",function(){load(function(){input.focus();});}); el.addEventListener("hidden.bs.modal",function(){input.value="";results.innerHTML="";empty.style.display="none";hint.style.display="";}); input.addEventListener("input",function(){render(this.value.trim());}); input.addEventListener("keydown",function(e){ var a=results.querySelectorAll(".search-result"); if(e.key==="ArrowDown"){e.preventDefault();pick(Math.min(sel+1,a.length-1));} else if(e.key==="ArrowUp"){e.preventDefault();pick(Math.max(sel-1,0));} else if(e.key==="Enter"&&sel>=0&&a[sel]){a[sel].click();} }); document.addEventListener("keydown",function(e){ if((e.ctrlKey||e.metaKey)&&e.key==="k"){e.preventDefault();modal.show();} }); }); })(); </script> <script> (function(){var b=document.createElement("div");b.className="reading-progress";document.body.appendChild(b);window.addEventListener("scroll",function(){var h=document.documentElement.scrollHeight-window.innerHeight;if(h>0)b.style.width=(window.scrollY/h*100)+"%";});})(); </script> <script> // Scroll fade-in for sections (function() { var items = document.querySelectorAll("section.mb-5, .post-card"); if (!items.length) return; items.forEach(function(s) { s.classList.add("fade-in-section"); }); var obs = new IntersectionObserver(function(entries) { entries.forEach(function(e) { if (e.isIntersecting) { e.target.classList.add("is-visible"); obs.unobserve(e.target); } }); }, { threshold: 0.1 }); items.forEach(function(s) { obs.observe(s); }); })(); </script> <script> // Auto-add share buttons to articles (function() { var article = document.querySelector('.article-content'); if (!article) return; var title = encodeURIComponent(document.title); var url = encodeURIComponent(location.href); var div = document.createElement('div'); div.className = 'share-buttons'; div.innerHTML = '<a href="https://twitter.com/intent/tweet?url=' + url + '&text=' + title + '" target="_blank" rel="noopener"><i class="bi bi-twitter-x"></i> Share</a>' + '<a href="https://www.linkedin.com/shareArticle?mini=true&url=' + url + '" target="_blank" rel="noopener"><i class="bi bi-linkedin"></i> LinkedIn</a>' + '<a href="mailto:?subject=' + title + '&body=' + url + '"><i class="bi bi-envelope"></i> Email</a>'; var btn = document.createElement('button'); btn.innerHTML = '<i class="bi bi-link-45deg"></i> Copy Link'; btn.addEventListener('click', function() { navigator.clipboard.writeText(location.href); btn.innerHTML = '<i class="bi bi-check2"></i> Copied!'; setTimeout(function() { btn.innerHTML = '<i class="bi bi-link-45deg"></i> Copy Link'; }, 1500); }); div.appendChild(btn); article.appendChild(div); })(); </script> </body> </html>