/** * Text Processor Module * Handles text formatting and typography enhancements like smart quotes and hyphenation */ import { BaseModule } from './base-module.js'; import Hyphenopoly from './hyphenopoly.module.js'; class TextProcessorModule extends BaseModule { constructor() { super('text-processor', 'Text Processor'); this.smartyPants = null; this.smartypantsu = null; this.hyphenator = null; this.hyphenatorReady = false; this.locale = 'en-us'; // Add localization as a dependency this.dependencies = ['localization']; // Bind methods using parent's bindMethods utility this.bindMethods([ 'loadSmartyPantsScript', 'initializeHyphenation', 'process', 'isHyphenationAvailable', 'hyphenate', 'setLocale', 'handleLocaleChanged' ]); } /** * Initialize the module * @returns {Promise} - Resolves with success status */ async initialize() { try { this.reportProgress(10, "Initializing text processor"); // Get locale from Localization module if available const localizationModule = this.getModule('localization'); if (!localizationModule) { console.error("Localization module not found, required dependency missing"); this.reportProgress(100, "Text processor initialization failed - missing localization"); return false; } this.locale = localizationModule.getLocale(); // Register for locale changes using the proper event pattern this.addEventListener(document, 'locale-changed', this.handleLocaleChanged); this.reportProgress(30, `Locale set to ${this.locale}`); // Ensure global locale is set for SmartyPants window.locale = this.locale; // Load SmartyPants - critical dependency this.reportProgress(40, "Loading SmartyPants"); try { await this.loadSmartyPantsScript(); // Verify SmartyPants is properly loaded if (!this.smartyPants || typeof this.smartyPants !== 'function') { throw new Error('SmartyPants not properly loaded'); } this.reportProgress(70, "SmartyPants loaded successfully"); } catch (error) { console.error("Failed to load SmartyPants:", error); this.reportProgress(100, "Text processor initialization failed - SmartyPants not available"); return false; } // Initialize hyphenation (non-critical) this.reportProgress(80, "Initializing hyphenation"); try { await this.initializeHyphenation(); this.reportProgress(100, "Text processor ready"); return true; } catch (error) { console.warn("Failed to initialize hyphenation:", error); // Continue without hyphenation, still mark as successful this.reportProgress(100, "Text processor ready (without hyphenation)"); return true; } } catch (error) { console.error("Error initializing text processor:", error); this.reportProgress(100, "Text processor initialization failed"); return false; } } /** * Handle locale changed event * @param {CustomEvent} event - The locale-changed event */ handleLocaleChanged(event) { if (event && event.detail && event.detail.locale) { this.setLocale(event.detail.locale); } } /** * Set the locale for the text processor * @param {string} locale - The locale to set */ setLocale(locale) { this.locale = locale; console.log(`Text processor locale set to ${locale}`); // Reinitialize hyphenation with new locale if needed if (this.hyphenatorReady) { this.initializeHyphenation(); } } /** * Load the SmartyPants script dynamically and wait for it to be ready * @returns {Promise} */ loadSmartyPantsScript() { return new Promise((resolve, reject) => { // Check if already loaded globally if (typeof window.SmartyPants === 'object' && typeof window.SmartyPants.smartypants === 'function') { this.smartyPants = window.SmartyPants.smartypants; this.smartypantsu = window.SmartyPants.smartypantsu; console.log("SmartyPants already loaded globally"); resolve(); return; } // Create script element const script = document.createElement('script'); script.type = 'text/javascript'; script.src = '/js/smartypants.js'; // Use relative URL script.async = true; // Set up load and error handlers script.onload = () => { if (typeof window.SmartyPants === 'object' && typeof window.SmartyPants.smartypants === 'function') { this.smartyPants = window.SmartyPants.smartypants; this.smartypantsu = window.SmartyPants.smartypantsu; console.log("SmartyPants loaded successfully"); resolve(); } else { const error = new Error('SmartyPants loaded but functions not found'); console.error(error); reject(error); } }; script.onerror = (error) => { console.error("Error loading SmartyPants script:", error); reject(new Error('Failed to load SmartyPants script')); }; // Add script to document document.head.appendChild(script); }); } /** * Initialize hyphenation using Hyphenopoly module * @returns {Promise} - Resolves when hyphenation is initialized */ initializeHyphenation() { return new Promise((resolve, reject) => { try { console.log("Initializing hyphenation with Hyphenopoly module"); // Configure Hyphenopoly with our requirements const hyphenatorPromise = Hyphenopoly.config({ require: [this.locale], hyphen: '\u00AD', // Soft hyphen character minWordLength: 5, leftmin: 2, rightmin: 2, compound: "hyphen", // Define a custom loader for the patterns loader: (file) => { return new Promise((resolve, reject) => { // Determine correct pattern file based on locale let patternFile = file; // Special handling for 'en' locale - use en-us.wasm if available if (file === 'en.wasm') { patternFile = 'en-us.wasm'; } const patternPath = `/js/patterns/${patternFile}`; console.log(`Loading hyphenation pattern: ${patternPath}`); fetch(patternPath) .then(response => { if (!response.ok) { throw new Error(`Failed to load ${file}: ${response.status} ${response.statusText}`); } return response.arrayBuffer(); }) .then(arrayBuffer => { resolve(arrayBuffer); }) .catch(error => { console.error(`Error loading hyphenation pattern ${file}:`, error); reject(error); }); }); }, handleEvent: { error: (e) => { console.warn(`Hyphenopoly error: ${e.msg}`); }, engineReady: (e) => { console.log(`Hyphenopoly engine ready for ${e.msg}`); } } }); // Get the hyphenator for our locale hyphenatorPromise.get(this.locale) .then(hyphenator => { this.hyphenator = hyphenator; this.hyphenatorReady = true; console.log(`Hyphenator ready for ${this.locale}`); // Dispatch event that hyphenation is ready document.dispatchEvent(new CustomEvent('hyphenation-loaded')); resolve(true); // Successfully initialized }) .catch(error => { console.error(`Failed to initialize hyphenator for ${this.locale}:`, error); // Try to fall back to en-us if the current locale failed if (this.locale !== 'en-us') { console.log("Falling back to en-us hyphenation"); return hyphenatorPromise.get('en-us'); } throw error; }) .then(fallbackHyphenator => { if (fallbackHyphenator) { this.hyphenator = fallbackHyphenator; this.hyphenatorReady = true; console.log("Using fallback en-us hyphenator"); // Dispatch event that hyphenation is ready document.dispatchEvent(new CustomEvent('hyphenation-loaded')); resolve(true); // Successfully initialized with fallback } }) .catch(error => { console.error("Failed to initialize hyphenation even with fallback:", error); reject(error); // Failed to initialize }); } catch (error) { console.error("Error setting up hyphenation:", error); reject(error); // Failed to initialize } }); } /** * Check if hyphenation is available * @returns {boolean} - True if hyphenation is available */ isHyphenationAvailable() { return this.hyphenatorReady && this.hyphenator !== null; } /** * Hyphenate a text using the Hyphenopoly module * @param {string} text - The text to hyphenate * @returns {string} - The hyphenated text */ hyphenate(text) { if (!this.isHyphenationAvailable()) { return text; } try { return this.hyphenator(text); } catch (error) { console.error("Error hyphenating text:", error); return text; } } /** * Process text with typography enhancements * @param {string} text - The text to process * @param {Object} options - Processing options * @param {boolean} [options.smartypants=true] - Whether to apply SmartyPants processing * @param {boolean} [options.hyphenate=true] - Whether to apply hyphenation * @returns {string} - The processed text */ process(text, options = {}) { const opts = { smartypants: true, hyphenate: true, ...options }; let result = text; // Apply SmartyPants if available and requested if (opts.smartypants && this.smartyPants) { result = this.smartyPants(result); } // Apply hyphenation if available and requested if (opts.hyphenate && this.isHyphenationAvailable()) { result = this.hyphenate(result); } return result; } } // Create the singleton instance const TextProcessor = new TextProcessorModule(); // Export the module export { TextProcessor };