297 lines
9.8 KiB
JavaScript
297 lines
9.8 KiB
JavaScript
/**
|
|
* Paragraph Layout Module
|
|
* Implements the Knuth and Plass line breaking algorithm for optimal typography
|
|
* and connects it to the text rendering pipeline.
|
|
*/
|
|
import { BaseModule } from './base-module.js';
|
|
|
|
class ParagraphLayoutModule extends BaseModule {
|
|
constructor() {
|
|
super('paragraph-layout', 'Paragraph Layout');
|
|
|
|
this.dependencies = ['text-processor'];
|
|
|
|
this.ruler = null;
|
|
this.rulerStack = [];
|
|
|
|
this.updateConfig({
|
|
maxLineWidth: 600,
|
|
hyphenationEnabled: true,
|
|
defaultFontSize: '1rem',
|
|
defaultFontFamily: "'EB Garamond', serif",
|
|
defaultFontFeatureSettings: "'kern' on, 'liga' on, 'onum' on, 'pnum' on, 'dlig' on, 'clig' on, 'calt' on",
|
|
defaultLineHeight: 1.5,
|
|
debugMode: false
|
|
});
|
|
|
|
this.bindMethods([
|
|
'calculateLayout',
|
|
'measureText',
|
|
'setDebugMode',
|
|
'updateFont',
|
|
'processTextForLayout',
|
|
'initializeTextMeasurement',
|
|
'setupEventListeners',
|
|
'loadLayoutDependencies'
|
|
]);
|
|
}
|
|
|
|
async initialize() {
|
|
try {
|
|
this.reportProgress(20, "Initializing paragraph layout");
|
|
|
|
const textProcessor = this.getModule('text-processor');
|
|
if (!textProcessor) {
|
|
console.warn("Paragraph Layout: Text Processor not found, will use unprocessed text");
|
|
}
|
|
|
|
await this.loadLayoutDependencies();
|
|
this.initializeTextMeasurement();
|
|
this.setupEventListeners();
|
|
|
|
this.reportProgress(100, "Paragraph layout ready");
|
|
return true;
|
|
} catch (error) {
|
|
console.error("Error initializing Paragraph Layout:", error);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
async loadLayoutDependencies() {
|
|
try {
|
|
this.reportProgress(30, "Loading layout dependencies");
|
|
|
|
await this.loadScript('/js/linked-list.js');
|
|
await this.loadScript('/js/linebreak.js');
|
|
await this.loadScript('/js/knuth-and-plass.js');
|
|
|
|
this.reportProgress(50, "Layout dependencies loaded");
|
|
return true;
|
|
} catch (error) {
|
|
console.error("Error loading layout dependencies:", error);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
initializeTextMeasurement() {
|
|
let ruler = document.getElementById('ruler');
|
|
|
|
if (!ruler) {
|
|
ruler = document.createElement('span');
|
|
ruler.id = 'ruler';
|
|
document.body.appendChild(ruler);
|
|
}
|
|
|
|
Object.assign(ruler.style, {
|
|
visibility: 'hidden',
|
|
position: 'absolute',
|
|
top: '-8000px',
|
|
left: '-8000px',
|
|
width: 'auto',
|
|
display: 'inline',
|
|
textIndent: '0',
|
|
textAlign: 'left',
|
|
hyphens: 'none',
|
|
marginBlockEnd: '0'
|
|
});
|
|
|
|
this.ruler = ruler;
|
|
this.rulerStack = [ruler];
|
|
this.updateFont(this.config.defaultFontSize, this.config.defaultFontFamily);
|
|
}
|
|
|
|
setupEventListeners() {
|
|
this.addEventListener(document, 'ui:font:change', (event) => {
|
|
if (event.detail) {
|
|
const { fontSize, fontFamily } = event.detail;
|
|
if (fontSize || fontFamily) {
|
|
this.updateFont(fontSize || this.config.defaultFontSize, fontFamily || this.config.defaultFontFamily);
|
|
}
|
|
}
|
|
});
|
|
|
|
this.addEventListener(document, 'ui:hyphenation:toggle', (event) => {
|
|
if (event.detail && typeof event.detail.enabled === 'boolean') {
|
|
this.updateConfig({ hyphenationEnabled: event.detail.enabled });
|
|
}
|
|
});
|
|
}
|
|
|
|
updateFont(fontSize, fontFamily, fontFeatureSettings = null, fontVariantCaps = null) {
|
|
if (!this.ruler) {
|
|
console.warn("Text measurement ruler not initialized");
|
|
return;
|
|
}
|
|
|
|
if (fontSize) this.updateConfig({ defaultFontSize: fontSize });
|
|
if (fontFamily) this.updateConfig({ defaultFontFamily: fontFamily });
|
|
|
|
this.ruler.style.fontSize = fontSize;
|
|
this.ruler.style.fontFamily = fontFamily;
|
|
this.ruler.style.fontFeatureSettings = fontFeatureSettings || this.config.defaultFontFeatureSettings;
|
|
this.ruler.style.fontVariantCaps = fontVariantCaps || 'normal';
|
|
|
|
if (this.config.debugMode) {
|
|
console.log(`Font updated: ${fontSize} ${fontFamily}`);
|
|
}
|
|
}
|
|
|
|
measureText(text) {
|
|
if (!this.ruler) return 0;
|
|
if (!text) return 0;
|
|
|
|
if (text.substr(0, 2) === '</') {
|
|
if (this.rulerStack.length > 1) {
|
|
const child = this.rulerStack.pop();
|
|
const parent = this.rulerStack[this.rulerStack.length - 1];
|
|
if (child.parentElement === parent) {
|
|
parent.removeChild(child);
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
if (text.substr(0, 1) === '<') {
|
|
const template = document.createElement('div');
|
|
template.innerHTML = text;
|
|
const child = template.firstChild;
|
|
|
|
if (child) {
|
|
const parent = this.rulerStack[this.rulerStack.length - 1];
|
|
this.rulerStack.push(child);
|
|
parent.appendChild(child);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
if (text === '|') return 0;
|
|
if (text === ' ') text = '\u00A0';
|
|
|
|
const parent = this.rulerStack[this.rulerStack.length - 1];
|
|
const textNode = document.createTextNode(text);
|
|
parent.appendChild(textNode);
|
|
const rect = parent.getClientRects()[0] || parent.getBoundingClientRect();
|
|
const width = rect ? rect.width : 0;
|
|
parent.removeChild(textNode);
|
|
|
|
return width;
|
|
}
|
|
|
|
processTextForLayout(text) {
|
|
if (!text) return '';
|
|
|
|
let processedText = text;
|
|
const textProcessor = this.getModule('text-processor');
|
|
|
|
if (textProcessor && typeof textProcessor.process === 'function') {
|
|
processedText = textProcessor.process(processedText, {
|
|
smartypants: true,
|
|
hyphenate: this.config.hyphenationEnabled,
|
|
hyphenSelector: '.hyphenatePipe'
|
|
});
|
|
} else if (this.config.debugMode) {
|
|
console.log("Text processor not available, skipping text processing");
|
|
}
|
|
|
|
return processedText;
|
|
}
|
|
|
|
calculateLayout(text, options = {}) {
|
|
if (!text) return null;
|
|
|
|
try {
|
|
if (typeof window.kap !== 'function') {
|
|
console.error("Paragraph Layout: kap function not available. Make sure knuth-and-plass.js is loaded.");
|
|
return null;
|
|
}
|
|
|
|
const processedText = this.processTextForLayout(text);
|
|
const layoutOptions = {
|
|
width: options.width || this.config.maxLineWidth,
|
|
fontSize: options.fontSize || this.config.defaultFontSize,
|
|
fontFamily: options.fontFamily || this.config.defaultFontFamily,
|
|
lineHeight: options.lineHeight || this.config.defaultLineHeight,
|
|
lineHeightPx: options.lineHeightPx,
|
|
fontFeatureSettings: options.fontFeatureSettings || this.config.defaultFontFeatureSettings,
|
|
fontVariantCaps: options.fontVariantCaps || null,
|
|
tolerance: options.tolerance || 3,
|
|
demerits: options.demerits || {
|
|
line: 10,
|
|
flagged: 100,
|
|
fitness: 3000
|
|
}
|
|
};
|
|
|
|
this.updateFont(
|
|
layoutOptions.fontSize,
|
|
layoutOptions.fontFamily,
|
|
layoutOptions.fontFeatureSettings,
|
|
layoutOptions.fontVariantCaps
|
|
);
|
|
const numericFontSize = parseFloat(layoutOptions.fontSize) || 16;
|
|
const lineHeightPx = layoutOptions.lineHeightPx || (numericFontSize * layoutOptions.lineHeight);
|
|
|
|
const measure = options.measures || [layoutOptions.width];
|
|
|
|
if (this.config.debugMode) {
|
|
console.log("Paragraph Layout: Calculating layout for text", {
|
|
text: processedText,
|
|
measure,
|
|
options: layoutOptions
|
|
});
|
|
}
|
|
|
|
const layout = window.kap(
|
|
processedText,
|
|
this.measureText.bind(this),
|
|
measure,
|
|
this.config.hyphenationEnabled,
|
|
layoutOptions.tolerance,
|
|
layoutOptions.demerits
|
|
);
|
|
|
|
if (!layout || !layout.breaks || !layout.nodes) {
|
|
console.warn("Paragraph Layout: Failed to calculate layout for text");
|
|
return null;
|
|
}
|
|
|
|
if (this.config.debugMode) {
|
|
console.log("Paragraph Layout: Layout calculated", {
|
|
breaks: layout.breaks.length,
|
|
nodes: layout.nodes.length
|
|
});
|
|
}
|
|
|
|
return {
|
|
...layout,
|
|
originalText: text,
|
|
processedText,
|
|
width: layoutOptions.width,
|
|
lineHeight: layoutOptions.lineHeight,
|
|
lineHeightPx,
|
|
fontSize: layoutOptions.fontSize,
|
|
fontFamily: layoutOptions.fontFamily
|
|
};
|
|
} catch (error) {
|
|
console.error("Error calculating layout:", error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
setDebugMode(enabled) {
|
|
this.updateConfig({ debugMode: enabled });
|
|
console.log(`Paragraph Layout: Debug mode ${enabled ? 'enabled' : 'disabled'}`);
|
|
}
|
|
}
|
|
|
|
const ParagraphLayout = new ParagraphLayoutModule();
|
|
|
|
if (window.moduleRegistry) {
|
|
window.moduleRegistry.register(ParagraphLayout);
|
|
}
|
|
|
|
export { ParagraphLayout };
|
|
|
|
window.ParagraphLayout = ParagraphLayout;
|