Repository navigation
Expand file tree
/
Copy pathruby.html
More file actions
228 lines (215 loc) · 9.95 KB
/
Copy pathruby.html
File metadata and controls
228 lines (215 loc) · 9.95 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Symbolic Web demo — ruby + lang=zbl + your preferred symbol set</title>
<style>
:root { color-scheme: dark; }
@font-face {
font-family: 'BlissaryFont';
src: url('data/processed/BlissaryFont-Regular.woff2') format('woff2');
font-display: swap;
}
body { font: 16px/1.7 system-ui, sans-serif; background: #0d1117; color: #e6edf3; margin: 0; padding: 24px; }
main { max-width: 860px; margin: 0 auto; }
h1 { font-size: 20px; margin-bottom: 2px; }
p.sub { color: #8b949e; font-size: 13.5px; margin-top: 0; }
textarea { width: 100%; min-height: 90px; background: #161b22; color: #e6edf3;
border: 1px solid #30363d; border-radius: 6px; padding: 10px; font: inherit; }
.controls { display: flex; gap: 10px; flex-wrap: wrap; align-items: center; margin: 10px 0 18px; }
button { padding: 8px 14px; background: #238636; color: #fff; border: 0; border-radius: 6px;
font-size: 14px; cursor: pointer; }
button.secondary { background: #21262d; border: 1px solid #30363d; color: #e6edf3; }
label { font-size: 13px; color: #8b949e; }
select { background: #161b22; color: #e6edf3; border: 1px solid #30363d; border-radius: 6px; padding: 6px; }
/* ---- the ruby demo surface ---- */
/* Native ruby interlinear layout fights oversized glyphs (annotation dips
into the base line), so each ruby is forced into a two-row stack while
keeping <ruby>/<rt> semantics for AT and the WAI-Adapt model. */
.reading { font-size: 20px; line-height: 2.8; background: #161b22; border: 1px solid #30363d;
border-radius: 8px; padding: 22px 26px; }
ruby { display: inline-flex; flex-direction: column-reverse; align-items: center;
vertical-align: middle; }
ruby rt { display: block; font-size: 12px; padding-bottom: 3px; }
ruby .base-text { display: block; }
.bliss-glyph { font-family: 'BlissaryFont', sans-serif; font-size: 26px;
color: #f0f6fc; }
.ext-symbol { height: 28px; vertical-align: middle; }
.unresolved { opacity: 0.6; }
.composed svg { height: 28px; width: auto; display: inline-block; vertical-align: middle; }
/* display modes — the WAI-Adapt "user preference" model */
body[data-mode="text"] ruby rt { display: none; }
body[data-mode="symbols"] .base-text { display: none; }
.stats { color: #8b949e; font-size: 13px; margin-top: 12px; }
.stats b { color: #e6edf3; }
</style>
</head>
<body data-mode="both">
<main>
<h1>Symbolic web, today — <ruby lang="zbl"> + your preferred symbol set</h1>
<p class="sub">A working sketch of the post-workshop WAI-Adapt direction (matatk/Samsung):
author once in English, annotate each word with a Bliss concept from the BCI-AV registry,
then let the <b>reader</b> choose symbols-above / text-only / symbols-only — and switch
symbol <b>sets</b> (Bliss glyphs vs ARASAAC/Mulberry art via GlobalSymbols links).
Everything runs client-side: SQLite-WASM resolver + webfont. No server, no API keys.</p>
<textarea id="input">So she was considering in her own mind, as well as she could, for the hot day made her feel very sleepy and stupid, whether the pleasure of making a daisy-chain would be worth the trouble of getting up and picking the daisies.</textarea>
<div class="controls">
<button onclick="render()">Annotate</button>
<label>display
<select id="mode" onchange="document.body.dataset.mode=this.value">
<option value="both">symbols above text</option>
<option value="text">text only</option>
<option value="symbols">symbols only</option>
</select>
</label>
<label>symbol set
<select id="set" onchange="render()">
<option value="bliss">Bliss (webfont)</option>
<option value="arasaac">ARASAAC (via GlobalSymbols)</option>
<option value="mulberry">Mulberry (via GlobalSymbols)</option>
</select>
</label>
<span id="status" class="stats"></span>
</div>
<div class="reading" id="output">press Annotate…</div>
<p class="stats" id="stats"></p>
</main>
<script src="https://cdnjs.cloudflare.com/ajax/libs/sql.js/1.10.2/sql-wasm.js"></script>
<script src="resolver.js"></script>
<script>
let db = null, blissBuilderReady = null;
const esc = (s) => String(s ?? '').replace(/[&<>"]/g, c => ({'&':'&','<':'<','>':'>','"':'"'}[c]));
const norm = Resolver.norm;
// lazy-load bliss-svg-builder (hlridge, MPL-2.0) from unpkg for client-side
// composition of words that have no single symbol
function loadBlissBuilder() {
if (blissBuilderReady) return blissBuilderReady;
blissBuilderReady = new Promise((res, rej) => {
const s = document.createElement('script');
s.src = 'https://unpkg.com/bliss-svg-builder@1.0.0/dist/bliss-svg-builder.iife.js';
s.onload = res; s.onerror = rej;
document.head.appendChild(s);
});
return blissBuilderReady;
}
async function init() {
const status = document.getElementById('status');
status.textContent = 'loading resolver (~11 MB gzipped)…';
const SQL = await initSqlJs({ locateFile: f => `https://cdnjs.cloudflare.com/ajax/libs/sql.js/1.10.2/${f}` });
const res = await fetch('data/web/bliss_resolver.bin?v=20261031');
db = new SQL.Database(new Uint8Array(await res.arrayBuffer()));
status.textContent = 'ready';
render();
}
// word -> {bci, pua, gloss} via staged lookup: exact label -> prefix -> head word
function lookup(word) {
// shared staged lookup: exact -> plural retry -> shortest prefix
const rows = Resolver.labelQuery(db, word);
if (!rows.length) return null;
const [key, gloss, pos, pua, bcode] = rows[0];
return { bci: key, pua, gloss, pos, bcode };
}
// compose an unmapped word from parts ("fire+water", or de-compounded) via
// bliss-svg-builder — renders as ONE properly kerned client-side SVG
async function composeWord(word) {
const w = norm(word);
if (!w) return null;
let parts = w.includes('+') ? w.split('+') : null;
if (!parts) {
for (let i = 3; i <= w.length - 3; i++) {
if (Resolver.labelQuery(db, w.slice(0, i)).length &&
Resolver.labelQuery(db, w.slice(i)).length) {
parts = [w.slice(0, i), w.slice(i)];
break;
}
}
}
if (!parts || parts.length < 2) return null;
const codes = parts.map(p => {
const rows = Resolver.labelQuery(db, p);
return rows.length ? rows[0][4] : null; // blissary_code
});
if (codes.some(c => !c)) return null;
await loadBlissBuilder();
try {
const b = new BlissSVGBuilder(`[color=#F0F6FC]||${codes.join('/')}`);
if (b.warnings.length || b.elementCount < 1) return null;
return b.svgCode;
} catch (e) { return null; }
}
const imgCache = new Map();
async function externalImage(bci, set) {
if (imgCache.has(bci)) return imgCache.get(bci);
const r = db.exec(
`SELECT img_url FROM links WHERE bliss_key=? AND dataset=? AND img_url IS NOT NULL
ORDER BY CASE WHEN method LIKE 'label-exact%' THEN 0 ELSE 1 END, score DESC LIMIT 1`,
[bci, set]);
const img = r.length ? r[0].values[0][0] : null;
imgCache.set(bci, img);
return img;
}
async function render() {
if (!db) return;
const text = document.getElementById('input').value;
const set = document.getElementById('set').value;
const out = document.getElementById('output');
const stats = document.getElementById('stats');
const words = text.split(/(\s+)/);
let n = 0, hit = 0, extHit = 0, composed = 0;
const parts = [];
const jobs = [];
for (const w of words) {
if (!w.trim()) { parts.push({ html: ' ' }); continue; }
const lead = w.match(/^[^\w]+/)?.[0] ?? '';
const trail = w.match(/[^\w]+$/)?.[0] ?? '';
const core = w.slice(lead.length, w.length - trail.length);
const found = core ? lookup(core) : null;
n++;
if (!found) {
// Core 2: try client-side composition before giving up on the word
const idx = parts.length;
parts.push({ pending: true, lead: esc(lead), trail: esc(trail) });
jobs.push((async () => {
const svg = core ? await composeWord(core) : null;
if (svg) {
composed++;
hit++;
parts[idx] = { html:
`${parts[idx].lead}<ruby lang="zbl"><span class="base-text">${esc(core)}</span><rt><span class="composed" title="composed client-side by bliss-svg-builder (hlridge)">${svg}</span></rt></ruby>${parts[idx].trail}` };
} else {
parts[idx] = { html: esc(w) };
}
})());
continue;
}
hit++;
const idx = parts.length;
parts.push({ pending: true, lead: esc(lead), trail: esc(trail), found });
jobs.push((async () => {
let inner;
if (set === 'bliss') {
const ch = found.pua ? String.fromCodePoint(parseInt(found.pua.slice(2), 16)) : '';
inner = `<span class="bliss-glyph" lang="zbl">${ch}</span>`;
} else {
const img = await externalImage(found.bci, set);
if (img) { extHit++; inner = `<img class="ext-symbol" src="${esc(img)}" alt="${esc(found.gloss)}" title="${esc(found.gloss)} (ARASAAC-style art hotlinked from GlobalSymbols)">`; }
else {
const ch = found.pua ? String.fromCodePoint(parseInt(found.pua.slice(2), 16)) : '';
inner = `<span class="bliss-glyph unresolved" lang="zbl" title="no ${set} link — falling back to Bliss">${ch}</span>`;
}
}
parts[idx] = { html:
`${parts[idx].lead}<ruby lang="zbl"><span class="base-text">${esc(core)}</span><rt>${inner}</rt></ruby>${parts[idx].trail}` };
})());
}
await Promise.all(jobs);
out.innerHTML = parts.map(p => p.html).join('');
stats.innerHTML = `words: <b>${n}</b> · annotated: <b>${hit}</b> (${(100*hit/n).toFixed(0)}%)` +
(composed ? ` · <b>${composed}</b> composed client-side` : '') +
(set !== 'bliss' ? ` · ${set} art available for <b>${extHit}</b> of those (rest fall back to Bliss — the coverage gap is the demo)` : '');
}
init().catch(e => { document.getElementById('status').textContent = 'error: ' + e; });
</script>
</body>
</html>