Checkpoint current interactive fiction changes

This commit is contained in:
2026-06-03 12:59:43 +02:00
parent 61127c0a92
commit bccefd2a68
11 changed files with 342 additions and 895 deletions
+90 -2
View File
@@ -26,6 +26,7 @@ class LayoutRendererModule extends BaseModule {
'adjustJustification',
'decorateInlineWord',
'applyGlossaryEntries',
'applyGlossaryEntriesToInline',
'normalizeGlossaryText',
'normalizeGlossaryToken',
'normalizeGlossaryCompact',
@@ -34,6 +35,7 @@ class LayoutRendererModule extends BaseModule {
'decorateGlossarySegment',
'decorateGlossaryRange',
'decorateGlossaryWord',
'renderGlossaryHtml',
'ensureGlossaryTooltip',
'showGlossaryTooltip',
'hideGlossaryTooltip',
@@ -390,6 +392,81 @@ class LayoutRendererModule extends BaseModule {
});
}
applyGlossaryEntriesToInline(container, entries = []) {
if (!container || !Array.isArray(entries) || entries.length === 0) return;
const filter = window.NodeFilter || NodeFilter;
entries
.filter(entry => entry && entry.term && entry.definition)
.forEach(entry => {
const walker = document.createTreeWalker(container, filter.SHOW_TEXT, {
acceptNode: (node) => {
if (!node?.nodeValue?.trim()) return filter.FILTER_REJECT;
if (node.parentElement?.closest?.('.story-glossary-word')) return filter.FILTER_REJECT;
return filter.FILTER_ACCEPT;
}
});
const textNodes = [];
let node;
while ((node = walker.nextNode())) {
textNodes.push(node);
}
textNodes.forEach((textNode) => {
if (!textNode.parentNode) return;
const original = textNode.nodeValue || '';
if (!this.normalizeGlossaryText(original)) return;
const matches = [];
this.buildGlossaryTermPatterns(entry.term).forEach((pattern) => {
const matcher = new RegExp(`(^|\\s)(${pattern})(?=\\s|$|[.,;:!?])`, 'giu');
let match;
while ((match = matcher.exec(original)) !== null) {
const matchStart = match.index + match[1].length;
const matchEnd = matchStart + match[2].length;
if (!matches.some(existing => existing.start === matchStart && existing.end === matchEnd)) {
matches.push({ start: matchStart, end: matchEnd });
}
}
});
this.buildCompactGlossaryTermPatterns(entry.term).forEach((pattern) => {
const compactOriginal = this.normalizeGlossaryCompact(original);
if (!compactOriginal) return;
const matcher = new RegExp(pattern, 'giu');
let match;
while ((match = matcher.exec(compactOriginal)) !== null) {
if (match.index !== 0 || match[0].length !== compactOriginal.length) continue;
if (!matches.some(existing => existing.start === 0 && existing.end === original.length)) {
matches.push({ start: 0, end: original.length });
}
}
});
matches.sort((a, b) => a.start - b.start);
if (matches.length === 0) return;
const parent = textNode.parentNode;
let cursor = 0;
matches.forEach((match) => {
if (match.start < cursor) return;
const before = original.slice(cursor, match.start);
const matched = original.slice(match.start, match.end);
if (before) parent.insertBefore(document.createTextNode(before), textNode);
if (matched) {
const gloss = document.createElement('span');
gloss.textContent = matched;
this.decorateGlossaryWord(gloss, entry);
parent.insertBefore(gloss, textNode);
}
cursor = match.end;
});
const after = original.slice(cursor);
if (after) parent.insertBefore(document.createTextNode(after), textNode);
parent.removeChild(textNode);
});
});
}
normalizeGlossaryText(text) {
return String(text || '')
.normalize('NFC')
@@ -568,6 +645,17 @@ class LayoutRendererModule extends BaseModule {
word.addEventListener('blur', this.hideGlossaryTooltip);
}
renderGlossaryHtml(text) {
const markupParser = this.getModule('markup-parser');
if (markupParser && typeof markupParser.markdownToHtml === 'function') {
return markupParser.markdownToHtml(String(text || ''));
}
return String(text || '')
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
}
ensureGlossaryTooltip() {
let tooltip = document.getElementById('story_glossary_tooltip');
if (tooltip) return tooltip;
@@ -598,8 +686,8 @@ class LayoutRendererModule extends BaseModule {
const tooltip = this.ensureGlossaryTooltip();
const title = tooltip.querySelector('.story-glossary-tooltip-title');
const body = tooltip.querySelector('.story-glossary-tooltip-body');
if (title) title.textContent = word.dataset.glossaryTerm || this.normalizeGlossaryText(word.textContent || '');
if (body) body.textContent = word.dataset.glossary || '';
if (title) title.innerHTML = this.renderGlossaryHtml(word.dataset.glossaryTerm || this.normalizeGlossaryText(word.textContent || ''));
if (body) body.innerHTML = this.renderGlossaryHtml(word.dataset.glossary || '');
tooltip.dataset.anchorId = word.id || '';
tooltip.__anchorElement = word;
tooltip.classList.add('visible');