/** * Text Processor Module * Handles text formatting and typography enhancements like smart quotes and hyphenation */ import { BaseModule } from './base-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', 'loadHyphenopolyLoader', 'normalizeHyphenationLocale' ]); } /** * 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 the browser Hyphenopoly loader used by the prototype. * @returns {Promise} - Resolves when hyphenation is initialized */ async initializeHyphenation() { try { console.log("Initializing hyphenation with browser Hyphenopoly loader"); const locale = this.normalizeHyphenationLocale(this.locale); this.hyphenator = null; this.hyphenatorReady = false; await this.loadHyphenopolyLoader(); window.Hyphenopoly.config({ require: { [locale]: "FORCEHYPHENOPOLY" }, paths: { maindir: "/js/", patterndir: "/js/patterns/" }, setup: { hide: "element", selectors: { ".hyphenate": { hyphen: "\u00AD" }, ".hyphenatePipe": { hyphen: "|" } } }, handleEvent: { error: (event) => { console.warn(`Hyphenopoly error: ${event.msg || event.message || event.type}`); }, engineReady: (event) => { console.log(`Hyphenopoly engine ready: ${event.msg || locale}`); } } }); this.hyphenator = await window.Hyphenopoly.hyphenators[locale]; this.hyphenatorReady = true; this.locale = locale; console.log(`Hyphenator ready for ${locale}`); document.dispatchEvent(new CustomEvent('hyphenation-loaded')); return true; } catch (error) { this.hyphenator = null; this.hyphenatorReady = false; console.error("Failed to initialize Hyphenopoly browser hyphenation:", error); throw error; } } loadHyphenopolyLoader() { return new Promise((resolve, reject) => { if (window.Hyphenopoly && typeof window.Hyphenopoly.config === 'function') { resolve(); return; } const existingScript = document.querySelector('script[src="/js/Hyphenopoly_Loader.js"]'); if (existingScript) { existingScript.addEventListener('load', () => resolve(), { once: true }); existingScript.addEventListener('error', () => reject(new Error('Failed to load Hyphenopoly loader')), { once: true }); return; } const script = document.createElement('script'); script.src = '/js/Hyphenopoly_Loader.js'; script.async = true; script.onload = () => resolve(); script.onerror = () => reject(new Error('Failed to load Hyphenopoly loader')); document.head.appendChild(script); }); } normalizeHyphenationLocale(locale) { const normalized = String(locale || 'en-us').toLowerCase(); if (normalized === 'en') return 'en-us'; if (normalized === 'de-de') return 'de'; return normalized; } /** * 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 * @param {string} selector - Optional selector for Hyphenopoly * @returns {string} - The hyphenated text */ hyphenate(text, selector = null) { if (!this.isHyphenationAvailable()) { return text; } try { // If selector provided, pass it to hyphenator return selector ? this.hyphenator(text, selector) : 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 * @param {string} [options.hyphenSelector=null] - Selector for hyphen character (e.g., '.hyphenatePipe') * @returns {string} - The processed text */ process(text, options = {}) { const opts = { smartypants: true, hyphenate: true, hyphenSelector: null, ...options }; let result = text; // Apply SmartyPants if available and requested if (opts.smartypants && this.smartypantsu) { result = this.smartypantsu(result, 1); } else if (opts.smartypants && this.smartyPants) { result = this.smartyPants(result); } // Apply hyphenation if available and requested if (opts.hyphenate && this.isHyphenationAvailable()) { result = this.hyphenate(result, opts.hyphenSelector); } return result; } } // Create the singleton instance const TextProcessor = new TextProcessorModule(); // Export the module export { TextProcessor };