Checkpoint current interactive fiction changes
This commit is contained in:
@@ -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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>');
|
||||
}
|
||||
|
||||
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');
|
||||
|
||||
Reference in New Issue
Block a user