feat(reader): implement FEAT-01 Focus Zen Mode (100% vh/vw, auto-hiding HUD, hotkeys F/Esc, native fullscreen)
This commit is contained in:
@@ -49,6 +49,111 @@ export function formatTranslationWithSpoiler(text) {
|
||||
return `${escapeHtml(cleanText)} <span class="grammar-hint-badge" title="Нажмите, чтобы показать форму" onclick="this.classList.toggle('active')"><span class="badge-label">ℹ️ форма</span><span class="badge-body">${escapeHtml(noteText)}</span></span>`;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Lightweight Native Markdown Parsers (Tables & Notes for Murphy Grammar)
|
||||
// Zero-dependency, safe escaping first, pure ES6
|
||||
// -----------------------------------------------------------------------
|
||||
export function renderMarkdownInline(text) {
|
||||
if (!text) return '';
|
||||
let str = escapeHtml(text);
|
||||
// Bold: **text**
|
||||
str = str.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
|
||||
// Inline code: `text`
|
||||
str = str.replace(/`([^`]+)`/g, '<code class="grammar-inline-code">$1</code>');
|
||||
// Italic: *text* (excluding already formatted strong)
|
||||
str = str.replace(/(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)/g, '<em>$1</em>');
|
||||
return str;
|
||||
}
|
||||
|
||||
export function renderMarkdownTable(mdText) {
|
||||
if (!mdText || typeof mdText !== 'string' || !mdText.trim()) return '';
|
||||
const rawLines = mdText.trim().split(/\r?\n/).map(l => l.trim()).filter(l => l.length > 0);
|
||||
const tableLines = rawLines.filter(l => l.startsWith('|') && l.endsWith('|'));
|
||||
if (tableLines.length < 2) {
|
||||
return `<div class="grammar-table-raw"><pre>${escapeHtml(mdText)}</pre></div>`;
|
||||
}
|
||||
|
||||
const splitRow = (rowStr) => {
|
||||
const inner = rowStr.replace(/^\|/, '').replace(/\|$/, '');
|
||||
return inner.split(/(?<!\\)\|/).map(c => c.trim().replace(/\\\|/g, '|'));
|
||||
};
|
||||
|
||||
const headers = splitRow(tableLines[0]);
|
||||
const alignRow = splitRow(tableLines[1]);
|
||||
const alignments = alignRow.map(col => {
|
||||
const tc = col.trim();
|
||||
if (tc.startsWith(':') && tc.endsWith(':')) return 'center';
|
||||
if (tc.endsWith(':')) return 'right';
|
||||
return 'left';
|
||||
});
|
||||
|
||||
const rows = [];
|
||||
for (let r = 2; r < tableLines.length; r++) {
|
||||
rows.push(splitRow(tableLines[r]));
|
||||
}
|
||||
|
||||
let html = '<div class="grammar-table-wrap"><table class="grammar-cheatsheet-table"><thead><tr>';
|
||||
headers.forEach((h, idx) => {
|
||||
const align = alignments[idx] || 'left';
|
||||
html += `<th style="text-align: ${align}">${renderMarkdownInline(h)}</th>`;
|
||||
});
|
||||
html += '</tr></thead><tbody>';
|
||||
|
||||
rows.forEach(row => {
|
||||
html += '<tr>';
|
||||
row.forEach((cell, idx) => {
|
||||
const align = alignments[idx] || 'left';
|
||||
html += `<td style="text-align: ${align}">${renderMarkdownInline(cell)}</td>`;
|
||||
});
|
||||
html += '</tr>';
|
||||
});
|
||||
html += '</tbody></table></div>';
|
||||
return html;
|
||||
}
|
||||
|
||||
export function renderMarkdownNotes(mdText) {
|
||||
if (!mdText || typeof mdText !== 'string' || !mdText.trim()) return '';
|
||||
const lines = mdText.split(/\r?\n/);
|
||||
const out = [];
|
||||
let inList = false;
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const trimmed = lines[i].trim();
|
||||
|
||||
if (trimmed.startsWith('- ') || trimmed.startsWith('* ')) {
|
||||
if (!inList) {
|
||||
out.push('<ul class="grammar-notes-list">');
|
||||
inList = true;
|
||||
}
|
||||
const itemText = trimmed.replace(/^[-*]\s+/, '');
|
||||
out.push(`<li>${renderMarkdownInline(itemText)}</li>`);
|
||||
continue;
|
||||
} else if (inList) {
|
||||
out.push('</ul>');
|
||||
inList = false;
|
||||
}
|
||||
|
||||
if (trimmed.startsWith('#### ')) {
|
||||
out.push(`<h5 class="grammar-note-h5">${renderMarkdownInline(trimmed.replace(/^####\s+/, ''))}</h5>`);
|
||||
} else if (trimmed.startsWith('### ')) {
|
||||
out.push(`<h4 class="grammar-note-h4">${renderMarkdownInline(trimmed.replace(/^###\s+/, ''))}</h4>`);
|
||||
} else if (trimmed.startsWith('## ')) {
|
||||
out.push(`<h3 class="grammar-note-h3">${renderMarkdownInline(trimmed.replace(/^##\s+/, ''))}</h3>`);
|
||||
} else if (trimmed.startsWith('> ')) {
|
||||
out.push(`<blockquote class="grammar-note-quote">${renderMarkdownInline(trimmed.replace(/^>\s+/, ''))}</blockquote>`);
|
||||
} else if (trimmed === '') {
|
||||
// blank line separator
|
||||
} else {
|
||||
out.push(`<p class="grammar-note-p">${renderMarkdownInline(trimmed)}</p>`);
|
||||
}
|
||||
}
|
||||
if (inList) {
|
||||
out.push('</ul>');
|
||||
}
|
||||
|
||||
return `<div class="grammar-notes-box">${out.join('\n')}</div>`;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Toast Notifications
|
||||
// -----------------------------------------------------------------------
|
||||
@@ -168,6 +273,11 @@ export function closeModal(modalElem) {
|
||||
// View Switcher Helper
|
||||
// -----------------------------------------------------------------------
|
||||
export function switchView(targetViewId) {
|
||||
if (targetViewId !== 'reader-view') {
|
||||
document.body.classList.remove('zen-mode-active');
|
||||
const hud = document.getElementById('reader-zen-hud');
|
||||
if (hud) hud.classList.add('hidden');
|
||||
}
|
||||
const allViews = [
|
||||
'dictionary-view',
|
||||
'srs-view',
|
||||
@@ -286,6 +396,9 @@ export const ui = {
|
||||
playTextSpeech,
|
||||
cleanPromptTranslation,
|
||||
formatTranslationWithSpoiler,
|
||||
renderMarkdownInline,
|
||||
renderMarkdownTable,
|
||||
renderMarkdownNotes,
|
||||
};
|
||||
|
||||
export default ui;
|
||||
|
||||
Reference in New Issue
Block a user