Files
ai.interactive.fiction/public/js/text-processor-module.js

327 lines
12 KiB
JavaScript

/**
* 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',
'normalizeHyphenationLocale',
'applyLocaleTypography',
'getTypographyLocale',
'normalizeDialogueQuotes'
]);
}
/**
* Initialize the module
* @returns {Promise<boolean>} - 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<void>}
*/
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<boolean>} - 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').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 };