Files
2026-02-10 22:11:06 -05:00

190 lines
5.8 KiB
JavaScript

(() => {
const endpoint = window.PROOFREAD_ENDPOINT;
if (!endpoint) return;
const csrfNameEl = document.getElementById('proofreadCsrfName');
const csrfHashEl = document.getElementById('proofreadCsrfHash');
const resize = (el) => {
if (!el) return;
el.style.height = 'auto';
el.style.height = `${el.scrollHeight}px`;
};
const isSafeMatch = (m) => {
const issueType = (m.rule?.issueType || '').toLowerCase();
const repl = m.replacements || [];
if (repl.length !== 1) return false;
const len = m.length ?? 0;
if (len > 25) return false;
if (issueType === 'misspelling') return true;
const msg = (m.message || '').toLowerCase();
if (msg.includes('whitespace') || msg.includes('space') || msg.includes('punctuation')) return true;
return false;
};
const applyMatches = (text, matches) => {
const sorted = [...matches].sort((a, b) => (b.offset || 0) - (a.offset || 0));
let out = text;
for (const m of sorted) {
const offset = m.offset || 0;
const length = m.length || 0;
const repl = m.replacements?.[0]?.value;
if (typeof repl !== 'string') continue;
out = out.slice(0, offset) + repl + out.slice(offset + length);
}
return out;
};
const refreshCsrf = (hash) => {
if (!hash) return;
if (csrfHashEl) csrfHashEl.value = hash;
const name = csrfNameEl ? csrfNameEl.value : null;
if (!name) return;
document.querySelectorAll(`input[name="${name}"]`).forEach((el) => {
el.value = hash;
});
};
const setStatus = (targetId, message, isError = false) => {
const status = document.querySelector(`[data-status-for="${targetId}"]`);
if (!status) return;
status.textContent = message;
status.classList.toggle('text-danger', !!isError);
};
const clearList = (targetId) => {
const list = document.querySelector(`[data-list-for="${targetId}"]`);
if (list) list.innerHTML = '';
};
const renderRisky = (targetId, risky, textarea) => {
const list = document.querySelector(`[data-list-for="${targetId}"]`);
if (!list) return;
list.innerHTML = '';
risky.forEach((match) => {
const li = document.createElement('li');
li.className = 'mb-1';
const suggestion = match.replacements?.[0]?.value || '(no suggestion)';
const label = document.createElement('span');
label.textContent = `${match.message || 'Suggestion'}${suggestion}`;
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'btn btn-link btn-sm ps-0';
btn.textContent = 'Apply';
btn.addEventListener('click', () => {
textarea.value = applyMatches(textarea.value, [match]);
resize(textarea);
textarea.dispatchEvent(new Event('input', { bubbles: true }));
li.remove();
});
li.appendChild(label);
li.appendChild(document.createTextNode(' '));
li.appendChild(btn);
list.appendChild(li);
});
};
const proofreadField = async (textarea) => {
if (!textarea) return { ok: false };
const targetId = textarea.id || textarea.name || '';
const text = textarea.value || '';
if (!text.trim()) {
setStatus(targetId, 'Enter text to proofread.', true);
clearList(targetId);
return { ok: false };
}
setStatus(targetId, 'Checking...');
clearList(targetId);
const body = new URLSearchParams();
body.append('text', text);
const name = csrfNameEl ? csrfNameEl.value : null;
const hash = csrfHashEl ? csrfHashEl.value : null;
if (name && hash) body.append(name, hash);
let data;
try {
const resp = await fetch(endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body,
});
data = await resp.json();
} catch (err) {
setStatus(targetId, 'Network error.', true);
return { ok: false, error: 'Network error.' };
}
refreshCsrf(data?.csrfHash);
if (!data || !data.ok) {
setStatus(targetId, data?.error || 'Proofread failed.', true);
return { ok: false, error: data?.error || 'Proofread failed.' };
}
const matches = data.result?.matches || [];
if (!matches.length) {
setStatus(targetId, 'No issues found.');
return { ok: true, safe: 0, risky: 0 };
}
const safe = matches.filter(isSafeMatch);
const risky = matches.filter((m) => !isSafeMatch(m));
const updated = applyMatches(text, safe);
if (updated !== text) {
textarea.value = updated;
resize(textarea);
textarea.dispatchEvent(new Event('input', { bubbles: true }));
}
renderRisky(targetId, risky, textarea);
setStatus(targetId, `Auto-fixed: ${safe.length}. Needs review: ${risky.length}.`);
return { ok: true, safe: safe.length, risky: risky.length };
};
const proofreadAll = async () => {
const btn = document.getElementById('proofreadAllBtn');
const summary = document.getElementById('proofreadAllStatus');
const fields = Array.from(document.querySelectorAll('textarea.review-field'));
if (!fields.length) return;
if (btn) btn.disabled = true;
if (summary) summary.textContent = 'Checking all reviews...';
let totalSafe = 0;
let totalRisky = 0;
let errors = 0;
for (const field of fields) {
const result = await proofreadField(field);
if (result?.safe) totalSafe += result.safe;
if (result?.risky) totalRisky += result.risky;
if (!result?.ok) errors += 1;
}
if (summary) {
summary.textContent = `Auto-fixed: ${totalSafe}. Needs review: ${totalRisky}. ${errors ? 'Errors on ' + errors + ' field(s).' : ''}`;
}
if (btn) btn.disabled = false;
};
document.addEventListener('DOMContentLoaded', () => {
const btn = document.getElementById('proofreadAllBtn');
if (btn) btn.addEventListener('click', proofreadAll);
});
})();