about summary refs log tree commit diff stats
diff options
context:
space:
mode:
authorelioat <elioat@tilde.institute>2025-02-23 22:06:47 -0500
committerelioat <elioat@tilde.institute>2025-02-23 22:06:47 -0500
commit649883cae43e78c0a84f23b4bfa5622a95ae85a9 (patch)
tree279021b885450473de335bfb76e1ff428d08f267
parent3bed0d3550d620217d29626cb0b4354e087f94f2 (diff)
downloadtour-649883cae43e78c0a84f23b4bfa5622a95ae85a9.tar.gz
*
-rw-r--r--js/sentiment/bookmarklet-minified.js1
-rw-r--r--js/sentiment/bookmarklet.js8
-rw-r--r--js/sentiment/sentiment/PressStart2P-Regular.ttf0
-rw-r--r--js/sentiment/sentiment/index.html250
-rw-r--r--js/sentiment/sentiment/sentiment.browser.js220
5 files changed, 479 insertions, 0 deletions
diff --git a/js/sentiment/bookmarklet-minified.js b/js/sentiment/bookmarklet-minified.js
new file mode 100644
index 0000000..4671638
--- /dev/null
+++ b/js/sentiment/bookmarklet-minified.js
@@ -0,0 +1 @@
+javascript:void function(){let e=document.createElement("script");e.src="https://eli.li/_assets/bin/sentiment.browser.js",e.onload=function(){analyzePage()},document.head.appendChild(e)}(); 
\ No newline at end of file
diff --git a/js/sentiment/bookmarklet.js b/js/sentiment/bookmarklet.js
new file mode 100644
index 0000000..9209754
--- /dev/null
+++ b/js/sentiment/bookmarklet.js
@@ -0,0 +1,8 @@
+javascript:(function(){
+    const script = document.createElement('script');
+    script.src = 'https://eli.li/_assets/bin/sentiment.browser.js';
+    script.onload = function() {
+        analyzePage();
+    };
+    document.head.appendChild(script);
+})(); 
\ No newline at end of file
diff --git a/js/sentiment/sentiment/PressStart2P-Regular.ttf b/js/sentiment/sentiment/PressStart2P-Regular.ttf
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/js/sentiment/sentiment/PressStart2P-Regular.ttf
diff --git a/js/sentiment/sentiment/index.html b/js/sentiment/sentiment/index.html
new file mode 100644
index 0000000..5c6569f
--- /dev/null
+++ b/js/sentiment/sentiment/index.html
@@ -0,0 +1,250 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>Sentiment Analyzer</title>
+    <link href="./PressStart2P-Regular.ttf" rel="stylesheet">
+    <style>
+        @font-face {
+            font-family: 'Press Start 2P';
+            src: url('./PressStart2P-Regular.ttf') format('truetype');
+            font-weight: normal;
+            font-style: normal;
+        }
+        :root {
+            --gba-dark: #081820;
+            --gba-mid: #346856;
+            --gba-light: #88c070;
+            --gba-pale: #e0f8d0;
+        }
+
+        body {
+            font-family: 'Press Start 2P', monospace;
+            line-height: 1.6;
+            max-width: 800px;
+            margin: 0 auto;
+            padding: 20px;
+            background-color: var(--gba-dark);
+            color: var(--gba-pale);
+            font-size: 12px;
+        }
+
+        /* Retro window styling */
+        .window {
+            border: 4px solid var(--gba-pale);
+            border-radius: 0;
+            padding: 20px;
+            margin: 20px 0;
+            position: relative;
+            background: var(--gba-dark);
+        }
+
+        .window::before {
+            content: '';
+            position: absolute;
+            top: -8px;
+            left: -8px;
+            right: -8px;
+            bottom: -8px;
+            border: 2px solid var(--gba-dark);
+            z-index: -1;
+        }
+
+        h1 {
+            color: var(--gba-light);
+            text-align: center;
+            margin: 40px 0;
+            text-transform: uppercase;
+            letter-spacing: 2px;
+            text-shadow: 
+                2px 2px 0 var(--gba-dark),
+                4px 4px 0 var(--gba-mid);
+        }
+
+        h2 {
+            color: var(--gba-light);
+            font-size: 14px;
+            margin-top: 30px;
+            border-bottom: 4px solid var(--gba-mid);
+            padding-bottom: 10px;
+        }
+
+        .bookmarklet {
+            display: inline-block;
+            padding: 15px 30px;
+            background: var(--gba-light);
+            color: var(--gba-dark);
+            text-decoration: none;
+            margin: 20px 0;
+            cursor: move;
+            border: 4px solid var(--gba-pale);
+            box-shadow: 
+                4px 4px 0 var(--gba-mid),
+                8px 8px 0 var(--gba-dark);
+            transition: all 0.1s ease;
+        }
+
+        .bookmarklet:hover {
+            transform: translate(2px, 2px);
+            box-shadow: 
+                2px 2px 0 var(--gba-mid),
+                6px 6px 0 var(--gba-dark);
+        }
+
+        .installation {
+            border: 4px solid var(--gba-light);
+            padding: 20px;
+            margin: 20px 0;
+            background: var(--gba-mid);
+        }
+
+        code {
+            background: var(--gba-dark);
+            padding: 4px 8px;
+            border: 2px solid var(--gba-light);
+            color: var(--gba-pale);
+        }
+
+        .warning {
+            border: 4px solid var(--gba-light);
+            border-style: dashed;
+            padding: 20px;
+            margin: 20px 0;
+            animation: blink 2s infinite;
+        }
+
+        @keyframes blink {
+            0% { border-color: var(--gba-light); }
+            50% { border-color: var(--gba-mid); }
+            100% { border-color: var(--gba-light); }
+        }
+
+        ul, ol {
+            padding-left: 20px;
+        }
+
+        li {
+            margin: 10px 0;
+            position: relative;
+        }
+
+        li::before {
+            content: '►';
+            position: absolute;
+            left: -20px;
+            color: var(--gba-light);
+        }
+
+        footer {
+            margin-top: 40px;
+            padding-top: 20px;
+            border-top: 4px solid var(--gba-mid);
+            color: var(--gba-light);
+            text-align: center;
+            font-size: 10px;
+        }
+
+        a {
+            color: var(--gba-light);
+            text-decoration: none;
+        }
+
+        a:hover {
+            color: var(--gba-pale);
+            text-decoration: underline;
+        }
+
+        /* Pixel art decorations */
+        .pixel-corner {
+            position: fixed;
+            width: 32px;
+            height: 32px;
+            background: var(--gba-light);
+            clip-path: polygon(0 0, 100% 0, 0 100%);
+        }
+
+        .top-left { top: 0; left: 0; }
+        .top-right { top: 0; right: 0; transform: rotate(90deg); }
+        .bottom-left { bottom: 0; left: 0; transform: rotate(-90deg); }
+        .bottom-right { bottom: 0; right: 0; transform: rotate(180deg); }
+
+    </style>
+</head>
+<body>
+    <!-- Pixel art corners -->
+    <div class="pixel-corner top-left"></div>
+    <div class="pixel-corner top-right"></div>
+    <div class="pixel-corner bottom-left"></div>
+    <div class="pixel-corner bottom-right"></div>
+
+    <h1>Sentiment Scanner</h1>
+    
+    <div class="window">
+        <p>
+            This bookmarklet analyzes the emotional tone of any webpage.
+        </p>
+    </div>
+
+    <div class="installation">
+        <h2>Installation guide</h2>
+        <p><strong>Drag this power-up to your inventory:</strong></p>
+        <a class="bookmarklet" href="javascript:void function(){let e=document.createElement('script');e.src='https://smallandnearlysilent.com/sentiment/sentiment.browser.js',e.onload=function(){analyzePage()},document.head.appendChild(e)}();">
+            Sentiment
+        </a>
+        
+        <p><strong>Installation steps:</strong></p>
+        <ol>
+            <li>Open your bookmarks (<code>ctrl/cmd + shift + b</code>)</li>
+            <li>Drag the bookmarklet to your bookmarks bar</li>
+            <li>Cook</li>
+        </ol>
+    </div>
+
+    <h2>How to use it</h2>
+    <ol>
+        <li>Navigate to target webpage</li>
+        <li>Click the bookmarklet in your bookmarks bar</li>
+        <li>It'll analyze the page and display the results in a popup</li>
+    </ol>
+
+    <div class="window">
+        <h3>Results include:</h3>
+        <ul>
+            <li>Emotional alignment</li>
+            <li>Power level</li>
+            <li>Sentiment score</li>
+            <li>Word statistics</li>
+            <li>Positive/negative ratio</li>
+        </ul>
+    </div>
+
+    <div class="warning">
+        <h3>Important notice</h3>
+        <ul>
+            <li>Optimal targets: articles & blog posts</li>
+            <li>English text only</li>
+            <li>Some sites may have shields up</li>
+            <li>No data collection - stealth mode</li>
+        </ul>
+    </div>
+
+    <h2>Security Considerations</h2>
+    <p>
+        Everything runs locally. No data is transmitted anywhere.
+    </p>
+
+    <footer>
+        <p>
+            <a href="https://eli.li/_assets/bin/sentiment.browser.js" target="_blank">VIEW THE SCRIPT</a>
+        </p>
+    </footer>
+
+    <script>
+        document.querySelector('.bookmarklet').addEventListener('click', function(e) {
+            e.preventDefault();
+            alert('COMMAND INVALID!\nDRAG TO INVENTORY (BOOKMARKS) INSTEAD!');
+        });
+    </script>
+</body>
+</html> 
\ No newline at end of file
diff --git a/js/sentiment/sentiment/sentiment.browser.js b/js/sentiment/sentiment/sentiment.browser.js
new file mode 100644
index 0000000..e12f72f
--- /dev/null
+++ b/js/sentiment/sentiment/sentiment.browser.js
@@ -0,0 +1,220 @@
+/**
+ * Browser-friendly Web Sentiment Analyzer
+ * Simplified version optimized for bookmarklet use
+ */
+
+const createWebSentimentAnalyzer = (config = {}) => {
+    // Simplified default config with core sentiment words
+    const defaultConfig = {
+        positiveWords: new Set([
+            'love', 'joy', 'happy', 'excited', 'peaceful', 'wonderful', 'fantastic',
+            'excellent', 'outstanding', 'superb', 'brilliant', 'helpful', 'great',
+            'efficient', 'effective', 'reliable', 'innovative', 'productive',
+            'friendly', 'supportive', 'kind', 'generous', 'caring'
+        ]),
+        
+        negativeWords: new Set([
+            'hate', 'angry', 'sad', 'upset', 'frustrated', 'disappointed', 'anxious',
+            'poor', 'bad', 'terrible', 'horrible', 'awful', 'dreadful', 'inferior',
+            'inefficient', 'ineffective', 'unreliable', 'problematic', 'failing',
+            'hostile', 'unfriendly', 'unhelpful', 'rude', 'mean'
+        ]),
+        
+        intensifiers: new Map([
+            ['extremely', 2.0],
+            ['very', 1.5],
+            ['really', 1.5],
+            ['quite', 1.25]
+        ]),
+        
+        negators: new Set([
+            'not', 'no', 'never', 'none',
+            "n't", 'cannot', "won't", "wouldn't"
+        ])
+    };
+
+    const finalConfig = { ...defaultConfig, ...config };
+
+    const analyzeText = (text) => {
+        if (!text || typeof text !== 'string') {
+            return {
+                score: 0,
+                summary: { positive: 0, negative: 0, neutral: 0 },
+                sentiment: 'Neutral',
+                intensity: 'None',
+                wordCount: 0
+            };
+        }
+        
+        const words = text.toLowerCase()
+            .replace(/[.,\/#!$%\^&\*;:{}=\-_`~()]/g, '')
+            .split(/\s+/);
+            
+        let score = 0;
+        let multiplier = 1;
+        let positiveCount = 0;
+        let negativeCount = 0;
+        let intensifierCount = 0;
+        
+        for (let i = 0; i < words.length; i++) {
+            const word = words[i];
+            
+            if (finalConfig.intensifiers.has(word)) {
+                multiplier = finalConfig.intensifiers.get(word);
+                intensifierCount++;
+                continue;
+            }
+            
+            if (finalConfig.negators.has(word)) {
+                multiplier *= -1;
+                continue;
+            }
+            
+            if (finalConfig.positiveWords.has(word)) {
+                score += 1 * multiplier;
+                positiveCount++;
+            } else if (finalConfig.negativeWords.has(word)) {
+                score += -1 * multiplier;
+                negativeCount++;
+            }
+            
+            multiplier = 1;
+        }
+        
+        const getIntensity = (score, intensifierCount) => {
+            const magnitude = Math.abs(score);
+            if (magnitude > 10 || intensifierCount > 5) return 'Very Strong';
+            if (magnitude > 7 || intensifierCount > 3) return 'Strong';
+            if (magnitude > 4 || intensifierCount > 1) return 'Moderate';
+            if (magnitude > 0) return 'Mild';
+            return 'Neutral';
+        };
+
+        const getSentiment = (score) => {
+            if (score > 5) return 'Very Positive';
+            if (score > 0) return 'Positive';
+            if (score < -5) return 'Very Negative';
+            if (score < 0) return 'Negative';
+            return 'Neutral';
+        };
+        
+        return {
+            score,
+            summary: {
+                positive: positiveCount,
+                negative: negativeCount,
+                neutral: words.length - positiveCount - negativeCount,
+                total: words.length
+            },
+            sentiment: getSentiment(score),
+            intensity: getIntensity(score, intensifierCount),
+            wordCount: words.length
+        };
+    };
+
+    const extractPageContent = () => {
+        // Priority content selectors
+        const contentSelectors = [
+            'article',
+            'main',
+            '.content',
+            '.post-content',
+            'article p',
+            '.content p',
+            'p'
+        ];
+        
+        let content = '';
+        for (const selector of contentSelectors) {
+            const elements = document.querySelectorAll(selector);
+            if (elements.length) {
+                elements.forEach(el => {
+                    if (!el.closest('nav') && !el.closest('header') && !el.closest('footer')) {
+                        content += el.textContent + '\n\n';
+                    }
+                });
+                if (content.trim().length > 0) break;
+            }
+        }
+        
+        // Fallback to body text if no content found
+        if (!content) {
+            content = document.body.textContent || '';
+        }
+        
+        return content
+            .replace(/\s+/g, ' ')
+            .replace(/\n\s*\n/g, '\n\n')
+            .trim();
+    };
+
+    const createResultsOverlay = (analysis) => {
+        const overlay = document.createElement('div');
+        overlay.style.cssText = `
+            position: fixed;
+            top: 20px;
+            right: 20px;
+            max-width: 400px;
+            background: white;
+            border: 2px solid #ccc;
+            border-radius: 8px;
+            padding: 20px;
+            box-shadow: 0 4px 12px rgba(0,0,0,0.15);
+            z-index: 999999;
+            font-family: Arial, sans-serif;
+            font-size: 14px;
+            line-height: 1.4;
+        `;
+
+        const closeButton = document.createElement('button');
+        closeButton.textContent = '×';
+        closeButton.style.cssText = `
+            position: absolute;
+            top: 10px;
+            right: 10px;
+            border: none;
+            background: none;
+            font-size: 20px;
+            cursor: pointer;
+            color: #666;
+        `;
+        closeButton.onclick = () => overlay.remove();
+
+        const content = document.createElement('div');
+        content.innerHTML = `
+            <h2 style="margin: 0 0 15px 0; color: #333;">Sentiment Analysis</h2>
+            <p style="margin: 0 0 10px 0;"><strong>Overall Sentiment:</strong> ${analysis.sentiment}</p>
+            <p style="margin: 0 0 10px 0;"><strong>Intensity:</strong> ${analysis.intensity}</p>
+            <p style="margin: 0 0 10px 0;"><strong>Score:</strong> ${analysis.score.toFixed(2)}</p>
+            <hr style="margin: 15px 0; border: none; border-top: 1px solid #eee;">
+            <p style="margin: 0 0 10px 0;"><strong>Word Count:</strong> ${analysis.wordCount}</p>
+            <p style="margin: 0 0 5px 0;"><strong>Breakdown:</strong></p>
+            <ul style="margin: 0; padding-left: 20px;">
+                <li>Positive Words: ${analysis.summary.positive}</li>
+                <li>Negative Words: ${analysis.summary.negative}</li>
+                <li>Neutral Words: ${analysis.summary.neutral}</li>
+            </ul>
+        `;
+
+        overlay.appendChild(closeButton);
+        overlay.appendChild(content);
+        document.body.appendChild(overlay);
+    };
+
+    return {
+        analyzeText,
+        extractPageContent,
+        createResultsOverlay
+    };
+};
+
+// Bookmarklet-friendly function
+const analyzePage = () => {
+    const analyzer = createWebSentimentAnalyzer();
+    const content = analyzer.extractPageContent();
+    const analysis = analyzer.analyzeText(content);
+    analyzer.createResultsOverlay(analysis);
+};
+
+// For testing in browser console
+// analyzePage(); 
\ No newline at end of file