190 lines
5.8 KiB
JavaScript
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);
|
|
});
|
|
})();
|