/** * 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'; this.dependencies = ['localization', 'game-config']; // Bind methods using parent's bindMethods utility this.bindMethods([ 'loadSmartyPantsScript', 'initializeHyphenation', 'process', 'isHyphenationAvailable', 'hyphenate', 'setLocale', 'loadHyphenopolyLoader', 'ensureHyphenopolySeedElements', 'normalizeHyphenationLocale', 'applyLocaleTypography', 'getTypographyLocale', 'normalizeDialogueQuotes' ]); } /** * Initialize the module * @returns {Promise} - Resolves with success status */ async initialize() { try { this.reportProgress(10, "Initializing text processor"); const gameConfig = this.getModule('game-config'); this.locale = gameConfig?.getLocale?.() || 'en_US'; this.addEventListener(document, 'game:config', (event) => { const gameLocale = event.detail?.metadata?.language || event.detail?.locale; if (gameLocale) { this.setLocale(gameLocale); } }); 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; } } /** * 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(); this.ensureHyphenopolySeedElements(locale); 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; } } ensureHyphenopolySeedElements(locale = 'en-us') { const normalizedLocale = this.normalizeHyphenationLocale(locale); let container = document.getElementById('hyphenopoly_seed_elements'); if (!container) { container = document.createElement('div'); container.id = 'hyphenopoly_seed_elements'; container.setAttribute('aria-hidden', 'true'); Object.assign(container.style, { position: 'absolute', width: '1px', height: '1px', overflow: 'hidden', opacity: '0', pointerEvents: 'none', left: '-9999px', top: '-9999px' }); document.body.appendChild(container); } container.innerHTML = ''; ['hyphenate', 'hyphenatePipe'].forEach((className) => { const seed = document.createElement('span'); seed.className = className; seed.lang = normalizedLocale; seed.textContent = normalizedLocale.startsWith('de') ? 'Silbentrennung' : 'hyphenation'; container.appendChild(seed); }); } 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').trim().toLowerCase().replace('_', '-'); 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); } if (opts.smartypants) { result = this.applyLocaleTypography(result); } // Apply hyphenation if available and requested if (opts.hyphenate && this.isHyphenationAvailable()) { result = this.hyphenate(result, opts.hyphenSelector); } return result; } applyLocaleTypography(text) { const locale = this.getTypographyLocale(); if (locale.startsWith('de')) { return this.normalizeDialogueQuotes(text); } return text; } getTypographyLocale() { return String(this.locale || 'en_US').trim().toLowerCase().replace('_', '-'); } normalizeDialogueQuotes(text) { return String(text || '') .replace(/&(ldquo|bdquo|laquo|raquo);([^&\n]+?)&(rdquo|ldquo|laquo|raquo);/gi, '»$2«') .replace(/["\u201c\u201e\u201d\u00ab\u00bb]([^"\u201c\u201e\u201d\u00ab\u00bb\n]+?)["\u201c\u201d\u201e\u00ab\u00bb]/g, '»$1«'); } } // Create the singleton instance const TextProcessor = new TextProcessorModule(); // Export the module export { TextProcessor };