diff options
-rw-r--r-- | js/sentiment/.gitignore | 175 | ||||
-rw-r--r-- | js/sentiment/README.md | 108 | ||||
-rw-r--r-- | js/sentiment/app.js | 757 | ||||
-rwxr-xr-x | js/sentiment/bun.lockb | bin | 0 -> 25383 bytes | |||
-rw-r--r-- | js/sentiment/jsconfig.json | 27 | ||||
-rw-r--r-- | js/sentiment/package.json | 14 |
6 files changed, 1081 insertions, 0 deletions
diff --git a/js/sentiment/.gitignore b/js/sentiment/.gitignore new file mode 100644 index 0000000..9b1ee42 --- /dev/null +++ b/js/sentiment/.gitignore @@ -0,0 +1,175 @@ +# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore + +# Logs + +logs +_.log +npm-debug.log_ +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Caches + +.cache + +# Diagnostic reports (https://nodejs.org/api/report.html) + +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# Runtime data + +pids +_.pid +_.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover + +lib-cov + +# Coverage directory used by tools like istanbul + +coverage +*.lcov + +# nyc test coverage + +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) + +.grunt + +# Bower dependency directory (https://bower.io/) + +bower_components + +# node-waf configuration + +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) + +build/Release + +# Dependency directories + +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) + +web_modules/ + +# TypeScript cache + +*.tsbuildinfo + +# Optional npm cache directory + +.npm + +# Optional eslint cache + +.eslintcache + +# Optional stylelint cache + +.stylelintcache + +# Microbundle cache + +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history + +.node_repl_history + +# Output of 'npm pack' + +*.tgz + +# Yarn Integrity file + +.yarn-integrity + +# dotenv environment variable files + +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) + +.parcel-cache + +# Next.js build output + +.next +out + +# Nuxt.js build / generate output + +.nuxt +dist + +# Gatsby files + +# Comment in the public line in if your project uses Gatsby and not Next.js + +# https://nextjs.org/blog/next-9-1#public-directory-support + +# public + +# vuepress build output + +.vuepress/dist + +# vuepress v2.x temp and cache directory + +.temp + +# Docusaurus cache and generated files + +.docusaurus + +# Serverless directories + +.serverless/ + +# FuseBox cache + +.fusebox/ + +# DynamoDB Local files + +.dynamodb/ + +# TernJS port file + +.tern-port + +# Stores VSCode versions used for testing VSCode extensions + +.vscode-test + +# yarn v2 + +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +# IntelliJ based IDEs +.idea + +# Finder (MacOS) folder config +.DS_Store diff --git a/js/sentiment/README.md b/js/sentiment/README.md new file mode 100644 index 0000000..141470d --- /dev/null +++ b/js/sentiment/README.md @@ -0,0 +1,108 @@ +# Sentiment Analyzer + +## Overview + +The Sentiment Analyzer is a JavaScript application designed to analyze the sentiment of web content. It utilizes a combination of dictionary-based sentiment analysis, emotion categorization, intensity analysis, web content extraction, and metadata parsing to provide a comprehensive emotional analysis of text from web pages. + +### Key Features + +- **Emotion Categorization**: Classifies emotions into various categories such as joy, sadness, anger, and more. +- **Intensity Analysis**: Measures the intensity of sentiments based on the context and usage of words. +- **Web Content Extraction**: Fetches and extracts meaningful content from web pages, ignoring irrelevant sections like headers and footers. +- **Metadata Parsing**: Extracts useful metadata such as titles, authors, and publication dates from web pages. + +## Installation + +To install dependencies, run: + +```bash +bun install +``` + +## Usage + +To run the sentiment analyzer, use the following command: + +```bash +bun run app.js <url> +``` + +You can also analyze multiple URLs at once: + +```bash +bun run app.js <url1> <url2> <url3> +``` + +### Example + +```bash +bun run app.js https://example.com/blog-post +``` + +### Help + +To display help information, use: + +```bash +bun run app.js --help +``` + +## Building a Static Binary + +Bun allows you to build your application as a static binary, which can be distributed and run without requiring a separate runtime environment. To build the Sentiment Analyzer as a binary, follow these steps: + +1. **Build the Binary**: Run the following command in your terminal: + + ```bash + bun build app.js --outdir ./bin --target node + ``` + + This command compiles your application into a single binary executable for Node.js and places it in the `./bin` directory. + +2. **Run the Binary**: After building, you can run the binary directly: + + ```bash + ./bin/app.js <url> + ``` + + Or for multiple URLs: + + ```bash + ./bin/app.js <url1> <url2> <url3> + ``` + +### Example + +```bash +./bin/app.js https://example.com/blog-post +``` + +## Extending the Program + +The Sentiment Analyzer is designed to be extensible. Here are some ways you can enhance its functionality: + +1. **Add More Dictionaries**: You can extend the positive and negative word dictionaries by adding more words or phrases relevant to specific contexts or industries. + +2. **Enhance Emotion Categories**: Modify the `emotionCategories` object in `app.js` to include additional emotions or synonyms that are relevant to your analysis needs. + +3. **Implement Machine Learning**: Consider integrating machine learning models for more advanced sentiment analysis that can learn from context and improve over time. + +4. **Support for Multiple Languages**: Extend the program to support sentiment analysis in different languages by adding language-specific dictionaries and rules. + +5. **Dynamic Content Handling**: Improve the content extraction logic to handle dynamic web pages (Single Page Applications) that load content asynchronously. + +6. **Batch Processing**: Implement functionality to read URLs from a file and process them in batches, which can be useful for analyzing large datasets. + +7. **Output Formatting Options**: Add options to format the output in different ways (e.g., JSON, CSV) for easier integration with other tools or systems. + +## Contributing + +Contributions are welcome! If you have suggestions for improvements or new features, feel free to open an issue or submit a pull request. + +## License + +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. + +## Acknowledgments + +This project was created using `bun init` in bun v1.1.29. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime. diff --git a/js/sentiment/app.js b/js/sentiment/app.js new file mode 100644 index 0000000..66735fd --- /dev/null +++ b/js/sentiment/app.js @@ -0,0 +1,757 @@ +/** + * Web Sentiment Analyzer + * + * This program analyzes the sentiment of web content using a combination of: + * - Dictionary-based sentiment analysis + * - Emotion categorization + * - Intensity analysis + * - Web content extraction + * - Metadata parsing + * + * Architecture Overview: + * - Factory Pattern: Uses createWebSentimentAnalyzer to create analyzer instances + * - Builder Pattern: Configurable through dictionary additions and modifications + * - Strategy Pattern: Separates content fetching, analysis, and display logic + * - Command Pattern: CLI interface for processing multiple URLs + * + * @module sentiment-analyzer + */ + +import { JSDOM } from 'jsdom'; + +/** + * Creates a web-enabled sentiment analyzer with extended capabilities + * + * @param {Object} config - Optional configuration to override default dictionaries + * @returns {Object} An analyzer instance with public methods for sentiment analysis + * + * Extensibility Points: + * - Add more dictionaries (e.g., industry-specific terms) + * - Enhance emotion categories + * - Add language support + * - Implement ML-based sentiment analysis + */ +const createWebSentimentAnalyzer = (config = {}) => { + /** + * Default configuration with extensive sentiment dictionaries + * + * @property {Set} positiveWords - Words indicating positive sentiment + * @property {Set} negativeWords - Words indicating negative sentiment + * @property {Map} intensifiers - Words that modify sentiment intensity + * @property {Set} negators - Words that negate sentiment + * + * Potential Enhancements: + * - Add multi-word phrases + * - Include context-dependent sentiments + * - Add domain-specific dictionaries + */ + const defaultConfig = { + positiveWords: new Set([ + // Emotional positives + 'love', 'joy', 'happy', 'excited', 'peaceful', 'wonderful', 'fantastic', + 'delighted', 'pleased', 'glad', 'cheerful', 'content', 'satisfied', + 'grateful', 'thankful', 'blessed', 'optimistic', 'hopeful', + + // Quality positives + 'excellent', 'outstanding', 'superb', 'magnificent', 'brilliant', + 'exceptional', 'perfect', 'remarkable', 'spectacular', 'impressive', + 'incredible', 'amazing', 'extraordinary', 'marvelous', 'wonderful', + + // Performance positives + 'efficient', 'effective', 'reliable', 'innovative', 'productive', + 'successful', 'accomplished', 'achieved', 'improved', 'enhanced', + 'optimized', 'streamlined', 'breakthrough', 'revolutionary', + + // Relationship positives + 'friendly', 'helpful', 'supportive', 'kind', 'generous', 'caring', + 'compassionate', 'thoughtful', 'considerate', 'engaging', 'collaborative', + + // Experience positives + 'enjoyable', 'fun', 'entertaining', 'engaging', 'interesting', 'fascinating', + 'captivating', 'inspiring', 'motivating', 'enriching', 'rewarding', + + // Growth positives + 'growing', 'improving', 'developing', 'advancing', 'progressing', + 'evolving', 'flourishing', 'thriving', 'prospering', 'succeeding' + ]), + + negativeWords: new Set([ + // Emotional negatives + 'hate', 'angry', 'sad', 'upset', 'frustrated', 'disappointed', 'anxious', + 'worried', 'stressed', 'depressed', 'miserable', 'unhappy', 'distressed', + 'irritated', 'annoyed', 'furious', 'outraged', 'bitter', + + // Quality negatives + 'poor', 'bad', 'terrible', 'horrible', 'awful', 'dreadful', 'inferior', + 'mediocre', 'subpar', 'unacceptable', 'disappointing', 'inadequate', + 'deficient', 'flawed', 'defective', + + // Performance negatives + 'inefficient', 'ineffective', 'unreliable', 'problematic', 'failing', + 'broken', 'malfunctioning', 'corrupted', 'crashed', 'buggy', 'error', + 'failed', 'unsuccessful', 'unproductive', + + // Relationship negatives + 'hostile', 'unfriendly', 'unhelpful', 'rude', 'mean', 'cruel', 'harsh', + 'inconsiderate', 'selfish', 'aggressive', 'confrontational', 'toxic', + + // Experience negatives + 'boring', 'dull', 'tedious', 'monotonous', 'uninteresting', 'tiresome', + 'exhausting', 'frustrating', 'confusing', 'complicated', 'difficult', + + // Decline negatives + 'declining', 'deteriorating', 'worsening', 'failing', 'regressing', + 'degrading', 'diminishing', 'decreasing', 'falling', 'shrinking' + ]), + + intensifiers: new Map([ + // Strong intensifiers + ['extremely', 2.0], + ['absolutely', 2.0], + ['completely', 2.0], + ['totally', 2.0], + ['entirely', 2.0], + ['utterly', 2.0], + + // Moderate intensifiers + ['very', 1.5], + ['really', 1.5], + ['particularly', 1.5], + ['especially', 1.5], + ['notably', 1.5], + ['significantly', 1.5], + + // Mild intensifiers + ['quite', 1.25], + ['rather', 1.25], + ['somewhat', 1.25], + ['fairly', 1.25], + ['pretty', 1.25], + ['relatively', 1.25], + + // Emphatic phrases + ['without a doubt', 2.0], + ['beyond question', 2.0], + ['by far', 1.75], + ['to a great extent', 1.75] + ]), + + negators: new Set([ + // Direct negators + 'not', 'no', 'never', 'none', 'neither', 'nor', 'nothing', + + // Contracted negators + "n't", 'cannot', "won't", "wouldn't", "shouldn't", "couldn't", "haven't", + "hasn't", "didn't", "isn't", "aren't", "weren't", + + // Complex negators + 'hardly', 'scarcely', 'barely', 'rarely', 'seldom', 'few', 'little', + 'nowhere', 'nobody', 'none', 'by no means', 'on no account', + + // Implicit negators + 'deny', 'reject', 'refuse', 'prevent', 'avoid', 'stop', 'exclude', + 'doubt', 'question', 'dispute' + ]) + }; + + // Merge with provided config + const finalConfig = { ...defaultConfig, ...config }; + + /** + * Core sentiment analyzer implementation + * + * @param {Object} config - Configuration object with word dictionaries + * @returns {Object} Methods for text analysis + * + * Implementation Notes: + * - Uses a sliding window approach for context analysis + * - Implements multiplier-based intensity scoring + * - Categorizes emotions using predefined taxonomies + */ + const createSentimentAnalyzer = (config) => { + /** + * Main text analysis function + * + * @param {string} text - Text to analyze + * @returns {Object} Comprehensive analysis results + * + * Potential Improvements: + * - Add sentence-level analysis + * - Implement paragraph breakdown + * - Add statistical confidence scores + * - Consider word positioning and emphasis + */ + const analyzeText = (text) => { + // Ensure text is a string and has content + if (!text || typeof text !== 'string') { + console.warn('Invalid input to analyzeText:', text); + return { + score: 0, + words: [], + summary: { positive: 0, negative: 0, neutral: 0 }, + sentiment: 'Neutral', + topEmotions: [], + intensity: 'None', + wordCount: 0 + }; + } + + const words = text.toLowerCase() + .replace(/[.,\/#!$%\^&\*;:{}=\-_`~()]/g, '') + .split(/\s+/); + + let score = 0; + let multiplier = 1; + const analyzedWords = []; + const emotionCounts = new Map(); + let positiveCount = 0; + let negativeCount = 0; + let intensifierCount = 0; + + // Emotion categories for classification + const emotionCategories = { + // Positive Emotions + joy: ['happy', 'joy', 'delighted', 'pleased', 'excited', 'ecstatic', 'elated', 'jubilant', 'thrilled', 'overjoyed'], + gratitude: ['grateful', 'thankful', 'blessed', 'appreciative', 'indebted', 'humbled', 'moved'], + satisfaction: ['content', 'satisfied', 'fulfilled', 'pleased', 'accomplished', 'proud', 'complete'], + optimism: ['optimistic', 'hopeful', 'promising', 'confident', 'assured', 'encouraged', 'positive'], + serenity: ['peaceful', 'calm', 'tranquil', 'relaxed', 'serene', 'composed', 'centered'], + amusement: ['fun', 'funny', 'amused', 'entertained', 'playful', 'silly', 'humorous', 'laughing'], + interest: ['curious', 'intrigued', 'fascinated', 'engaged', 'absorbed', 'captivated', 'inspired'], + admiration: ['impressed', 'awed', 'amazed', 'respected', 'valued', 'esteemed', 'revered'], + love: ['loving', 'adoring', 'fond', 'affectionate', 'caring', 'cherished', 'devoted'], + + // Negative Emotions + frustration: ['frustrated', 'annoyed', 'irritated', 'agitated', 'exasperated', 'thwarted', 'hindered'], + concern: ['worried', 'concerned', 'anxious', 'uneasy', 'apprehensive', 'troubled', 'disturbed'], + disappointment: ['disappointed', 'letdown', 'dissatisfied', 'disheartened', 'dismayed', 'unfulfilled'], + anger: ['angry', 'furious', 'outraged', 'enraged', 'hostile', 'irate', 'livid', 'incensed'], + sadness: ['sad', 'unhappy', 'sorrowful', 'depressed', 'melancholy', 'gloomy', 'heartbroken'], + fear: ['afraid', 'scared', 'fearful', 'terrified', 'panicked', 'petrified', 'dreading'], + confusion: ['confused', 'puzzled', 'perplexed', 'bewildered', 'disoriented', 'uncertain', 'unclear'], + regret: ['regretful', 'sorry', 'remorseful', 'guilty', 'apologetic', 'ashamed', 'contrite'], + + // Complex Emotions + anticipation: ['eager', 'anticipating', 'expecting', 'awaiting', 'looking forward', 'preparing'], + surprise: ['surprised', 'astonished', 'startled', 'shocked', 'stunned', 'unexpected', 'remarkable'], + nostalgia: ['nostalgic', 'reminiscent', 'remembering', 'longing', 'wistful', 'retrospective'], + determination: ['determined', 'resolved', 'committed', 'focused', 'dedicated', 'persistent', 'steadfast'], + relief: ['relieved', 'reassured', 'unburdened', 'comforted', 'calmed', 'settled', 'eased'], + ambivalence: ['conflicted', 'uncertain', 'unsure', 'mixed feelings', 'undecided', 'torn'], + + // Professional/Work-Related + confidence: ['confident', 'capable', 'competent', 'skilled', 'proficient', 'qualified', 'expert'], + motivation: ['motivated', 'driven', 'inspired', 'energized', 'enthusiastic', 'passionate', 'eager'], + productivity: ['productive', 'efficient', 'effective', 'accomplished', 'successful', 'achieving'], + collaboration: ['collaborative', 'cooperative', 'supportive', 'helpful', 'team-oriented', 'united'], + + // Growth/Learning + growth: ['growing', 'developing', 'improving', 'progressing', 'advancing', 'learning', 'evolving'], + curiosity: ['curious', 'inquisitive', 'interested', 'exploring', 'discovering', 'wondering'], + insight: ['understanding', 'realizing', 'comprehending', 'grasping', 'enlightened', 'aware'] + }; + + for (let i = 0; i < words.length; i++) { + const word = words[i]; + let wordImpact = { + word, + score: 0, + multiplier, + category: null + }; + + // Check for intensifiers + if (config.intensifiers.has(word)) { + multiplier = config.intensifiers.get(word); + intensifierCount++; + continue; + } + + // Check for negators + if (config.negators.has(word)) { + multiplier *= -1; + continue; + } + + // Score the word and categorize emotions + if (config.positiveWords.has(word)) { + const wordScore = 1 * multiplier; + score += wordScore; + positiveCount++; + wordImpact.score = wordScore; + + // Categorize emotion + for (const [category, keywords] of Object.entries(emotionCategories)) { + if (keywords.includes(word)) { + wordImpact.category = category; + emotionCounts.set(category, (emotionCounts.get(category) || 0) + 1); + break; + } + } + + } else if (config.negativeWords.has(word)) { + const wordScore = -1 * multiplier; + score += wordScore; + negativeCount++; + wordImpact.score = wordScore; + + // Categorize emotion + for (const [category, keywords] of Object.entries(emotionCategories)) { + if (keywords.includes(word)) { + wordImpact.category = category; + emotionCounts.set(category, (emotionCounts.get(category) || 0) + 1); + break; + } + } + } + + if (wordImpact.score !== 0 || wordImpact.category) { + analyzedWords.push(wordImpact); + } + + // Reset multiplier after scoring a word + multiplier = 1; + } + + // Calculate intensity based on score magnitude and intensifier usage + 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'; + }; + + // Get top emotions + const topEmotions = Array.from(emotionCounts.entries()) + .sort((a, b) => b[1] - a[1]) + .slice(0, 3) + .map(([emotion, count]) => ({ emotion, count })); + + return { + score, + words: analyzedWords, + summary: { + positive: positiveCount, + negative: negativeCount, + neutral: words.length - positiveCount - negativeCount, + total: words.length + }, + sentiment: getEmotionalTone(score), + topEmotions, + intensity: getIntensity(score, intensifierCount), + wordCount: words.length, + averageSentiment: score / words.length || 0 + }; + }; + + return { + analyzeText, + calculateSentimentScore: (text) => analyzeText(text).score, + getEmotionalTone: (text) => analyzeText(text).sentiment, + getTopWords: (text) => analyzeText(text).words + }; + }; + + // Re-use previous sentiment analysis functions + const { analyzeText, calculateSentimentScore, getEmotionalTone, getTopWords } = createSentimentAnalyzer(finalConfig); + + /** + * Fetches and extracts content from web pages + * + * @param {string} url - URL to analyze + * @returns {Promise<string>} Extracted text content + * + * Implementation Notes: + * - Uses progressive enhancement for content selection + * - Implements fallback strategies for content extraction + * - Handles various DOM structures + * + * Potential Enhancements: + * - Add support for dynamic content (SPA) + * - Implement content cleaning rules + * - Add support for paywalled content + * - Handle rate limiting + */ + const fetchContent = async (url) => { + try { + const response = await fetch(url); + const html = await response.text(); + + const dom = new JSDOM(html); + const doc = dom.window.document; + + // Enhanced content selectors + const contentSelectors = [ + 'article', + 'main', + '.content', + '.post-content', + '.entry-content', + '.article-content', + '.blog-post', + '.post', + 'article p', + '.content p', + 'p' + ]; + + let content = ''; + for (const selector of contentSelectors) { + const elements = doc.querySelectorAll(selector); + if (elements.length) { + elements.forEach(el => { + // Skip if element contains mostly navigation/header/footer content + if (el.closest('nav') || el.closest('header') || el.closest('footer')) { + return; + } + content += el.textContent + '\n\n'; + }); + if (content.trim().length > 0) break; + } + } + + // If no content was found through selectors, get all text content + if (!content) { + content = doc.body.textContent || ''; + } + + // Clean up the content + content = content + .replace(/\s+/g, ' ') + .replace(/\n\s*\n/g, '\n\n') + .trim(); + + // Ensure we're returning a string + return content || ''; + + } catch (error) { + console.error('Fetch error:', error); + throw new Error(`Failed to fetch content: ${error.message}`); + } + }; + + /** + * Extracts metadata from web documents + * + * @param {Document} doc - DOM document + * @returns {Object} Extracted metadata + * + * Implementation Notes: + * - Supports multiple metadata formats (meta tags, OpenGraph, etc.) + * - Uses fallback strategies for missing data + * + * Potential Improvements: + * - Add schema.org parsing + * - Support more metadata formats + * - Add validation and cleaning + */ + const extractMetadata = (doc) => { + const metadata = { + title: '', + description: '', + author: '', + date: '', + keywords: [] + }; + + // Extract meta tags with enhanced selectors + const metaTags = doc.querySelectorAll('meta'); + metaTags.forEach(tag => { + const name = tag.getAttribute('name')?.toLowerCase(); + const property = tag.getAttribute('property')?.toLowerCase(); + const content = tag.getAttribute('content'); + + if (content) { + if (name === 'description' || property === 'og:description') { + metadata.description = content; + } + if (name === 'author' || property === 'article:author') { + metadata.author = content; + } + if (name === 'keywords') { + metadata.keywords = content.split(',').map(k => k.trim()); + } + if (name === 'date' || property === 'article:published_time' || + property === 'article:modified_time') { + metadata.date = content; + } + } + }); + + // Try different title sources + metadata.title = doc.querySelector('meta[property="og:title"]')?.getAttribute('content') || + doc.querySelector('h1')?.textContent || + doc.title || ''; + + // Try to find author in structured data + const authorElement = doc.querySelector('[rel="author"], .author, .byline'); + if (authorElement && !metadata.author) { + metadata.author = authorElement.textContent.trim(); + } + + // Try to find date in structured data + if (!metadata.date) { + const dateElement = doc.querySelector('time, .date, .published'); + if (dateElement) { + metadata.date = dateElement.getAttribute('datetime') || dateElement.textContent.trim(); + } + } + + return metadata; + }; + + /** + * Analyzes sentiment of web page content + * + * @param {string} url - URL to analyze + * @returns {Promise<Object>} Complete analysis results + * + * Potential Enhancements: + * - Add caching + * - Implement batch processing + * - Add historical tracking + */ + const analyzeUrl = async (url) => { + try { + const content = await fetchContent(url); + + if (!content) { + console.warn(`No content found for URL: ${url}`); + return { + score: 0, + words: [], + summary: { positive: 0, negative: 0, neutral: 0 }, + sentiment: 'Neutral', + topEmotions: [], + intensity: 'None', + wordCount: 0, + url, + metadata: {}, + fetchDate: new Date().toISOString() + }; + } + + const analysis = analyzeText(content); + + // Create a new JSDOM instance for metadata extraction + const response = await fetch(url); + const html = await response.text(); + const dom = new JSDOM(html); + + // Additional URL-specific analysis + analysis.url = url; + analysis.metadata = extractMetadata(dom.window.document); + analysis.fetchDate = new Date().toISOString(); + + return analysis; + } catch (error) { + console.error('Analysis error:', error); + throw new Error(`Analysis failed: ${error.message}`); + } + }; + + // Enhanced API + return { + analyzeText, + analyzeUrl, + addPositiveWords: (words) => words.forEach(word => finalConfig.positiveWords.add(word)), + addNegativeWords: (words) => words.forEach(word => finalConfig.negativeWords.add(word)), + addIntensifier: (word, multiplier) => finalConfig.intensifiers.set(word, multiplier), + addNegator: (word) => finalConfig.negators.add(word), + getConfig: () => ({ ...finalConfig }), + getDictionaries: () => ({ + positiveCount: finalConfig.positiveWords.size, + negativeCount: finalConfig.negativeWords.size, + intensifierCount: finalConfig.intensifiers.size, + negatorCount: finalConfig.negators.size + }) + }; + }; + + // Example usage: + const analyzer = createWebSentimentAnalyzer(); + + /** + * Creates a visual representation of sentiment score + * + * @param {number} score - Sentiment score to visualize + * @returns {string} ASCII visualization of sentiment scale + * + * Design Notes: + * - Uses Unicode characters for better visualization + * - Implements fixed-width scale for consistent display + * + * Potential Enhancements: + * - Add color support + * - Implement alternative visualizations + * - Add interactive elements + */ + const createSentimentScale = (score) => { + const width = 40; // Width of the scale + const middle = Math.floor(width / 2); + // Clamp score between -10 and 10 for display purposes + const clampedScore = Math.max(-10, Math.min(10, score)); + const position = Math.round(middle + (clampedScore * middle / 10)); + + let scale = ''; + for (let i = 0; i < width; i++) { + if (i === middle) scale += '│'; // Using Unicode box drawing character + else if (i === position) scale += '●'; + else scale += '─'; + } + + // Simpler scale display without arrows and extra spacing + return ` +NEGATIVE ${' '.repeat(middle-5)}NEUTRAL${' '.repeat(middle-5)} POSITIVE +[-10] ${scale} [+10] +Score: ${score.toFixed(2)} +`; + }; + + /** + * Formats analysis results for human readability + * + * @param {Object} analysis - Analysis results to format + * @returns {string} Formatted analysis report + * + * Implementation Notes: + * - Uses structured format for consistency + * - Implements progressive disclosure of details + * + * Potential Improvements: + * - Add output format options (JSON, CSV, etc.) + * - Implement templating system + * - Add internationalization support + */ + const formatAnalysisResults = (analysis) => { + const { + score, + summary, + sentiment, + topEmotions, + intensity, + wordCount, + metadata, + url + } = analysis; + + return ` +=== Sentiment Analysis for ${metadata.title || url} === + +${createSentimentScale(score)} + +Overall Assessment: +• Sentiment: ${sentiment} (${intensity}) +• Total Words Analyzed: ${wordCount} + +Word Breakdown: +• Positive Words: ${summary.positive} +• Negative Words: ${summary.negative} +• Neutral Words: ${summary.neutral} + +${topEmotions.length ? `Dominant Emotions: +${topEmotions.map(e => `• ${e.emotion} (mentioned ${e.count} time${e.count > 1 ? 's' : ''})`).join('\n')}` : ''} + +Content Details: +• Author: ${metadata.author || 'Not specified'} +• Date: ${metadata.date || 'Not specified'} +${metadata.description ? `• Description: ${metadata.description}` : ''} + +Notable Words: +${analysis.words + .filter(w => w.score !== 0) + .slice(0, 5) + .map(w => `• "${w.word}" (${w.score > 0 ? 'positive' : 'negative'}, ${w.category || 'general'})`) + .join('\n')} + +${'-'.repeat(60)} +`; + }; + + // Update the analyzeWebPage function + const analyzeWebPage = async (url) => { + try { + const analysis = await analyzer.analyzeUrl(url); + console.log(formatAnalysisResults(analysis)); + } catch (error) { + console.error(`\n❌ Analysis failed for ${url}:`, error.message); + } + }; + + // Example: + // analyzeWebPage('https://example.com/blog-post'); + + // Add custom words +// analyzer.addPositiveWords(['groundbreaking', 'game-changing']); +// analyzer.addNegativeWords(['concerning', 'questionable']); +// analyzer.addIntensifier('incredibly', 1.8); +// analyzer.addNegator('lacks'); + +// // Get dictionary stats +// console.log(analyzer.getDictionaries()); + +// Remove the hard-coded URLs and add CLI handling +const helpText = ` +Sentiment Analyzer +================= + +Analyzes the sentiment of web pages and provides detailed emotional analysis. + +Usage: + bun run app.js <url> + bun run app.js <url1> <url2> <url3> ... + +Example: + bun run app.js https://example.com/blog-post + bun run app.js https://blog1.com https://blog2.com + +Options: + --help, -h Show this help message +`; + +/** + * CLI program entry point + * + * Implementation Notes: + * - Uses async/await for proper error handling + * - Implements command pattern for URL processing + * + * Potential Enhancements: + * - Add configuration file support + * - Implement batch processing from file + * - Add progress indicators + * - Add output formatting options + */ +const main = async () => { + // Get command line arguments (skip first two as they're node/bun and script path) + const args = process.argv.slice(2); + + // Show help if no arguments or help flag + if (args.length === 0 || args.includes('--help') || args.includes('-h')) { + console.log(helpText); + return; + } + + // Create analyzer instance + const analyzer = createWebSentimentAnalyzer(); + + // Analyze each URL + for (const url of args) { + try { + // Skip any help flags that might have been passed + if (url.startsWith('-')) continue; + + await analyzeWebPage(url); + } catch (error) { + console.error(`\n❌ Failed to analyze ${url}:`, error.message); + } + } +}; + +// Run the program +main().catch(error => { + console.error('Fatal error:', error); + process.exit(1); +}); \ No newline at end of file diff --git a/js/sentiment/bun.lockb b/js/sentiment/bun.lockb new file mode 100755 index 0000000..e7e3ab3 --- /dev/null +++ b/js/sentiment/bun.lockb Binary files differdiff --git a/js/sentiment/jsconfig.json b/js/sentiment/jsconfig.json new file mode 100644 index 0000000..238655f --- /dev/null +++ b/js/sentiment/jsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + // Enable latest features + "lib": ["ESNext", "DOM"], + "target": "ESNext", + "module": "ESNext", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + + // Bundler mode + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + + // Best practices + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + + // Some stricter flags (disabled by default) + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false + } +} diff --git a/js/sentiment/package.json b/js/sentiment/package.json new file mode 100644 index 0000000..c55b3ee --- /dev/null +++ b/js/sentiment/package.json @@ -0,0 +1,14 @@ +{ + "name": "sentiment", + "module": "app.js", + "type": "module", + "devDependencies": { + "@types/bun": "latest" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "dependencies": { + "jsdom": "^26.0.0" + } +} \ No newline at end of file |