316 lines
11 KiB
JavaScript
316 lines
11 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';
|
|
|
|
// 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<boolean>} - 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<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);
|
|
}
|
|
|
|
// 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 };
|