MediaWiki:PreDiphicTyper.js
来自HUIJIA FUN
注意:在发布之后,您可能需要清除浏览器缓存才能看到所作出的更改的影响。
- Firefox或Safari:按住Shift的同时单击刷新,或按Ctrl-F5或Ctrl-R(Mac为⌘-R)
- Google Chrome:按Ctrl-Shift-R(Mac为⌘-Shift-R)
- Edge:按住Ctrl的同时单击刷新,或按Ctrl-F5。
/* MediaWiki:PreDiphicTyper.js
* Pre-Diphic打字页(移植自PreDiphic_Typer.html;Pre-Diphic Mono 1.1 / Keyman pre_diphic 1.1)
* 只在页面里有 <div id="pd-typer"></div> 时运行,由MediaWiki:Common.js按需加载。
* 字体由MediaWiki:Common.css的@font-face提供,这里不内嵌字体。
*/
(function () {
'use strict';
/* ===== 转换逻辑:与PreDiphic_Typer.html第67–101行一致。改规则时两边同步。 ===== */
var V = 'aeoiu', VO = [0xEE20, 0xEE21, 0xEE22, 0xEE23, 0xEE24];
var CK = 'pbtdkgrnfvszlmqjxywhc',
CO = [0xEE00, 0xEE01, 0xEE02, 0xEE03, 0xEE04, 0xEE05, 0xEE09, 0xEE0A, 0xEE0B, 0xEE0C, 0xEE0D, 0xEE0E, 0xEE13, 0xEE14, 0xEE15, 0xEE16, 0xEE17, 0xEE18, 0xEE19, 0xEE1A, 0xEE1B];
var KO = { r: 0xEE25, l: 0xEE26, n: 0xEE27, m: 0xEE28 };
var PK = ',.!?-', PO = [0xEE30, 0xEE31, 0xEE32, 0xEE33, 0xEE34];
var DIG = '0123456789';
var NULLC = 0xEE1F, RV = 0xEE29;
var DI = {};
DI[0xEE02 + ':s'] = 0xEE11; DI[0xEE03 + ':z'] = 0xEE12; DI[0xEE0D + ':t'] = 0xEE0F; DI[0xEE0E + ':d'] = 0xEE10;
DI[0xEE0A + ':g'] = 0xEE1D; DI[0xEE1B + ':h'] = 0xEE06; DI[0xEE0E + ':h'] = 0xEE07; DI[0xEE0D + ':h'] = 0xEE08;
function isV(c) { return VO.indexOf(c) !== -1; }
function convert(src) {
var o = [], syl = false;
for (var ch of src) {
var last = o.length ? o[o.length - 1] : null;
if (ch === ' ') { if (syl) { o.push(0x20); syl = false; } else syl = true; continue; }
if (ch === '\n') { o.push(10); syl = false; continue; }
var wasSyl = syl; syl = false;
if (V.indexOf(ch) !== -1) {
var v = VO[V.indexOf(ch)];
if (!wasSyl && last !== null && ((last >= 0xEE00 && last <= 0xEE1D) || isV(last))) o.push(v);
else o.push(NULLC, v);
continue;
}
if (PK.indexOf(ch) !== -1) { o.push(PO[PK.indexOf(ch)]); continue; }
if (DIG.indexOf(ch) !== -1) { o.push(0xEE50 + DIG.indexOf(ch)); continue; }
if (wasSyl) { if (CK.indexOf(ch) !== -1) o.push(CO[CK.indexOf(ch)]); else o.push(ch.codePointAt(0)); continue; }
if (ch === "'") { if (last !== null && isV(last)) o.push(last); continue; }
if (ch === '^') { if (last !== null && last >= 0xEE00 && last <= 0xEE1D) o.push(RV); continue; }
if (ch === 'h' && o.length >= 2 && o[o.length - 2] === NULLC && last === 0xEE21) { o.pop(); o.pop(); o.push(0xEE1C); continue; } // eh
if (last !== null && DI[last + ':' + ch] !== undefined) { o[o.length - 1] = DI[last + ':' + ch]; continue; }
if (KO[ch] !== undefined && last !== null && (isV(last) || last === RV)) { o.push(KO[ch]); continue; }
if (CK.indexOf(ch) !== -1) { o.push(CO[CK.indexOf(ch)]); continue; }
o.push(ch.codePointAt(0));
}
return String.fromCodePoint.apply(null, o);
}
/* 罗马字:音节之间用连字符,词之间用空格(Yo-li Di-pi-ka) */
function romanize(line) {
return line.trim().split(/ {2,}/).map(function (w) { return w.trim().split(/ +/).join('-'); }).join(' ');
}
function escHtml(s) { return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>'); }
/* ===== 界面 ===== */
function el(tag, cls, text) {
var e = document.createElement(tag);
if (cls) e.className = cls;
if (text != null) e.textContent = text;
return e;
}
function btn(label, onclick) {
var b = el('button', null, label);
b.type = 'button';
b.onclick = onclick;
return b;
}
function build(root) {
root.textContent = '';
var src = '';
var out = el('textarea', 'pd pd-typer-out');
out.spellcheck = false;
out.placeholder = '在这里打字…';
out.setAttribute('autocomplete', 'off');
out.setAttribute('autocapitalize', 'off');
out.setAttribute('aria-label', 'Pre-Diphic输入框');
var msg = el('span', 'pd-typer-msg');
var srcEl = el('div', 'pd-typer-src');
/* ===== 编辑模型 =====
* 框里显示的是转换后的Pre-Diphic字,真正保存的是你打的字母(src)。
* 选区、光标位置都先换算回字母位置,再修改src,所以框选删除、在中间插字、Ctrl+Z都能用。 */
var undoStack = [], redoStack = [];
function render(caret) {
out.value = convert(src);
srcEl.textContent = src.replace(/ /g, '␣') || '(空)';
var len = out.value.length;
if (caret == null || caret > len) caret = len;
out.setSelectionRange(caret, caret);
if (caret === len) out.scrollLeft = out.scrollWidth;
}
// 框里的位置 → 字母位置。只认「转换结果正好是框里内容开头」的字母前缀。
// 选区起点取不短于pos的最短前缀;选区终点取不超过pos的最长前缀(连带吃掉后面的空格)
function prefixes() {
var full = out.value, list = [];
for (var k = 0; k <= src.length; k++) {
var f = convert(src.slice(0, k));
if (full.slice(0, f.length) === f) list.push([k, f.length]);
}
return list;
}
function startPos(list, pos) {
for (var i = 0; i < list.length; i++) if (list[i][1] >= pos) return list[i][0];
return src.length;
}
function endPos(list, pos) {
var best = 0;
for (var i = 0; i < list.length; i++) if (list[i][1] <= pos) best = list[i][0];
return best;
}
// 记住上次编辑后光标在字母里的位置:光标没被移动过时直接用它,避免空格(不占位置)造成歧义
var lastOut = { a: 0, b: 0 }, lastSrc = { a: 0, b: 0 };
var lastEdit = { kind: '', time: 0 };
function snapshot() {
var a = out.selectionStart, b = out.selectionEnd, same = a === lastOut.a && b === lastOut.b;
return { src: src, a: a, b: b, sa: same ? lastSrc.a : null, sb: same ? lastSrc.b : null };
}
function restore(st) {
src = st.src; render();
out.setSelectionRange(st.a, st.b);
if (st.sa != null) { lastOut = { a: st.a, b: st.b }; lastSrc = { a: st.sa, b: st.sb }; }
else { lastOut = { a: -1, b: -1 }; }
lastEdit = { kind: '', time: 0 };
}
function undo() { if (!undoStack.length) return; redoStack.push(snapshot()); restore(undoStack.pop()); }
function redo() { if (!redoStack.length) return; undoStack.push(snapshot()); restore(redoStack.pop()); }
// 用text替换当前选区;没有选区时,dir=-1删前一个字母,dir=1删后一个字母
function replaceSel(text, dir) {
var a = out.selectionStart, b = out.selectionEnd, ka, kb;
if (a === lastOut.a && b === lastOut.b) {
ka = lastSrc.a; kb = lastSrc.b;
} else if (a === 0 && b === out.value.length) {
ka = 0; kb = src.length; // 全选
} else if (a === b && a === out.value.length) {
ka = kb = src.length; // 光标在最后
} else {
var list = prefixes();
ka = startPos(list, a);
kb = a === b ? ka : Math.max(ka, endPos(list, b));
}
if (ka === kb && dir < 0) ka = Math.max(0, ka - 1);
if (ka === kb && dir > 0) kb = Math.min(src.length, kb + 1);
if (ka === kb && !text) return;
// 连续打字合并成一步撤销;遇到空格、换行、删除或停顿超过1秒就另起一步
var kind = text ? (/[\s]/.test(text) || text.length > 1 ? 'other' : 'type') : 'delete';
var now = Date.now();
var merge = kind !== 'other' && kind === lastEdit.kind && now - lastEdit.time < 1000 &&
a === lastOut.a && b === lastOut.b;
if (!merge) {
undoStack.push(snapshot());
if (undoStack.length > 500) undoStack.shift();
}
redoStack = [];
lastEdit = { kind: kind, time: now };
src = src.slice(0, ka) + text + src.slice(kb);
var caretSrc = ka + text.length;
var caretOut = convert(src.slice(0, caretSrc)).length;
render(caretOut);
lastOut = { a: out.selectionStart, b: out.selectionEnd };
lastSrc = { a: caretSrc, b: caretSrc };
}
function add(s) { replaceSel(s, 0); }
function flash(t) { msg.textContent = t; setTimeout(function () { msg.textContent = ''; }, 1800); }
function clip(t, m) {
var done = function () { flash(m); };
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(t).then(done, function () { fallback(t); done(); });
} else { fallback(t); done(); }
}
function fallback(t) {
var ta = el('textarea'); ta.value = t; ta.style.position = 'fixed'; ta.style.opacity = '0';
document.body.appendChild(ta); ta.select(); document.execCommand('copy'); ta.remove();
}
function lines() { return src.split('\n').filter(function (l) { return l.trim(); }); }
out.addEventListener('keydown', function (e) {
if (!(e.ctrlKey || e.metaKey) || e.altKey) return;
var k = e.key.toLowerCase();
if (k === 'z' && !e.shiftKey) { e.preventDefault(); undo(); }
else if (k === 'y' || (k === 'z' && e.shiftKey)) { e.preventDefault(); redo(); }
});
out.addEventListener('beforeinput', function (e) {
var t = e.inputType;
e.preventDefault();
if (t === 'insertText' || t === 'insertCompositionText' || t === 'insertReplacementText') {
if (e.data) add(e.data.toLowerCase());
} else if (t === 'insertLineBreak' || t === 'insertParagraph') {
add('\n');
} else if (t === 'insertFromDrop' || t === 'insertFromPaste') {
add(((e.dataTransfer && e.dataTransfer.getData('text')) || '').toLowerCase());
} else if (t === 'historyUndo') {
undo();
} else if (t === 'historyRedo') {
redo();
} else if (t.indexOf('delete') === 0) {
// 有选区:删除整个选区(框选删除、剪切);没有选区:Backspace删前一个字母,Delete删后一个
replaceSel('', /Forward$/.test(t) ? 1 : -1);
}
});
out.addEventListener('paste', function (e) { e.preventDefault(); add((e.clipboardData.getData('text') || '').toLowerCase()); });
var size = el('input'); size.type = 'range'; size.min = 24; size.max = 120; size.value = 48;
size.oninput = function () { out.style.fontSize = size.value + 'px'; };
var sizeLab = el('label', null, '字号 '); sizeLab.appendChild(size);
var row = el('div', 'pd-typer-row');
row.appendChild(sizeLab);
row.appendChild(btn('复制文字', function () { clip(out.value, '已复制文字'); }));
row.appendChild(btn('复制为模板', function () {
clip(lines().map(function (l) {
return '{{PD|内容=' + l.trim() + '}}';
}).join('<br>\n'), '已复制{{PD}}模板');
}));
row.appendChild(btn('复制为HTML', function () {
clip(lines().map(function (l) {
return '<span class="pd pd-v" title="' + escHtml(romanize(l)) + '">' + escHtml(convert(l)) + '</span>';
}).join('<br>\n'), '已复制HTML');
}));
row.appendChild(btn('下载.txt', function () {
var a = document.createElement('a');
a.href = URL.createObjectURL(new Blob([out.value], { type: 'text/plain;charset=utf-8' }));
a.download = 'pre-diphic.txt'; a.click();
}));
row.appendChild(btn('清空', function () { out.select(); replaceSel('', 0); out.focus(); }));
row.appendChild(msg);
var kbC = el('div', 'pd-typer-kb'), kbV = el('div', 'pd-typer-kb');
function key(host, items) {
items.forEach(function (it) {
var b = btn('', function () { add(it[1]); out.focus(); });
b.appendChild(el('span', 'pd', String.fromCodePoint(it[2])));
b.appendChild(el('small', null, it[0]));
host.appendChild(b);
});
}
var cons = ['p', 'b', 't', 'd', 'k', 'g', 'ch', 'zh', 'sh', 'r', 'n', 'f', 'v', 's', 'z', 'st', 'zd', 'ts', 'dz', 'l', 'm', 'q', 'j', 'x', 'y', 'w', 'h', 'c', 'eh', 'ng'];
key(kbC, cons.map(function (c, i) { return [c, c, 0xEE00 + i]; }));
key(kbV, Array.from('aeoiu').map(function (v, i) { return [v, v, 0xEE20 + i]; })
.concat(Array.from('rlnm').map(function (v, i) { return [v + '(尾)', v, 0xEE25 + i]; }))
.concat([['^', '^', 0xEE29]]));
kbV.appendChild(btn("长音 '", function () { add("'"); out.focus(); }));
kbV.appendChild(btn('空格 ␣', function () { add(' '); out.focus(); }));
kbV.appendChild(btn('换词 ␣␣', function () { add(' '); out.focus(); }));
root.appendChild(out);
root.appendChild(row);
root.appendChild(el('div', 'pd-typer-label', '你打的字母:'));
root.appendChild(srcEl);
root.appendChild(el('div', 'pd-typer-h', '点击输入'));
root.appendChild(kbC);
root.appendChild(kbV);
render();
}
function init($content) {
var root = ($content && $content[0] && $content[0].querySelector('#pd-typer')) || document.getElementById('pd-typer');
if (root && !root.getAttribute('data-ready')) {
root.setAttribute('data-ready', '1');
build(root);
}
}
if (window.mw && mw.hook) mw.hook('wikipage.content').add(init);
else init();
}());
